3
0
mirror of https://github.com/Qortal/q-share.git synced 2025-01-30 14:52:20 +00:00

Massive Category overhaul.

Many new categories added, All categories have an "Other" field that is sorted last

Media category removed, its subcategories are now main categories. WARNING: This is a breaking change that will disrupt categories for some currently published files

Categories associated with Copyright Infringement have been removed.

Categories are sorted by name, "Other" is always last

The FileList component now only stores the file list. The rest of it was moved to Home.tsx

Category <select> components moved into a CategoryList.tsx component

Icons now load before file title in FileContent.tsx

Icons are an optional field of each category and loaded dynamically from Category Data to improve performance
This commit is contained in:
Qortal Dev 2024-03-11 09:22:17 -06:00
parent fa0a1dc16a
commit 9d483cf65d
40 changed files with 3131 additions and 3382 deletions

10
.prettierrc Normal file
View File

@ -0,0 +1,10 @@
{
"printWidth": 80,
"singleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "avoid",
"tabWidth": 2,
"semi": true
}

20
package-lock.json generated
View File

@ -17,6 +17,7 @@
"dompurify": "^3.0.6", "dompurify": "^3.0.6",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"prettier": "^3.2.4",
"quill-image-resize-module-react": "^3.0.0", "quill-image-resize-module-react": "^3.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -3429,6 +3430,20 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/prettier": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz",
"integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prop-types": { "node_modules/prop-types": {
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@ -6574,6 +6589,11 @@
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true "dev": true
}, },
"prettier": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz",
"integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ=="
},
"prop-types": { "prop-types": {
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",

View File

@ -19,6 +19,7 @@
"dompurify": "^3.0.6", "dompurify": "^3.0.6",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"prettier": "^3.2.4",
"quill-image-resize-module-react": "^3.0.0", "quill-image-resize-module-react": "^3.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@ -8,7 +8,7 @@ import { Provider } from "react-redux";
import GlobalWrapper from "./wrappers/GlobalWrapper"; import GlobalWrapper from "./wrappers/GlobalWrapper";
import Notification from "./components/common/Notification/Notification"; import Notification from "./components/common/Notification/Notification";
import { Home } from "./pages/Home/Home"; import { Home } from "./pages/Home/Home";
import { VideoContent } from "./pages/VideoContent/VideoContent"; import { FileContent } from "./pages/FileContent/FileContent.tsx";
import DownloadWrapper from "./wrappers/DownloadWrapper"; import DownloadWrapper from "./wrappers/DownloadWrapper";
import { IndividualProfile } from "./pages/IndividualProfile/IndividualProfile"; import { IndividualProfile } from "./pages/IndividualProfile/IndividualProfile";
@ -26,7 +26,7 @@ function App() {
<CssBaseline /> <CssBaseline />
<Routes> <Routes>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/share/:name/:id" element={<VideoContent />} /> <Route path="/share/:name/:id" element={<FileContent />} />
<Route path="/channel/:name" element={<IndividualProfile />} /> <Route path="/channel/:name" element={<IndividualProfile />} />
</Routes> </Routes>
</GlobalWrapper> </GlobalWrapper>

BIN
src/assets/icons/book.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 28 KiB

BIN
src/assets/icons/image.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -1,4 +1,4 @@
import React, {useEffect, useState} from "react"; import React, { useEffect, useRef, useState } from "react";
import { import {
CrowdfundActionButton, CrowdfundActionButton,
CrowdfundActionButtonRow, CrowdfundActionButtonRow,
@ -6,18 +6,7 @@ import {
ModalBody, ModalBody,
NewCrowdfundTitle, NewCrowdfundTitle,
} from "./Upload-styles"; } from "./Upload-styles";
import { import { Box, Modal, Typography, useTheme } from "@mui/material";
Box,
FormControl,
InputLabel,
MenuItem,
Modal,
OutlinedInput,
Select,
SelectChangeEvent,
Typography,
useTheme,
} from "@mui/material";
import RemoveIcon from "@mui/icons-material/Remove"; import RemoveIcon from "@mui/icons-material/Remove";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
@ -27,13 +16,22 @@ import {useDropzone} from "react-dropzone";
import { setNotification } from "../../state/features/notificationsSlice"; import { setNotification } from "../../state/features/notificationsSlice";
import { objectToBase64 } from "../../utils/toBase64"; import { objectToBase64 } from "../../utils/toBase64";
import { RootState } from "../../state/store"; import { RootState } from "../../state/store";
import {setEditVideo, updateInHashMap, updateVideo,} from "../../state/features/videoSlice"; import {
import {QSHARE_FILE_BASE,} from "../../constants/Identifiers.ts"; setEditFile,
updateFile,
updateInHashMap,
} from "../../state/features/fileSlice.ts";
import { QSHARE_FILE_BASE } from "../../constants/Identifiers.ts";
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll"; import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll";
import { TextEditor } from "../common/TextEditor/TextEditor"; import { TextEditor } from "../common/TextEditor/TextEditor";
import { extractTextFromHTML } from "../common/TextEditor/utils"; import { extractTextFromHTML } from "../common/TextEditor/utils";
import {categories, subCategories, subCategories2, subCategories3} from "../../constants/Categories.ts"; import { allCategoryData } from "../../constants/Categories/1stCategories.ts";
import { titleFormatter } from "../../constants/Misc.ts"; import { titleFormatter } from "../../constants/Misc.ts";
import {
CategoryList,
CategoryListRef,
getCategoriesFromObject,
} from "../common/CategoryList/CategoryList.tsx";
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 }); const shortuid = new ShortUniqueId({ length: 5 });
@ -53,7 +51,7 @@ interface VideoFile {
description: string; description: string;
coverImage?: string; coverImage?: string;
identifier?: string; identifier?: string;
filename?:string filename?: string;
} }
export const EditFile = () => { export const EditFile = () => {
const theme = useTheme(); const theme = useTheme();
@ -62,8 +60,8 @@ export const EditFile = () => {
const userAddress = useSelector( const userAddress = useSelector(
(state: RootState) => state.auth?.user?.address (state: RootState) => state.auth?.user?.address
); );
const editVideoProperties = useSelector( const editFileProperties = useSelector(
(state: RootState) => state.video.editVideoProperties (state: RootState) => state.file.editFileProperties
); );
const [publishes, setPublishes] = useState<any>(null); const [publishes, setPublishes] = useState<any>(null);
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false); const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
@ -75,21 +73,14 @@ export const EditFile = () => {
const [coverImage, setCoverImage] = useState<string>(""); const [coverImage, setCoverImage] = useState<string>("");
const [file, setFile] = useState(null); const [file, setFile] = useState(null);
const [files, setFiles] = useState<VideoFile[]>([]); const [files, setFiles] = useState<VideoFile[]>([]);
const [editCategories, setEditCategories] = useState<string[]>([]);
const [selectedCategoryVideos, setSelectedCategoryVideos] = const categoryListRef = useRef<CategoryListRef>(null);
useState<any>(null);
const [selectedSubCategoryVideos, setSelectedSubCategoryVideos] =
useState<any>(null);
const [selectedSubCategoryVideos2, setSelectedSubCategoryVideos2] =
useState<any>(null);
const [selectedSubCategoryVideos3, setSelectedSubCategoryVideos3] =
useState<any>(null);
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
maxFiles: 10, maxFiles: 10,
maxSize: 419430400, // 400 MB in bytes maxSize: 419430400, // 400 MB in bytes
onDrop: (acceptedFiles, rejectedFiles) => { onDrop: (acceptedFiles, rejectedFiles) => {
const formatArray = acceptedFiles.map((item) => { const formatArray = acceptedFiles.map(item => {
return { return {
file: item, file: item,
title: "", title: "",
@ -98,11 +89,11 @@ export const EditFile = () => {
}; };
}); });
setFiles((prev) => [...prev, ...formatArray]); setFiles(prev => [...prev, ...formatArray]);
let errorString = null; let errorString = null;
rejectedFiles.forEach(({ file, errors }) => { rejectedFiles.forEach(({ file, errors }) => {
errors.forEach((error) => { errors.forEach(error => {
if (error.code === "file-too-large") { if (error.code === "file-too-large") {
errorString = "File must be under 400mb"; errorString = "File must be under 400mb";
} }
@ -120,109 +111,21 @@ export const EditFile = () => {
}, },
}); });
// useEffect(() => {
// if (editVideoProperties) {
// const descriptionString = editVideoProperties?.description || "";
// // Splitting the string at the asterisks
// const parts = descriptionString.split("**");
// // The part within the asterisks
// const extractedString = parts[1];
// // The part after the last asterisks
// const description = parts[2] || ""; // Using '|| '' to handle cases where there is no text after the last **
// setTitle(editVideoProperties?.title || "");
// setDescription(editVideoProperties?.fullDescription || "");
// setCoverImage(editVideoProperties?.videoImage || "");
// // Split the extracted string into key-value pairs
// const keyValuePairs = extractedString.split(";");
// // Initialize variables to hold the category and subcategory values
// let category, subcategory;
// // Loop through each key-value pair
// keyValuePairs.forEach((pair) => {
// const [key, value] = pair.split(":");
// // Check the key and assign the value to the appropriate variable
// if (key === "category") {
// category = value;
// } else if (key === "subcategory") {
// subcategory = value;
// }
// });
// if(category){
// const selectedOption = categories.find((option) => option.id === +category);
// setSelectedCategoryVideos(selectedOption || null);
// }
// if(subcategory){
// const selectedOption = categories.find((option) => option.id === +subcategory);
// setSelectedCategoryVideos(selectedOption || null);
// }
// }
// }, [editVideoProperties]);
useEffect(() => { useEffect(() => {
if (editVideoProperties) { if (editFileProperties) {
setTitle(editVideoProperties?.title || ""); setTitle(editFileProperties?.title || "");
setFiles(editVideoProperties?.files || []) setFiles(editFileProperties?.files || []);
if(editVideoProperties?.htmlDescription){ if (editFileProperties?.htmlDescription) {
setDescription(editVideoProperties?.htmlDescription); setDescription(editFileProperties?.htmlDescription);
} else if (editFileProperties?.fullDescription) {
} else if(editVideoProperties?.fullDescription) { const paragraph = `<p>${editFileProperties?.fullDescription}</p>`;
const paragraph = `<p>${editVideoProperties?.fullDescription}</p>`
setDescription(paragraph); setDescription(paragraph);
} }
setEditCategories(getCategoriesFromObject(editFileProperties));
if (editVideoProperties?.category) {
const selectedOption = categories.find(
(option) => option.id === +editVideoProperties.category
);
setSelectedCategoryVideos(selectedOption || null);
} }
if ( }, [editFileProperties]);
editVideoProperties?.category &&
editVideoProperties?.subcategory &&
subCategories[+editVideoProperties?.category]
) {
const selectedOption = subCategories[
+editVideoProperties?.category
]?.find((option) => option.id === +editVideoProperties.subcategory);
setSelectedSubCategoryVideos(selectedOption || null);
}
if (
editVideoProperties?.category &&
editVideoProperties?.subcategory2 &&
subCategories2[+editVideoProperties?.subcategory]
) {
const selectedOption = subCategories2[
+editVideoProperties?.subcategory
]?.find((option) => option.id === +editVideoProperties.subcategory2);
setSelectedSubCategoryVideos2(selectedOption || null);
}
if (
editVideoProperties?.category &&
editVideoProperties?.subcategory3 &&
subCategories3[+editVideoProperties?.subcategory2]
) {
const selectedOption = subCategories3[
+editVideoProperties?.subcategory2
]?.find((option) => option.id === +editVideoProperties.subcategory3);
setSelectedSubCategoryVideos3(selectedOption || null);
}
}
}, [editVideoProperties]);
const onClose = () => { const onClose = () => {
dispatch(setEditVideo(null)); dispatch(setEditFile(null));
setVideoPropertiesToSetToRedux(null); setVideoPropertiesToSetToRedux(null);
setFile(null); setFile(null);
setTitle(""); setTitle("");
@ -232,10 +135,11 @@ export const EditFile = () => {
async function publishQDNResource() { async function publishQDNResource() {
try { try {
const categoryList = categoryListRef.current?.getSelectedCategories();
if (!title) throw new Error("Please enter a title"); if (!title) throw new Error("Please enter a title");
if (!description) throw new Error("Please enter a description"); if (!description) throw new Error("Please enter a description");
if (!selectedCategoryVideos) throw new Error("Please select a category"); if (!categoryList[0]) throw new Error("Please select a category");
if (!editVideoProperties) return; if (!editFileProperties) return;
if (!userAddress) throw new Error("Unable to locate user address"); if (!userAddress) throw new Error("Unable to locate user address");
if (files.length === 0) throw new Error("Add at least one file"); if (files.length === 0) throw new Error("Add at least one file");
@ -249,7 +153,7 @@ export const EditFile = () => {
"Cannot publish without access to your name. Please authenticate."; "Cannot publish without access to your name. Please authenticate.";
} }
if (editVideoProperties?.user !== username) { if (editFileProperties?.user !== username) {
errorMsg = "Cannot publish another user's resource"; errorMsg = "Cannot publish another user's resource";
} }
@ -262,15 +166,11 @@ export const EditFile = () => {
); );
return; return;
} }
let fileReferences = [] let fileReferences = [];
let listOfPublishes = []; let listOfPublishes = [];
const fullDescription = extractTextFromHTML(description); const fullDescription = extractTextFromHTML(description);
const category = selectedCategoryVideos.id;
const subcategory = selectedSubCategoryVideos?.id || "";
const subcategory2 = selectedSubCategoryVideos2?.id || "";
const subcategory3 = selectedSubCategoryVideos3?.id || "";
const sanitizeTitle = title const sanitizeTitle = title
.replace(/[^a-zA-Z0-9\s-]/g, "") .replace(/[^a-zA-Z0-9\s-]/g, "")
.replace(/\s+/g, "-") .replace(/\s+/g, "-")
@ -278,25 +178,22 @@ export const EditFile = () => {
.trim() .trim()
.toLowerCase(); .toLowerCase();
for (const publish of files) { for (const publish of files) {
if (publish?.identifier) { if (publish?.identifier) {
fileReferences.push(publish) fileReferences.push(publish);
continue continue;
} }
const file = publish.file; const file = publish.file;
const id = uid(); const id = uid();
const identifier = `${QSHARE_FILE_BASE}${sanitizeTitle.slice(0, 30)}_${id}`; const identifier = `${QSHARE_FILE_BASE}${sanitizeTitle.slice(0, 30)}_${id}`;
let fileExtension = ""; let fileExtension = "";
const fileExtensionSplit = file?.name?.split("."); const fileExtensionSplit = file?.name?.split(".");
if (fileExtensionSplit?.length > 1) { if (fileExtensionSplit?.length > 1) {
fileExtension = fileExtensionSplit?.pop() || ""; fileExtension = fileExtensionSplit?.pop() || "";
} }
let firstPartName = fileExtensionSplit[0] let firstPartName = fileExtensionSplit[0];
let filename = firstPartName.slice(0, 15); let filename = firstPartName.slice(0, 15);
@ -312,15 +209,13 @@ export const EditFile = () => {
); );
if (fileExtension) { if (fileExtension) {
filename = `${alphanumericString.trim()}.${fileExtension}` filename = `${alphanumericString.trim()}.${fileExtension}`;
} else { } else {
filename = alphanumericString filename = alphanumericString;
} }
let metadescription = let metadescription =
`**cat:${category};sub:${subcategory};sub2:${subcategory2};sub3:${subcategory3}**` + `**${categoryListRef.current?.getCategoriesFetchString()}**` +
fullDescription.slice(0, 150); fullDescription.slice(0, 150);
const requestBodyVideo: any = { const requestBodyVideo: any = {
@ -339,27 +234,24 @@ export const EditFile = () => {
filename: file.name, filename: file.name,
identifier, identifier,
name, name,
service: 'FILE', service: "FILE",
mimetype: file.type, mimetype: file.type,
size: file.size size: file.size,
}) });
} }
const fileObject: any = { const fileObject: any = {
title, title,
version: editVideoProperties.version, version: editFileProperties.version,
fullDescription, fullDescription,
htmlDescription: description, htmlDescription: description,
commentsId: editVideoProperties.commentsId, commentsId: editFileProperties.commentsId,
category, ...categoryListRef.current?.categoriesToObject(),
subcategory, files: fileReferences,
subcategory2,
subcategory3,
files: fileReferences
}; };
let metadescription = let metadescription =
`**cat:${category};sub:${subcategory};sub2:${subcategory2}**` + `**${categoryListRef.current?.getCategoriesFetchString()}**` +
fullDescription.slice(0, 150); fullDescription.slice(0, 150);
const crowdfundObjectToBase64 = await objectToBase64(fileObject); const crowdfundObjectToBase64 = await objectToBase64(fileObject);
@ -372,7 +264,7 @@ export const EditFile = () => {
data64: crowdfundObjectToBase64, data64: crowdfundObjectToBase64,
title: title.slice(0, 50), title: title.slice(0, 50),
description: metadescription, description: metadescription,
identifier: editVideoProperties.id, identifier: editFileProperties.id,
tag1: QSHARE_FILE_BASE, tag1: QSHARE_FILE_BASE,
filename: `video_metadata.json`, filename: `video_metadata.json`,
}; };
@ -385,7 +277,7 @@ export const EditFile = () => {
setPublishes(multiplePublish); setPublishes(multiplePublish);
setIsOpenMultiplePublish(true); setIsOpenMultiplePublish(true);
setVideoPropertiesToSetToRedux({ setVideoPropertiesToSetToRedux({
...editVideoProperties, ...editFileProperties,
...fileObject, ...fileObject,
}); });
} catch (error: any) { } catch (error: any) {
@ -429,52 +321,10 @@ export const EditFile = () => {
// }); // });
}; };
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 handleOptionSubCategoryChangeVideos2 = (
event: SelectChangeEvent<string>,
subcategories: any[]
) => {
const optionId = event.target.value;
const selectedOption = subcategories.find(
(option) => option.id === +optionId
);
setSelectedSubCategoryVideos2(selectedOption || null);
};
const handleOptionSubCategoryChangeVideos3 = (
event: SelectChangeEvent<string>,
subcategories: any[]
) => {
const optionId = event.target.value;
const selectedOption = subcategories.find(
(option) => option.id === +optionId
);
setSelectedSubCategoryVideos3(selectedOption || null);
};
return ( return (
<> <>
<Modal <Modal
open={!!editVideoProperties} open={!!editFileProperties}
aria-labelledby="modal-title" aria-labelledby="modal-title"
aria-describedby="modal-description" aria-describedby="modal-description"
> >
@ -503,7 +353,7 @@ export const EditFile = () => {
<Typography>Click to add more files</Typography> <Typography>Click to add more files</Typography>
</Box> </Box>
{files.map((file, index) => { {files.map((file, index) => {
const isExistingFile = !!file?.identifier const isExistingFile = !!file?.identifier;
return ( return (
<React.Fragment key={index}> <React.Fragment key={index}>
<Box <Box
@ -513,10 +363,12 @@ export const EditFile = () => {
alignItems: "center", alignItems: "center",
}} }}
> >
<Typography>{isExistingFile? file.filename : file?.file?.name}</Typography> <Typography>
{isExistingFile ? file.filename : file?.file?.name}
</Typography>
<RemoveIcon <RemoveIcon
onClick={() => { onClick={() => {
setFiles((prev) => { setFiles(prev => {
const copyPrev = [...prev]; const copyPrev = [...prev];
copyPrev.splice(index, 1); copyPrev.splice(index, 1);
return copyPrev; return copyPrev;
@ -540,129 +392,21 @@ export const EditFile = () => {
> >
{files?.length > 0 && ( {files?.length > 0 && (
<> <>
<Box sx={{ <Box
display: 'flex', sx={{
flexDirection: 'column', display: "flex",
gap: '20px', flexDirection: "column",
width: '50%' gap: "20px",
}}> width: "100%",
<FormControl fullWidth sx={{ marginBottom: 2 }}> }}
<InputLabel id="Category">Select a Category</InputLabel>
<Select
labelId="Category"
input={<OutlinedInput label="Select a Category" />}
value={selectedCategoryVideos?.id || ""}
onChange={handleOptionCategoryChangeVideos}
> >
{categories.map((option) => ( <CategoryList
<MenuItem key={option.id} value={option.id}> categoryData={allCategoryData}
{option.name} initialCategories={editCategories}
</MenuItem> columns={3}
))} ref={categoryListRef}
</Select> />
</FormControl>
</Box> </Box>
{selectedCategoryVideos && (
<>
<Box sx={{
display: 'flex',
flexDirection: 'column',
gap: '20px',
width: '50%'
}}>
{selectedCategoryVideos &&
subCategories[selectedCategoryVideos?.id] && (
<FormControl fullWidth sx={{ marginBottom: 2 }}>
<InputLabel id="Category">
Select a Sub-Category
</InputLabel>
<Select
labelId="Sub-Category"
input={
<OutlinedInput label="Select a Sub-Category" />
}
value={selectedSubCategoryVideos?.id || ""}
onChange={(e) =>
handleOptionSubCategoryChangeVideos(
e,
subCategories[selectedCategoryVideos?.id]
)
}
>
{subCategories[selectedCategoryVideos.id].map(
(option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
)
)}
</Select>
</FormControl>
)}
{selectedSubCategoryVideos &&
subCategories2[selectedSubCategoryVideos?.id] && (
<FormControl fullWidth sx={{ marginBottom: 2 }}>
<InputLabel id="Category">
Select a Sub-sub-Category
</InputLabel>
<Select
labelId="Sub-Category"
input={
<OutlinedInput label="Select a Sub-sub-Category" />
}
value={selectedSubCategoryVideos2?.id || ""}
onChange={(e) =>
handleOptionSubCategoryChangeVideos2(
e,
subCategories2[selectedSubCategoryVideos?.id]
)
}
>
{subCategories2[selectedSubCategoryVideos.id].map(
(option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
)
)}
</Select>
</FormControl>
)}
{selectedSubCategoryVideos2 &&
subCategories3[selectedSubCategoryVideos2?.id] && (
<FormControl fullWidth sx={{ marginBottom: 2 }}>
<InputLabel id="Category">
Select a Sub-3x-subCategory
</InputLabel>
<Select
labelId="Sub-Category"
input={
<OutlinedInput label="Select a Sub-3x-Category" />
}
value={selectedSubCategoryVideos3?.id || ""}
onChange={(e) =>
handleOptionSubCategoryChangeVideos3(
e,
subCategories3[selectedSubCategoryVideos2?.id]
)
}
>
{subCategories3[selectedSubCategoryVideos2.id].map(
(option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
)
)}
</Select>
</FormControl>
)}
</Box>
</>
)}
</> </>
)} )}
</Box> </Box>
@ -673,7 +417,7 @@ export const EditFile = () => {
label="Title of share" label="Title of share"
variant="filled" variant="filled"
value={title} value={title}
onChange={(e) => { onChange={e => {
const value = e.target.value; const value = e.target.value;
const formattedValue = value.replace(titleFormatter, ""); const formattedValue = value.replace(titleFormatter, "");
setTitle(formattedValue); setTitle(formattedValue);
@ -690,7 +434,7 @@ export const EditFile = () => {
</Typography> </Typography>
<TextEditor <TextEditor
inlineContent={description} inlineContent={description}
setInlineContent={(value) => { setInlineContent={value => {
setDescription(value); setDescription(value);
}} }}
/> />
@ -730,22 +474,22 @@ export const EditFile = () => {
{isOpenMultiplePublish && ( {isOpenMultiplePublish && (
<MultiplePublish <MultiplePublish
isOpen={isOpenMultiplePublish} isOpen={isOpenMultiplePublish}
onError={(messageNotification)=> { onError={messageNotification => {
setIsOpenMultiplePublish(false); setIsOpenMultiplePublish(false);
setPublishes(null) setPublishes(null);
if (messageNotification) { if (messageNotification) {
dispatch( dispatch(
setNotification({ setNotification({
msg: messageNotification, msg: messageNotification,
alertType: 'error' alertType: "error",
}) })
) );
} }
}} }}
onSubmit={() => { onSubmit={() => {
setIsOpenMultiplePublish(false); setIsOpenMultiplePublish(false);
const clonedCopy = structuredClone(videoPropertiesToSetToRedux); const clonedCopy = structuredClone(videoPropertiesToSetToRedux);
dispatch(updateVideo(clonedCopy)); dispatch(updateFile(clonedCopy));
dispatch(updateInHashMap(clonedCopy)); dispatch(updateInHashMap(clonedCopy));
dispatch( dispatch(
setNotification({ setNotification({

View File

@ -34,21 +34,27 @@ import { setNotification } from "../../state/features/notificationsSlice";
import { objectToBase64, uint8ArrayToBase64 } from "../../utils/toBase64"; import { objectToBase64, uint8ArrayToBase64 } from "../../utils/toBase64";
import { RootState } from "../../state/store"; import { RootState } from "../../state/store";
import { import {
upsertVideosBeginning, upsertFilesBeginning,
addToHashMap, addToHashMap,
upsertVideos, upsertFiles,
setEditVideo, setEditFile,
updateVideo, updateFile,
updateInHashMap, updateInHashMap,
setEditPlaylist, setEditPlaylist,
} from "../../state/features/videoSlice"; } from "../../state/features/fileSlice.ts";
import ImageUploader from "../common/ImageUploader"; import ImageUploader from "../common/ImageUploader";
import { QSHARE_PLAYLIST_BASE, QSHARE_FILE_BASE } from "../../constants/Identifiers.ts"; import {
QSHARE_PLAYLIST_BASE,
QSHARE_FILE_BASE,
} from "../../constants/Identifiers.ts";
import { Playlists } from "../Playlists/Playlists"; import { Playlists } from "../Playlists/Playlists";
import { PlaylistListEdit } from "../PlaylistListEdit/PlaylistListEdit"; import { PlaylistListEdit } from "../PlaylistListEdit/PlaylistListEdit";
import { TextEditor } from "../common/TextEditor/TextEditor"; import { TextEditor } from "../common/TextEditor/TextEditor";
import { extractTextFromHTML } from "../common/TextEditor/utils"; import { extractTextFromHTML } from "../common/TextEditor/utils";
import {categories, subCategories} from "../../constants/Categories.ts"; import {
firstCategories,
secondCategories,
} from "../../constants/Categories/1stCategories.ts";
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 }); const shortuid = new ShortUniqueId({ length: 5 });
@ -76,7 +82,7 @@ export const EditPlaylist = () => {
(state: RootState) => state.auth?.user?.address (state: RootState) => state.auth?.user?.address
); );
const editVideoProperties = useSelector( const editVideoProperties = useSelector(
(state: RootState) => state.video.editPlaylistProperties (state: RootState) => state.file.editPlaylistProperties
); );
const [playlistData, setPlaylistData] = useState<any>(null); const [playlistData, setPlaylistData] = useState<any>(null);
const [title, setTitle] = useState<string>(""); const [title, setTitle] = useState<string>("");
@ -89,16 +95,16 @@ export const EditPlaylist = () => {
useState<any>(null); useState<any>(null);
const isNew = useMemo(() => { const isNew = useMemo(() => {
return editVideoProperties?.mode === 'new' return editVideoProperties?.mode === "new";
}, [editVideoProperties]) }, [editVideoProperties]);
useEffect(() => { useEffect(() => {
if (isNew) { if (isNew) {
setPlaylistData({ setPlaylistData({
videos: [] videos: [],
}) });
} }
}, [isNew]) }, [isNew]);
// useEffect(() => { // useEffect(() => {
// if (editVideoProperties) { // if (editVideoProperties) {
@ -146,7 +152,7 @@ export const EditPlaylist = () => {
// } // }
// }, [editVideoProperties]); // }, [editVideoProperties]);
const checkforPlaylist = React.useCallback(async (videoList) => { const checkforPlaylist = React.useCallback(async videoList => {
try { try {
const combinedData: any = {}; const combinedData: any = {};
const videos = []; const videos = [];
@ -178,18 +184,16 @@ export const EditPlaylist = () => {
if (editVideoProperties?.htmlDescription) { if (editVideoProperties?.htmlDescription) {
setDescription(editVideoProperties?.htmlDescription); setDescription(editVideoProperties?.htmlDescription);
} else if (editVideoProperties?.description) { } else if (editVideoProperties?.description) {
const paragraph = `<p>${editVideoProperties?.description}</p>` const paragraph = `<p>${editVideoProperties?.description}</p>`;
setDescription(paragraph); setDescription(paragraph);
} }
setCoverImage(editVideoProperties?.image || ""); setCoverImage(editVideoProperties?.image || "");
setVideos(editVideoProperties?.videos || []); setVideos(editVideoProperties?.videos || []);
if (editVideoProperties?.category) { if (editVideoProperties?.category) {
const selectedOption = categories.find( const selectedOption = firstCategories.find(
(option) => option.id === +editVideoProperties.category option => option.id === +editVideoProperties.category
); );
setSelectedCategoryVideos(selectedOption || null); setSelectedCategoryVideos(selectedOption || null);
} }
@ -197,11 +201,11 @@ export const EditPlaylist = () => {
if ( if (
editVideoProperties?.category && editVideoProperties?.category &&
editVideoProperties?.subcategory && editVideoProperties?.subcategory &&
subCategories[+editVideoProperties?.category] secondCategories[+editVideoProperties?.category]
) { ) {
const selectedOption = subCategories[ const selectedOption = secondCategories[
+editVideoProperties?.category +editVideoProperties?.category
]?.find((option) => option.id === +editVideoProperties.subcategory); ]?.find(option => option.id === +editVideoProperties.subcategory);
setSelectedSubCategoryVideos(selectedOption || null); setSelectedSubCategoryVideos(selectedOption || null);
} }
@ -212,24 +216,22 @@ export const EditPlaylist = () => {
}, [editVideoProperties]); }, [editVideoProperties]);
const onClose = () => { const onClose = () => {
setTitle("") setTitle("");
setDescription("") setDescription("");
setVideos([]) setVideos([]);
setPlaylistData(null) setPlaylistData(null);
setSelectedCategoryVideos(null) setSelectedCategoryVideos(null);
setSelectedSubCategoryVideos(null) setSelectedSubCategoryVideos(null);
setCoverImage("") setCoverImage("");
dispatch(setEditPlaylist(null)); dispatch(setEditPlaylist(null));
}; };
async function publishQDNResource() { async function publishQDNResource() {
try { try {
if (!title) throw new Error("Please enter a title");
if(!title) throw new Error('Please enter a title') if (!description) throw new Error("Please enter a description");
if(!description) throw new Error('Please enter a description') if (!coverImage) throw new Error("Please select cover image");
if(!coverImage) throw new Error('Please select cover image') if (!selectedCategoryVideos) throw new Error("Please select a category");
if(!selectedCategoryVideos) throw new Error('Please select a category')
if (!editVideoProperties) return; if (!editVideoProperties) return;
if (!userAddress) throw new Error("Unable to locate user address"); if (!userAddress) throw new Error("Unable to locate user address");
@ -259,7 +261,7 @@ export const EditPlaylist = () => {
const category = selectedCategoryVideos.id; const category = selectedCategoryVideos.id;
const subcategory = selectedSubCategoryVideos?.id || ""; const subcategory = selectedSubCategoryVideos?.id || "";
const videoStructured = playlistData.videos.map((item) => { const videoStructured = playlistData.videos.map(item => {
const descriptionVid = item?.metadata?.description; const descriptionVid = item?.metadata?.description;
if (!descriptionVid) throw new Error("cannot find video code"); if (!descriptionVid) throw new Error("cannot find video code");
@ -287,13 +289,12 @@ export const EditPlaylist = () => {
}); });
const id = uid(); const id = uid();
let commentsId = editVideoProperties?.id let commentsId = editVideoProperties?.id;
if (isNew) { if (isNew) {
commentsId = `${QSHARE_PLAYLIST_BASE}_cm_${id}` commentsId = `${QSHARE_PLAYLIST_BASE}_cm_${id}`;
} }
const stringDescription = extractTextFromHTML(description) const stringDescription = extractTextFromHTML(description);
const playlistObject: any = { const playlistObject: any = {
title, title,
@ -304,10 +305,10 @@ export const EditPlaylist = () => {
videos: videoStructured, videos: videoStructured,
commentsId: commentsId, commentsId: commentsId,
category, category,
subcategory subcategory,
}; };
const codes = videoStructured.map((item) => `c:${item.code};`).join(""); const codes = videoStructured.map(item => `c:${item.code};`).join("");
let metadescription = let metadescription =
`**category:${category};subcategory:${subcategory};${codes}**` + `**category:${category};subcategory:${subcategory};${codes}**` +
stringDescription.slice(0, 120); stringDescription.slice(0, 120);
@ -315,7 +316,7 @@ export const EditPlaylist = () => {
const crowdfundObjectToBase64 = await objectToBase64(playlistObject); const crowdfundObjectToBase64 = await objectToBase64(playlistObject);
// Description is obtained from raw data // Description is obtained from raw data
let identifier = editVideoProperties?.id let identifier = editVideoProperties?.id;
const sanitizeTitle = title const sanitizeTitle = title
.replace(/[^a-zA-Z0-9\s-]/g, "") .replace(/[^a-zA-Z0-9\s-]/g, "")
.replace(/\s+/g, "-") .replace(/\s+/g, "-")
@ -344,17 +345,13 @@ export const EditPlaylist = () => {
id: identifier, id: identifier,
service: "PLAYLIST", service: "PLAYLIST",
name: username, name: username,
...playlistObject ...playlistObject,
} };
dispatch( dispatch(updateFile(objectToStore));
updateVideo(objectToStore) dispatch(updateInHashMap(objectToStore));
);
dispatch(
updateInHashMap(objectToStore)
);
} else { } else {
dispatch( dispatch(
updateVideo({ updateFile({
...editVideoProperties, ...editVideoProperties,
...playlistObject, ...playlistObject,
}) })
@ -367,8 +364,6 @@ export const EditPlaylist = () => {
); );
} }
onClose(); onClose();
} catch (error: any) { } catch (error: any) {
let notificationObj: any = null; let notificationObj: any = null;
@ -415,7 +410,9 @@ export const EditPlaylist = () => {
event: SelectChangeEvent<string> event: SelectChangeEvent<string>
) => { ) => {
const optionId = event.target.value; const optionId = event.target.value;
const selectedOption = categories.find((option) => option.id === +optionId); const selectedOption = firstCategories.find(
option => option.id === +optionId
);
setSelectedCategoryVideos(selectedOption || null); setSelectedCategoryVideos(selectedOption || null);
}; };
const handleOptionSubCategoryChangeVideos = ( const handleOptionSubCategoryChangeVideos = (
@ -424,25 +421,26 @@ export const EditPlaylist = () => {
) => { ) => {
const optionId = event.target.value; const optionId = event.target.value;
const selectedOption = subcategories.find( const selectedOption = subcategories.find(
(option) => option.id === +optionId option => option.id === +optionId
); );
setSelectedSubCategoryVideos(selectedOption || null); setSelectedSubCategoryVideos(selectedOption || null);
}; };
const removeVideo = index => {
const removeVideo = (index) => {
const copyData = structuredClone(playlistData); const copyData = structuredClone(playlistData);
copyData.videos.splice(index, 1); copyData.videos.splice(index, 1);
setPlaylistData(copyData); setPlaylistData(copyData);
}; };
const addVideo = (data) => { const addVideo = data => {
if (playlistData?.videos?.length > 9) { if (playlistData?.videos?.length > 9) {
dispatch(setNotification({ dispatch(
setNotification({
msg: "Max 10 videos per playlist", msg: "Max 10 videos per playlist",
alertType: "error", alertType: "error",
})); })
return );
return;
} }
const copyData = structuredClone(playlistData); const copyData = structuredClone(playlistData);
copyData.videos = [...copyData.videos, { ...data }]; copyData.videos = [...copyData.videos, { ...data }];
@ -466,10 +464,8 @@ export const EditPlaylist = () => {
> >
{isNew ? ( {isNew ? (
<NewCrowdfundTitle>Create new playlist</NewCrowdfundTitle> <NewCrowdfundTitle>Create new playlist</NewCrowdfundTitle>
) : ( ) : (
<NewCrowdfundTitle>Update Playlist properties</NewCrowdfundTitle> <NewCrowdfundTitle>Update Playlist properties</NewCrowdfundTitle>
)} )}
</Box> </Box>
<> <>
@ -488,7 +484,7 @@ export const EditPlaylist = () => {
value={selectedCategoryVideos?.id || ""} value={selectedCategoryVideos?.id || ""}
onChange={handleOptionCategoryChangeVideos} onChange={handleOptionCategoryChangeVideos}
> >
{categories.map((option) => ( {firstCategories.map(option => (
<MenuItem key={option.id} value={option.id}> <MenuItem key={option.id} value={option.id}>
{option.name} {option.name}
</MenuItem> </MenuItem>
@ -496,22 +492,22 @@ export const EditPlaylist = () => {
</Select> </Select>
</FormControl> </FormControl>
{selectedCategoryVideos && {selectedCategoryVideos &&
subCategories[selectedCategoryVideos?.id] && ( secondCategories[selectedCategoryVideos?.id] && (
<FormControl fullWidth sx={{ marginBottom: 2 }}> <FormControl fullWidth sx={{ marginBottom: 2 }}>
<InputLabel id="Category">Select a Sub-Category</InputLabel> <InputLabel id="Category">Select a Sub-Category</InputLabel>
<Select <Select
labelId="Sub-Category" labelId="Sub-Category"
input={<OutlinedInput label="Select a Sub-Category" />} input={<OutlinedInput label="Select a Sub-Category" />}
value={selectedSubCategoryVideos?.id || ""} value={selectedSubCategoryVideos?.id || ""}
onChange={(e) => onChange={e =>
handleOptionSubCategoryChangeVideos( handleOptionSubCategoryChangeVideos(
e, e,
subCategories[selectedCategoryVideos?.id] secondCategories[selectedCategoryVideos?.id]
) )
} }
> >
{subCategories[selectedCategoryVideos.id].map( {secondCategories[selectedCategoryVideos.id].map(
(option) => ( option => (
<MenuItem key={option.id} value={option.id}> <MenuItem key={option.id} value={option.id}>
{option.name} {option.name}
</MenuItem> </MenuItem>
@ -550,9 +546,12 @@ export const EditPlaylist = () => {
label="Title of playlist" label="Title of playlist"
variant="filled" variant="filled"
value={title} value={title}
onChange={(e) => { onChange={e => {
const value = e.target.value; const value = e.target.value;
const formattedValue = value.replace(/[^a-zA-Z0-9\s-_!?]/g, ""); const formattedValue = value.replace(
/[^a-zA-Z0-9\s-_!?]/g,
""
);
setTitle(formattedValue); setTitle(formattedValue);
}} }}
inputProps={{ maxLength: 180 }} inputProps={{ maxLength: 180 }}
@ -569,12 +568,19 @@ export const EditPlaylist = () => {
maxRows={3} maxRows={3}
required required
/> */} /> */}
<Typography sx={{ <Typography
fontSize: '18px' sx={{
}}>Description of playlist</Typography> fontSize: "18px",
<TextEditor inlineContent={description} setInlineContent={(value)=> { }}
setDescription(value) >
}} /> Description of playlist
</Typography>
<TextEditor
inlineContent={description}
setInlineContent={value => {
setDescription(value);
}}
/>
</React.Fragment> </React.Fragment>
<PlaylistListEdit <PlaylistListEdit

View File

@ -7,8 +7,8 @@ import {
import { Box, Button, Input, Typography, useTheme } from "@mui/material"; import { Box, Button, Input, Typography, useTheme } from "@mui/material";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import { removeVideo } from "../../state/features/videoSlice"; import { removeFile } from "../../state/features/fileSlice.ts";
import AddIcon from '@mui/icons-material/Add'; import AddIcon from "@mui/icons-material/Add";
import { QSHARE_FILE_BASE } from "../../constants/Identifiers.ts"; import { QSHARE_FILE_BASE } from "../../constants/Identifiers.ts";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "../../state/store"; import { RootState } from "../../state/store";
@ -17,28 +17,29 @@ export const PlaylistListEdit = ({ playlistData, removeVideo, addVideo }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const username = useSelector((state: RootState) => state.auth?.user?.name); const username = useSelector((state: RootState) => state.auth?.user?.name);
const [searchResults, setSearchResults] = useState([]) const [searchResults, setSearchResults] = useState([]);
const [filterSearch, setFilterSearch] = useState("") const [filterSearch, setFilterSearch] = useState("");
const search = async () => { const search = async () => {
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&mode=ALL&identifier=${QSHARE_FILE_BASE}&title=${filterSearch}&limit=20&includemetadata=true&reverse=true&name=${username}&exactmatchnames=true&offset=0`;
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&mode=ALL&identifier=${QSHARE_FILE_BASE}&title=${filterSearch}&limit=20&includemetadata=true&reverse=true&name=${username}&exactmatchnames=true&offset=0`
const response = await fetch(url, { const response = await fetch(url, {
method: 'GET', method: "GET",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}) });
const responseDataSearchVid = await response.json() const responseDataSearchVid = await response.json();
setSearchResults(responseDataSearchVid) setSearchResults(responseDataSearchVid);
} };
return ( return (
<Box sx={{ <Box
display: 'flex', sx={{
gap: '10px', display: "flex",
width: '100%', gap: "10px",
justifyContent: 'center' width: "100%",
}}> justifyContent: "center",
}}
>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@ -55,7 +56,7 @@ const [filterSearch, setFilterSearch] = useState("")
sx={{ sx={{
marginTop: "25px", marginTop: "25px",
height: "450px", height: "450px",
overflow: 'auto' overflow: "auto",
}} }}
> >
{playlistData?.videos?.map((vid, index) => { {playlistData?.videos?.map((vid, index) => {
@ -82,7 +83,7 @@ const [filterSearch, setFilterSearch] = useState("")
<Typography <Typography
sx={{ sx={{
fontSize: "18px", fontSize: "18px",
wordBreak: 'break-word' wordBreak: "break-word",
}} }}
> >
{vid?.metadata?.title} {vid?.metadata?.title}
@ -109,7 +110,6 @@ const [filterSearch, setFilterSearch] = useState("")
width: "100%", width: "100%",
}} }}
> >
<CrowdfundSubTitleRow> <CrowdfundSubTitleRow>
<CrowdfundSubTitle>Add videos to playlist</CrowdfundSubTitle> <CrowdfundSubTitle>Add videos to playlist</CrowdfundSubTitle>
</CrowdfundSubTitleRow> </CrowdfundSubTitleRow>
@ -117,16 +117,18 @@ const [filterSearch, setFilterSearch] = useState("")
sx={{ sx={{
marginTop: "25px", marginTop: "25px",
height: "450px", height: "450px",
overflow: 'auto' overflow: "auto",
}}
>
<Box
sx={{
display: "flex",
gap: "10px",
}} }}
> >
<Box sx={{
display: 'flex',
gap: '10px'
}}>
<Input <Input
id="standard-adornment-name" id="standard-adornment-name"
onChange={(e) => { onChange={e => {
setFilterSearch(e.target.value); setFilterSearch(e.target.value);
}} }}
value={filterSearch} value={filterSearch}
@ -155,7 +157,6 @@ const [filterSearch, setFilterSearch] = useState("")
onClick={() => { onClick={() => {
search(); search();
}} }}
variant="contained" variant="contained"
> >
Search Search
@ -186,7 +187,7 @@ const [filterSearch, setFilterSearch] = useState("")
<Typography <Typography
sx={{ sx={{
fontSize: "18px", fontSize: "18px",
wordBreak: 'break-word' wordBreak: "break-word",
}} }}
> >
{vid?.metadata?.title} {vid?.metadata?.title}
@ -205,6 +206,5 @@ const [filterSearch, setFilterSearch] = useState("")
</CardContentContainerComment> </CardContentContainerComment>
</Box> </Box>
</Box> </Box>
); );
}; };

View File

@ -1,23 +1,15 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { import {
AddCoverImageButton,
AddLogoIcon,
CoverImagePreview,
CrowdfundActionButton, CrowdfundActionButton,
CrowdfundActionButtonRow, CrowdfundActionButtonRow,
CustomInputField, CustomInputField,
CustomSelect,
LogoPreviewRow,
ModalBody, ModalBody,
NewCrowdfundTitle, NewCrowdfundTitle,
StyledButton, StyledButton,
TimesIcon,
} from "./Upload-styles"; } from "./Upload-styles";
import { import {
Box, Box,
Button,
FormControl, FormControl,
Input,
InputLabel, InputLabel,
MenuItem, MenuItem,
Modal, Modal,
@ -32,35 +24,26 @@ import ShortUniqueId from "short-unique-id";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import AddBoxIcon from "@mui/icons-material/AddBox"; import AddBoxIcon from "@mui/icons-material/AddBox";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import AddIcon from "@mui/icons-material/Add";
import { setNotification } from "../../state/features/notificationsSlice"; import { setNotification } from "../../state/features/notificationsSlice";
import { objectToBase64, uint8ArrayToBase64 } from "../../utils/toBase64"; import { objectToBase64 } from "../../utils/toBase64";
import { RootState } from "../../state/store"; import { RootState } from "../../state/store";
import { import { QSHARE_FILE_BASE } from "../../constants/Identifiers.ts";
upsertVideosBeginning,
addToHashMap,
upsertVideos,
} from "../../state/features/videoSlice";
import ImageUploader from "../common/ImageUploader";
import {
QSHARE_PLAYLIST_BASE,
QSHARE_FILE_BASE,
} from "../../constants/Identifiers.ts";
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll"; import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll";
import {
CrowdfundSubTitle,
CrowdfundSubTitleRow,
} from "../EditPlaylist/Upload-styles.tsx";
import { CardContentContainerComment } from "../common/Comments/Comments-styles";
import { TextEditor } from "../common/TextEditor/TextEditor"; import { TextEditor } from "../common/TextEditor/TextEditor";
import { extractTextFromHTML } from "../common/TextEditor/utils"; import { extractTextFromHTML } from "../common/TextEditor/utils";
import {categories, subCategories, subCategories2, subCategories3} from "../../constants/Categories.ts"; import {
firstCategories,
secondCategories,
thirdCategories,
fourthCategories,
allCategoryData,
} from "../../constants/Categories/1stCategories.ts";
import { titleFormatter } from "../../constants/Misc.ts"; import { titleFormatter } from "../../constants/Misc.ts";
import {
CategoryList,
CategoryListRef,
} from "../common/CategoryList/CategoryList.tsx";
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 }); const shortuid = new ShortUniqueId({ length: 5 });
@ -104,22 +87,15 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
const [selectedCategory, setSelectedCategory] = useState<any>(null); const [selectedCategory, setSelectedCategory] = useState<any>(null);
const [selectedSubCategory, setSelectedSubCategory] = useState<any>(null); const [selectedSubCategory, setSelectedSubCategory] = useState<any>(null);
const [selectedCategoryVideos, setSelectedCategoryVideos] =
useState<any>(null);
const [selectedSubCategoryVideos, setSelectedSubCategoryVideos] =
useState<any>(null);
const [selectedSubCategoryVideos2, setSelectedSubCategoryVideos2] =
useState<any>(null);
const [selectedSubCategoryVideos3, setSelectedSubCategoryVideos3] =
useState<any>(null);
const [playlistSetting, setPlaylistSetting] = useState<null | string>(null); const [playlistSetting, setPlaylistSetting] = useState<null | string>(null);
const [publishes, setPublishes] = useState<any>(null); const [publishes, setPublishes] = useState<any>(null);
const categoryListRef = useRef<CategoryListRef>(null);
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
maxFiles: 10, maxFiles: 10,
maxSize: 419430400, // 400 MB in bytes maxSize: 419430400, // 400 MB in bytes
onDrop: (acceptedFiles, rejectedFiles) => { onDrop: (acceptedFiles, rejectedFiles) => {
const formatArray = acceptedFiles.map((item) => { const formatArray = acceptedFiles.map(item => {
return { return {
file: item, file: item,
title: "", title: "",
@ -128,11 +104,11 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
}; };
}); });
setFiles((prev) => [...prev, ...formatArray]); setFiles(prev => [...prev, ...formatArray]);
let errorString = null; let errorString = null;
rejectedFiles.forEach(({ file, errors }) => { rejectedFiles.forEach(({ file, errors }) => {
errors.forEach((error) => { errors.forEach(error => {
if (error.code === "file-too-large") { if (error.code === "file-too-large") {
errorString = "File must be under 400mb"; errorString = "File must be under 400mb";
} }
@ -159,16 +135,15 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
setIsOpen(false); setIsOpen(false);
}; };
async function publishQDNResource() { async function publishQDNResource() {
try { try {
if (!categoryListRef.current) throw new Error("No CategoryListRef found");
if (!userAddress) throw new Error("Unable to locate user address"); if (!userAddress) throw new Error("Unable to locate user address");
if (!title) throw new Error("Please enter a title"); if (!title) throw new Error("Please enter a title");
if (!description) throw new Error("Please enter a description"); if (!description) throw new Error("Please enter a description");
if (!selectedCategoryVideos) throw new Error("Please select a category"); if (!categoryListRef.current?.getSelectedCategories()[0])
throw new Error("Please select a category");
if (files.length === 0) throw new Error("Add at least one file"); if (files.length === 0) throw new Error("Add at least one file");
let errorMsg = ""; let errorMsg = "";
let name = ""; let name = "";
@ -194,15 +169,11 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
return; return;
} }
let fileReferences = [] let fileReferences = [];
let listOfPublishes = []; let listOfPublishes = [];
const fullDescription = extractTextFromHTML(description); const fullDescription = extractTextFromHTML(description);
const category = selectedCategoryVideos.id;
const subcategory = selectedSubCategoryVideos?.id || "";
const subcategory2 = selectedSubCategoryVideos2?.id || "";
const subcategory3 = selectedSubCategoryVideos3?.id || "";
const sanitizeTitle = title const sanitizeTitle = title
.replace(/[^a-zA-Z0-9\s-]/g, "") .replace(/[^a-zA-Z0-9\s-]/g, "")
@ -212,20 +183,17 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
.toLowerCase(); .toLowerCase();
for (const publish of files) { for (const publish of files) {
const file = publish.file; const file = publish.file;
const id = uid(); const id = uid();
const identifier = `${QSHARE_FILE_BASE}${sanitizeTitle.slice(0, 30)}_${id}`; const identifier = `${QSHARE_FILE_BASE}${sanitizeTitle.slice(0, 30)}_${id}`;
let fileExtension = ""; let fileExtension = "";
const fileExtensionSplit = file?.name?.split("."); const fileExtensionSplit = file?.name?.split(".");
if (fileExtensionSplit?.length > 1) { if (fileExtensionSplit?.length > 1) {
fileExtension = fileExtensionSplit?.pop() || ""; fileExtension = fileExtensionSplit?.pop() || "";
} }
let firstPartName = fileExtensionSplit[0] let firstPartName = fileExtensionSplit[0];
let filename = firstPartName.slice(0, 15); let filename = firstPartName.slice(0, 15);
@ -241,18 +209,15 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
); );
if (fileExtension) { if (fileExtension) {
filename = `${alphanumericString.trim()}.${fileExtension}` filename = `${alphanumericString.trim()}.${fileExtension}`;
} else { } else {
filename = alphanumericString filename = alphanumericString;
} }
let metadescription = let metadescription =
`**cat:${category};sub:${subcategory};sub2:${subcategory2};sub3:${subcategory3}**` + `**${categoryListRef.current?.getCategoriesFetchString()}**` +
fullDescription.slice(0, 150); fullDescription.slice(0, 150);
const requestBodyVideo: any = { const requestBodyVideo: any = {
action: "PUBLISH_QDN_RESOURCE", action: "PUBLISH_QDN_RESOURCE",
name: name, name: name,
@ -269,10 +234,10 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
filename: file.name, filename: file.name,
identifier, identifier,
name, name,
service: 'FILE', service: "FILE",
mimetype: file.type, mimetype: file.type,
size: file.size size: file.size,
}) });
} }
const idMeta = uid(); const idMeta = uid();
@ -283,15 +248,12 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
fullDescription, fullDescription,
htmlDescription: description, htmlDescription: description,
commentsId: `${QSHARE_FILE_BASE}_cm_${idMeta}`, commentsId: `${QSHARE_FILE_BASE}_cm_${idMeta}`,
category, ...categoryListRef.current?.categoriesToObject(),
subcategory, files: fileReferences,
subcategory2,
subcategory3,
files: fileReferences
}; };
let metadescription = let metadescription =
`**cat:${category};sub:${subcategory};sub2:${subcategory2}**` + `**${categoryListRef.current?.getCategoriesFetchString()}**` +
fullDescription.slice(0, 150); fullDescription.slice(0, 150);
const crowdfundObjectToBase64 = await objectToBase64(fileObject); const crowdfundObjectToBase64 = await objectToBase64(fileObject);
@ -315,7 +277,6 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
}; };
setPublishes(multiplePublish); setPublishes(multiplePublish);
setIsOpenMultiplePublish(true); setIsOpenMultiplePublish(true);
} catch (error: any) { } catch (error: any) {
let notificationObj: any = null; let notificationObj: any = null;
if (typeof error === "string") { if (typeof error === "string") {
@ -339,50 +300,6 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
} }
} }
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 handleOptionSubCategoryChangeVideos2 = (
event: SelectChangeEvent<string>,
subcategories: any[]
) => {
const optionId = event.target.value;
const selectedOption = subcategories.find(
(option) => option.id === +optionId
);
setSelectedSubCategoryVideos2(selectedOption || null);
};
const handleOptionSubCategoryChangeVideos3 = (
event: SelectChangeEvent<string>,
subcategories: any[]
) => {
const optionId = event.target.value;
const selectedOption = subcategories.find(
(option) => option.id === +optionId
);
setSelectedSubCategoryVideos3(selectedOption || null);
};
return ( return (
<> <>
{username && ( {username && (
@ -414,9 +331,7 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
justifyContent: "space-between", justifyContent: "space-between",
}} }}
> >
<NewCrowdfundTitle>Share</NewCrowdfundTitle> <NewCrowdfundTitle>Share</NewCrowdfundTitle>
</Box> </Box>
{step === "videos" && ( {step === "videos" && (
@ -449,7 +364,7 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
<Typography>{file?.file?.name}</Typography> <Typography>{file?.file?.name}</Typography>
<RemoveIcon <RemoveIcon
onClick={() => { onClick={() => {
setFiles((prev) => { setFiles(prev => {
const copyPrev = [...prev]; const copyPrev = [...prev];
copyPrev.splice(index, 1); copyPrev.splice(index, 1);
return copyPrev; return copyPrev;
@ -463,6 +378,9 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
</React.Fragment> </React.Fragment>
); );
})} })}
{files?.length > 0 && (
<>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@ -470,142 +388,18 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
alignItems: "flex-start", alignItems: "flex-start",
}} }}
> >
{files?.length > 0 && ( <CategoryList
<> categoryData={allCategoryData}
<Box sx={{ ref={categoryListRef}
display: 'flex', columns={3}
flexDirection: 'column', />
gap: '20px',
width: '50%'
}}>
<FormControl fullWidth sx={{ marginBottom: 2 }}>
<InputLabel id="Category">Select a Category</InputLabel>
<Select
labelId="Category"
input={<OutlinedInput label="Select a Category" />}
value={selectedCategoryVideos?.id || ""}
onChange={handleOptionCategoryChangeVideos}
>
{categories.map((option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
))}
</Select>
</FormControl>
</Box> </Box>
{selectedCategoryVideos && (
<>
<Box sx={{
display: 'flex',
flexDirection: 'column',
gap: '20px',
width: '50%'
}}>
{selectedCategoryVideos &&
subCategories[selectedCategoryVideos?.id] && (
<FormControl fullWidth sx={{ marginBottom: 2 }}>
<InputLabel id="Category">
Select a Sub-Category
</InputLabel>
<Select
labelId="Sub-Category"
input={
<OutlinedInput label="Select a Sub-Category" />
}
value={selectedSubCategoryVideos?.id || ""}
onChange={(e) =>
handleOptionSubCategoryChangeVideos(
e,
subCategories[selectedCategoryVideos?.id]
)
}
>
{subCategories[selectedCategoryVideos.id].map(
(option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
)
)}
</Select>
</FormControl>
)}
{selectedSubCategoryVideos &&
subCategories2[selectedSubCategoryVideos?.id] && (
<FormControl fullWidth sx={{ marginBottom: 2 }}>
<InputLabel id="Category">
Select a Sub-sub-Category
</InputLabel>
<Select
labelId="Sub-Category"
input={
<OutlinedInput label="Select a Sub-sub-Category" />
}
value={selectedSubCategoryVideos2?.id || ""}
onChange={(e) =>
handleOptionSubCategoryChangeVideos2(
e,
subCategories2[selectedSubCategoryVideos?.id]
)
}
>
{subCategories2[selectedSubCategoryVideos.id].map(
(option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
)
)}
</Select>
</FormControl>
)}
{selectedSubCategoryVideos2 &&
subCategories3[selectedSubCategoryVideos2?.id] && (
<FormControl fullWidth sx={{ marginBottom: 2 }}>
<InputLabel id="Category">
Select a Sub-3x-subCategory
</InputLabel>
<Select
labelId="Sub-Category"
input={
<OutlinedInput label="Select a Sub-3x-Category" />
}
value={selectedSubCategoryVideos3?.id || ""}
onChange={(e) =>
handleOptionSubCategoryChangeVideos3(
e,
subCategories3[selectedSubCategoryVideos2?.id]
)
}
>
{subCategories3[selectedSubCategoryVideos2.id].map(
(option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
)
)}
</Select>
</FormControl>
)}
</Box>
</>
)}
</>
)}
</Box>
{files?.length > 0 && (
<>
<CustomInputField <CustomInputField
name="title" name="title"
label="Title of share" label="Title of share"
variant="filled" variant="filled"
value={title} value={title}
onChange={(e) => { onChange={e => {
const value = e.target.value; const value = e.target.value;
const formattedValue = value.replace(titleFormatter, ""); const formattedValue = value.replace(titleFormatter, "");
setTitle(formattedValue); setTitle(formattedValue);
@ -622,13 +416,12 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
</Typography> </Typography>
<TextEditor <TextEditor
inlineContent={description} inlineContent={description}
setInlineContent={(value) => { setInlineContent={value => {
setDescription(value); setDescription(value);
}} }}
/> />
</> </>
)} )}
</> </>
)} )}
<CrowdfundActionButtonRow> <CrowdfundActionButtonRow>
@ -664,16 +457,16 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
{isOpenMultiplePublish && ( {isOpenMultiplePublish && (
<MultiplePublish <MultiplePublish
isOpen={isOpenMultiplePublish} isOpen={isOpenMultiplePublish}
onError={(messageNotification)=> { onError={messageNotification => {
setIsOpenMultiplePublish(false); setIsOpenMultiplePublish(false);
setPublishes(null) setPublishes(null);
if (messageNotification) { if (messageNotification) {
dispatch( dispatch(
setNotification({ setNotification({
msg: messageNotification, msg: messageNotification,
alertType: 'error' alertType: "error",
}) })
) );
} }
}} }}
onSubmit={() => { onSubmit={() => {
@ -686,9 +479,8 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
setPlaylistDescription(""); setPlaylistDescription("");
setSelectedCategory(null); setSelectedCategory(null);
setSelectedSubCategory(null); setSelectedSubCategory(null);
setSelectedCategoryVideos(null);
setSelectedSubCategoryVideos(null);
setPlaylistSetting(null); setPlaylistSetting(null);
categoryListRef.current?.clearCategories();
dispatch( dispatch(
setNotification({ setNotification({
msg: "Files published", msg: "Files published",

View File

@ -0,0 +1,61 @@
import React, { useEffect } from "react";
import { styled } from "@mui/system";
import { Grid } from "@mui/material";
import { useSelector } from "react-redux";
import { RootState } from "../state/store.ts";
import { useFetchFiles } from "../hooks/useFetchFiles.tsx";
export const StatsData = () => {
const StatsCol = styled(Grid)(({ theme }) => ({
display: "flex",
flexDirection: "column",
width: "100%",
padding: "20px 0px",
backgroundColor: theme.palette.background.default,
}));
const {
getFiles,
checkAndUpdateFile,
getFile,
hashMapFiles,
getNewFiles,
checkNewFiles,
getFilesFiltered,
getFilesCount,
} = useFetchFiles();
const totalVideosPublished = useSelector(
(state: RootState) => state.global.totalFilesPublished
);
const totalNamesPublished = useSelector(
(state: RootState) => state.global.totalNamesPublished
);
const videosPerNamePublished = useSelector(
(state: RootState) => state.global.filesPerNamePublished
);
useEffect(() => {
getFilesCount();
}, [getFilesCount]);
return (
<StatsCol>
<div>
Shares:{" "}
<span style={{ fontWeight: "bold" }}>{totalVideosPublished}</span>
</div>
<div>
Publishers:{" "}
<span style={{ fontWeight: "bold" }}>{totalNamesPublished}</span>
</div>
<div>
Average:{" "}
<span style={{ fontWeight: "bold" }}>
{videosPerNamePublished > 0 &&
Number(videosPerNamePublished).toFixed(0)}
</span>
</div>
</StatsCol>
);
};

View File

@ -0,0 +1,9 @@
import { styled } from "@mui/system";
import { Box } from "@mui/material";
export const CategoryContainer = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
flexDirection: "row",
gap: "5px",
}));

View File

@ -0,0 +1,284 @@
import {
Box,
FormControl,
InputLabel,
MenuItem,
OutlinedInput,
Select,
SelectChangeEvent,
SxProps,
Theme,
} from "@mui/material";
import React, { forwardRef, useImperativeHandle, useState } from "react";
import { CategoryContainer } from "./CategoryList-styles.tsx";
import { allCategoryData } from "../../../constants/Categories/1stCategories.ts";
export interface Category {
id: number;
name: string;
icon?: string;
}
export interface Categories {
[key: number]: Category[];
}
export interface CategoryData {
category: Category[];
subCategories: Categories[];
}
type ListDirection = "column" | "row";
interface CategoryListProps {
sx?: SxProps<Theme>;
categoryData: CategoryData;
initialCategories?: string[];
columns?: number;
}
export type CategoryListRef = {
getSelectedCategories: () => string[];
setSelectedCategories: (arr: string[]) => void;
clearCategories: () => void;
getCategoriesFetchString: () => string;
categoriesToObject: () => object;
};
export const CategoryList = React.forwardRef<
CategoryListRef,
CategoryListProps
>(
(
{ sx, categoryData, initialCategories, columns = 1 }: CategoryListProps,
ref
) => {
const categoriesLength = categoryData.subCategories.length + 1;
let emptyCategories: string[] = [];
for (let i = 0; i < categoriesLength; i++) emptyCategories.push("");
const [selectedCategories, setSelectedCategories] = useState<string[]>(
initialCategories || emptyCategories
);
const categoriesToObject = () => {
let categoriesObject = {};
selectedCategories.map((category, index) => {
if (index === 0) categoriesObject["category"] = category;
else if (index === 1) categoriesObject["subcategory"] = category;
else categoriesObject[`subcategory${index}`] = category;
});
console.log("categoriesObject is: ", categoriesObject);
return categoriesObject;
};
const clearCategories = () => {
setSelectedCategories(emptyCategories);
};
useImperativeHandle(ref, () => ({
getSelectedCategories: () => {
return selectedCategories;
},
setSelectedCategories: categories => {
console.log("setSelectedCategories: ", categories);
//categories.map((category, index) => selectCategory(category, index));
setSelectedCategories(categories);
},
clearCategories,
getCategoriesFetchString: () =>
getCategoriesFetchString(selectedCategories),
categoriesToObject,
}));
const selectCategory = (optionId: string, index: number) => {
const isMainCategory = index === 0;
const subCategoryIndex = index - 1;
const selectedOption = isMainCategory
? categoryData.category.find(option => option.id === +optionId)
: categoryData.subCategories[subCategoryIndex][
selectedCategories[subCategoryIndex]
].find(option => option.id === +optionId);
const newSelectedCategories: string[] = selectedCategories.map(
(category, categoryIndex) => {
if (index > categoryIndex) return category;
else if (index === categoryIndex) return selectedOption.id.toString();
else return "";
}
);
setSelectedCategories(newSelectedCategories);
};
const selectCategoryEvent = (event: SelectChangeEvent, index: number) => {
const optionId = event.target.value;
selectCategory(optionId, index);
};
const categorySelectSX = {
// 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
},
},
};
const fillMenu = (category: Categories, index: number) => {
const subCategoryIndex = selectedCategories[index];
console.log("selected categories: ", selectedCategories);
console.log("index is: ", index);
console.log("subCategoryIndex is: ", subCategoryIndex);
console.log("category is: ", category);
console.log(
"subCategoryIndex within category: ",
selectedCategories[subCategoryIndex]
);
console.log("categoryData: ", categoryData);
const menuToFill = category[subCategoryIndex];
if (menuToFill)
return menuToFill.map(option => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
));
};
const hasSubCategory = (category: Categories, index: number) => {
const subCategoryIndex = selectedCategories[index];
const subCategory = category[subCategoryIndex];
return subCategory && subCategoryIndex;
};
return (
<CategoryContainer sx={{ width: "100%", ...sx }}>
<FormControl sx={{ width: "100%" }}>
<Box
sx={{
display: "grid",
gridTemplateColumns: "repeat(" + columns + ", 1fr)",
width: "100%",
gap: "20px",
alignItems: "center",
marginTop: "30px",
}}
>
<FormControl fullWidth sx={{ marginBottom: 1 }}>
<InputLabel
sx={{
fontSize: "16px",
}}
id="Category-1"
>
Category
</InputLabel>
<Select
labelId="Category 1"
input={<OutlinedInput label="Category 1" />}
value={selectedCategories[0] || ""}
onChange={e => {
selectCategoryEvent(e, 0);
}}
sx={categorySelectSX}
>
{categoryData.category.map(option => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
))}
</Select>
</FormControl>
{categoryData.subCategories.map(
(category, index) =>
hasSubCategory(category, index) && (
<FormControl
fullWidth
sx={{
marginBottom: 1,
}}
key={selectedCategories[index] + index}
>
<InputLabel
sx={{
fontSize: "16px",
}}
id={`Category-${index + 2}`}
>
{`Category-${index + 2}`}
</InputLabel>
<Select
labelId={`Category ${index + 2}`}
input={<OutlinedInput label={`Category ${index + 2}`} />}
value={selectedCategories[index + 1] || ""}
onChange={e => {
selectCategoryEvent(e, index + 1);
}}
sx={{
width: "100%",
// 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
},
},
}}
>
{fillMenu(category, index)}
</Select>
</FormControl>
)
)}
</Box>
</FormControl>
</CategoryContainer>
);
}
);
export const getCategoriesFetchString = (categories: string[]) => {
let fetchString = "";
categories.map((category, index) => {
if (category) {
if (index === 0) fetchString += `cat:${category}`;
else if (index === 1) fetchString += `;sub:${category}`;
else fetchString += `;sub${index}:${category}`;
}
});
console.log("categoriesAsDescription: ", fetchString);
return fetchString;
};
export const getCategoriesFromObject = (editFileProperties: any) => {
const categoryList: string[] = [];
const categoryCount = allCategoryData.subCategories.length + 1;
for (let i = 0; i < categoryCount; i++) {
if (i === 0 && editFileProperties.category)
categoryList.push(editFileProperties.category);
else if (i === 1 && editFileProperties.subcategory)
categoryList.push(editFileProperties.subcategory);
else categoryList.push(editFileProperties[`subcategory${i}`] || "");
}
return categoryList;
};

View File

@ -20,58 +20,58 @@ interface Publish {
interface MultiplePublishProps { interface MultiplePublishProps {
publishes: Publish; publishes: Publish;
isOpen: boolean; isOpen: boolean;
onSubmit: ()=> void onSubmit: () => void;
onError: (message?: string)=> void onError: (message?: string) => void;
} }
export const MultiplePublish = ({ publishes, isOpen, onSubmit, onError}: MultiplePublishProps) => { export const MultiplePublish = ({
publishes,
isOpen,
onSubmit,
onError,
}: MultiplePublishProps) => {
const theme = useTheme(); const theme = useTheme();
const listOfSuccessfulPublishesRef = useRef([]) const listOfSuccessfulPublishesRef = useRef([]);
const [listOfSuccessfulPublishes, setListOfSuccessfulPublishes] = useState< const [listOfSuccessfulPublishes, setListOfSuccessfulPublishes] = useState<
any[] any[]
>([]); >([]);
const [listOfUnsuccessfulPublishes, setListOfUnSuccessfulPublishes] = useState< const [listOfUnsuccessfulPublishes, setListOfUnSuccessfulPublishes] =
any[] useState<any[]>([]);
>([]);
const [currentlyInPublish, setCurrentlyInPublish] = useState(null); const [currentlyInPublish, setCurrentlyInPublish] = useState(null);
const hasStarted = useRef(false); const hasStarted = useRef(false);
const publish = useCallback(async (pub: any) => { const publish = useCallback(async (pub: any) => {
const lengthOfResources = pub?.resources?.length const lengthOfResources = pub?.resources?.length;
const lengthOfTimeout = lengthOfResources * 30000 const lengthOfTimeout = lengthOfResources * 30000;
return await qortalRequestWithTimeout(pub, lengthOfTimeout); return await qortalRequestWithTimeout(pub, lengthOfTimeout);
}, []); }, []);
const [isPublishing, setIsPublishing] = useState(true) const [isPublishing, setIsPublishing] = useState(true);
const handlePublish = useCallback( const handlePublish = useCallback(
async (pub: any) => { async (pub: any) => {
try { try {
setCurrentlyInPublish(pub?.identifier); setCurrentlyInPublish(pub?.identifier);
setIsPublishing(true) setIsPublishing(true);
const res = await publish(pub); const res = await publish(pub);
onSubmit() onSubmit();
setListOfUnSuccessfulPublishes([]) setListOfUnSuccessfulPublishes([]);
} catch (error: any) { } catch (error: any) {
const unsuccessfulPublishes = error?.error?.unsuccessfulPublishes || [] const unsuccessfulPublishes = error?.error?.unsuccessfulPublishes || [];
if(error?.error === 'User declined request'){ if (error?.error === "User declined request") {
onError() onError();
return return;
} }
if(error?.error === 'The request timed out'){ if (error?.error === "The request timed out") {
onError("The request timed out") onError("The request timed out");
return return;
} }
if (unsuccessfulPublishes?.length > 0) { if (unsuccessfulPublishes?.length > 0) {
setListOfUnSuccessfulPublishes(unsuccessfulPublishes) setListOfUnSuccessfulPublishes(unsuccessfulPublishes);
} }
} finally { } finally {
setIsPublishing(false);
setIsPublishing(false)
} }
}, },
[publish] [publish]
@ -79,18 +79,20 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit, onError}: Multip
const retry = () => { const retry = () => {
let newlistOfMultiplePublishes: any[] = []; let newlistOfMultiplePublishes: any[] = [];
listOfUnsuccessfulPublishes?.forEach((item)=> { listOfUnsuccessfulPublishes?.forEach(item => {
const findPub = publishes?.resources.find((res: any)=> res?.identifier === item.identifier) const findPub = publishes?.resources.find(
(res: any) => res?.identifier === item.identifier
);
if (findPub) { if (findPub) {
newlistOfMultiplePublishes.push(findPub) newlistOfMultiplePublishes.push(findPub);
} }
}) });
const multiplePublish = { const multiplePublish = {
...publishes, ...publishes,
resources: newlistOfMultiplePublishes resources: newlistOfMultiplePublishes,
};
handlePublish(multiplePublish);
}; };
handlePublish(multiplePublish)
}
const startPublish = useCallback( const startPublish = useCallback(
async (pubs: any) => { async (pubs: any) => {
@ -106,7 +108,6 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit, onError}: Multip
} }
}, [startPublish, publishes, listOfSuccessfulPublishes]); }, [startPublish, publishes, listOfSuccessfulPublishes]);
return ( return (
<Modal <Modal
open={isOpen} open={isOpen}
@ -119,7 +120,9 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit, onError}: Multip
}} }}
> >
{publishes?.resources?.map((publish: any) => { {publishes?.resources?.map((publish: any) => {
const unpublished = listOfUnsuccessfulPublishes.map(item => item?.identifier) const unpublished = listOfUnsuccessfulPublishes.map(
item => item?.identifier
);
return ( return (
<Box <Box
sx={{ sx={{
@ -128,6 +131,7 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit, onError}: Multip
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
}} }}
key={publish?.identifier}
> >
<Typography>{publish?.identifier}</Typography> <Typography>{publish?.identifier}</Typography>
{!isPublishing && hasStarted.current ? ( {!isPublishing && hasStarted.current ? (
@ -146,29 +150,39 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit, onError}: Multip
/> />
)} )}
</> </>
): <CircularProgress size={16} color="secondary"/>} ) : (
<CircularProgress size={16} color="secondary" />
)}
</Box> </Box>
); );
})} })}
{!isPublishing && listOfUnsuccessfulPublishes.length > 0 && ( {!isPublishing && listOfUnsuccessfulPublishes.length > 0 && (
<> <>
<Typography sx={{ <Typography
marginTop: '20px', sx={{
fontSize: '16px' marginTop: "20px",
}}>Some files were not published. Please try again. It's important that all the files get published. Maybe wait a couple minutes if the error keeps occurring</Typography> fontSize: "16px",
<Button variant="contained" onClick={()=> { }}
retry() >
}}>Try again</Button> Some files were not published. Please try again. It's important
that all the files get published. Maybe wait a couple minutes if
the error keeps occurring
</Typography>
<Button
variant="contained"
onClick={() => {
retry();
}}
>
Try again
</Button>
</> </>
)} )}
</ModalBody> </ModalBody>
</Modal> </Modal>
); );
}; };
export const ModalBody = styled(Box)(({ theme }) => ({ export const ModalBody = styled(Box)(({ theme }) => ({
position: "absolute", position: "absolute",
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,

View File

@ -1,5 +1,12 @@
import React, { useState, useRef } from "react"; import React, { useState, useRef } from "react";
import { Box, Button, Input, Popover, Typography, useTheme } from "@mui/material"; import {
Box,
Button,
Input,
Popover,
Typography,
useTheme,
} from "@mui/material";
import ExitToAppIcon from "@mui/icons-material/ExitToApp"; import ExitToAppIcon from "@mui/icons-material/ExitToApp";
import { BlockedNamesModal } from "../../common/BlockedNamesModal/BlockedNamesModal"; import { BlockedNamesModal } from "../../common/BlockedNamesModal/BlockedNamesModal";
import AddBoxIcon from "@mui/icons-material/AddBox"; import AddBoxIcon from "@mui/icons-material/AddBox";
@ -28,11 +35,11 @@ import { DownloadTaskManager } from "../../common/DownloadTaskManager";
import QShareLogo from "../../../assets/img/q-share-icon.webp"; import QShareLogo from "../../../assets/img/q-share-icon.webp";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { import {
addFilteredVideos, addFilteredFiles,
setEditPlaylist, setEditPlaylist,
setFilterValue, setFilterValue,
setIsFiltering, setIsFiltering,
} from "../../../state/features/videoSlice"; } from "../../../state/features/fileSlice.ts";
import { RootState } from "../../../state/store"; import { RootState } from "../../../state/store";
import { useWindowSize } from "../../../hooks/useWindowSize"; import { useWindowSize } from "../../../hooks/useWindowSize";
import { PublishFile } from "../../PublishFile/PublishFile.tsx"; import { PublishFile } from "../../PublishFile/PublishFile.tsx";
@ -67,9 +74,7 @@ const NavBar: React.FC<Props> = ({
const [anchorElNotification, setAnchorElNotification] = const [anchorElNotification, setAnchorElNotification] =
React.useState<HTMLButtonElement | null>(null); React.useState<HTMLButtonElement | null>(null);
const filterValue = useSelector( const filterValue = useSelector((state: RootState) => state.file.filterValue);
(state: RootState) => state.video.filterValue
);
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => { const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
const target = event.currentTarget as unknown as HTMLButtonElement | null; const target = event.currentTarget as unknown as HTMLButtonElement | null;
@ -100,18 +105,20 @@ const NavBar: React.FC<Props> = ({
return ( return (
<CustomAppBar position="sticky" elevation={2}> <CustomAppBar position="sticky" elevation={2}>
<ThemeSelectRow> <ThemeSelectRow>
<Box sx={{ <Box
display: 'flex', sx={{
height: '100%', display: "flex",
alignItems: 'center', height: "100%",
gap: '20px' alignItems: "center",
}}> gap: "20px",
}}
>
<LogoContainer <LogoContainer
onClick={() => { onClick={() => {
navigate("/"); navigate("/");
dispatch(setIsFiltering(false)); dispatch(setIsFiltering(false));
dispatch(setFilterValue("")); dispatch(setFilterValue(""));
dispatch(addFilteredVideos([])); dispatch(addFilteredFiles([]));
searchValRef.current = ""; searchValRef.current = "";
if (!inputRef.current) return; if (!inputRef.current) return;
inputRef.current.value = ""; inputRef.current.value = "";
@ -126,10 +133,14 @@ const NavBar: React.FC<Props> = ({
}} }}
/> />
</LogoContainer> </LogoContainer>
<Typography sx={{ <Typography
fontSize: '16px', sx={{
whiteSpace: 'nowrap' fontSize: "16px",
}}>Sharing is caring</Typography> whiteSpace: "nowrap",
}}
>
Sharing is caring
</Typography>
</Box> </Box>
</ThemeSelectRow> </ThemeSelectRow>
<Box <Box
@ -289,15 +300,15 @@ const NavBar: React.FC<Props> = ({
<Input <Input
id="standard-adornment-name" id="standard-adornment-name"
inputRef={inputRef} inputRef={inputRef}
onChange={(e) => { onChange={e => {
searchValRef.current = e.target.value; searchValRef.current = e.target.value;
}} }}
onKeyDown={(event) => { onKeyDown={event => {
if (event.key === "Enter" || event.keyCode === 13) { if (event.key === "Enter" || event.keyCode === 13) {
if (!searchValRef.current) { if (!searchValRef.current) {
dispatch(setIsFiltering(false)); dispatch(setIsFiltering(false));
dispatch(setFilterValue("")); dispatch(setFilterValue(""));
dispatch(addFilteredVideos([])); dispatch(addFilteredFiles([]));
searchValRef.current = ""; searchValRef.current = "";
if (!inputRef.current) return; if (!inputRef.current) return;
inputRef.current.value = ""; inputRef.current.value = "";
@ -305,7 +316,7 @@ const NavBar: React.FC<Props> = ({
} }
navigate("/"); navigate("/");
dispatch(setIsFiltering(true)); dispatch(setIsFiltering(true));
dispatch(addFilteredVideos([])); dispatch(addFilteredFiles([]));
dispatch(setFilterValue(searchValRef.current)); dispatch(setFilterValue(searchValRef.current));
} }
}} }}
@ -338,7 +349,7 @@ const NavBar: React.FC<Props> = ({
if (!searchValRef.current) { if (!searchValRef.current) {
dispatch(setIsFiltering(false)); dispatch(setIsFiltering(false));
dispatch(setFilterValue("")); dispatch(setFilterValue(""));
dispatch(addFilteredVideos([])); dispatch(addFilteredFiles([]));
searchValRef.current = ""; searchValRef.current = "";
if (!inputRef.current) return; if (!inputRef.current) return;
inputRef.current.value = ""; inputRef.current.value = "";
@ -346,7 +357,7 @@ const NavBar: React.FC<Props> = ({
} }
navigate("/"); navigate("/");
dispatch(setIsFiltering(true)); dispatch(setIsFiltering(true));
dispatch(addFilteredVideos([])); dispatch(addFilteredFiles([]));
dispatch(setFilterValue(searchValRef.current)); dispatch(setFilterValue(searchValRef.current));
}} }}
/> />
@ -357,7 +368,7 @@ const NavBar: React.FC<Props> = ({
onClick={() => { onClick={() => {
dispatch(setIsFiltering(false)); dispatch(setIsFiltering(false));
dispatch(setFilterValue("")); dispatch(setFilterValue(""));
dispatch(addFilteredVideos([])); dispatch(addFilteredFiles([]));
searchValRef.current = ""; searchValRef.current = "";
if (!inputRef.current) return; if (!inputRef.current) return;
inputRef.current.value = ""; inputRef.current.value = "";
@ -403,8 +414,6 @@ const NavBar: React.FC<Props> = ({
<PublishFile /> <PublishFile />
</> </>
)} )}
</AvatarContainer> </AvatarContainer>
<Popover <Popover

View File

@ -1,221 +0,0 @@
import softwareIcon from "../assets/icons/software.webp";
import gamingIcon from "../assets/icons/gaming.webp";
import mediaIcon from "../assets/icons/media.webp";
import videoIcon from "../assets/icons/video.webp";
import audioIcon from "../assets/icons/audio.webp";
import documentIcon from "../assets/icons/document.webp";
interface SubCategory {
id: number;
name: string;
}
interface Categories {
[key: number]: SubCategory[];
}
const sortCategory = (a: SubCategory, b: SubCategory) => {
if (a.name === "Other") return 1;
else if (b.name === "Other") return -1;
else return a.name.localeCompare(b.name);
};
export const categories = [
{"id": 1, "name": "Software"},
{"id": 2, "name": "Gaming"},
{"id": 3, "name": "Media"},
{"id": 4, "name": "Other"}
].sort(sortCategory);
export const subCategories: Categories = {
1: [
{"id": 101, "name": "OS"},
{"id": 102, "name": "Application"},
{"id": 103, "name": "Source Code"},
{"id": 104, "name": "Other"}
].sort(sortCategory),
2: [
{"id": 201, "name": "NES"},
{"id": 202, "name": "SNES"},
{"id": 203, "name": "PC"},
{"id": 204, "name": "Other"}
].sort(sortCategory),
3: [
{"id": 301, "name": "Audio"},
{"id": 302, "name": "Video"},
{"id": 303, "name": "Image"},
{"id": 304, "name": "Document"},
{"id": 305, "name": "Other"}
].sort(sortCategory)
};
const gamingSystems = [
{"id": 20101, "name": "ROM"},
{"id": 20102, "name": "Romhack"},
{"id": 20103, "name": "Emulator"},
{"id": 20104, "name": "Guide"},
{"id": 20105, "name": "Other"},
].sort(sortCategory)
export const subCategories2: Categories = {
201: gamingSystems, // NES
202: gamingSystems, // SNES
301: [ // Audio
{"id": 30101, "name": "Music"},
{"id": 30102, "name": "Podcasts"},
{"id": 30103, "name": "Audiobooks"},
{"id": 30104, "name": "Sound Effects"},
{"id": 30105, "name": "Lectures & Speeches"},
{"id": 30106, "name": "Radio Shows"},
{"id": 30107, "name": "Ambient Sounds"},
{"id": 30108, "name": "Language Learning Material"},
{"id": 30109, "name": "Comedy & Satire"},
{"id": 30110, "name": "Documentaries"},
{"id": 30111, "name": "Guided Meditations & Yoga"},
{"id": 30112, "name": "Live Performances"},
{"id": 30113, "name": "Nature Sounds"},
{"id": 30114, "name": "Soundtracks"},
{"id": 30115, "name": "Interviews"}
].sort(sortCategory),
302: [ // Under Video
{"id": 30201, "name": "Movies"},
{"id": 30202, "name": "Series"},
{"id": 30203, "name": "Music"},
{"id": 30204, "name": "Education"},
{"id": 30205, "name": "Lifestyle"},
{"id": 30206, "name": "Gaming"},
{"id": 30207, "name": "Technology"},
{"id": 30208, "name": "Sports"},
{"id": 30209, "name": "News & Politics"},
{"id": 30210, "name": "Cooking & Food"},
{"id": 30211, "name": "Animation"},
{"id": 30212, "name": "Science"},
{"id": 30213, "name": "Health & Wellness"},
{"id": 30214, "name": "DIY & Crafts"},
{"id": 30215, "name": "Kids & Family"},
{"id": 30216, "name": "Comedy"},
{"id": 30217, "name": "Travel & Adventure"},
{"id": 30218, "name": "Art & Design"},
{"id": 30219, "name": "Nature & Environment"},
{"id": 30220, "name": "Business & Finance"},
{"id": 30221, "name": "Personal Development"},
{"id": 30222, "name": "Other"},
{"id": 30223, "name": "History"}
].sort(sortCategory),
303: [ // Image
{"id": 30301, "name": "Nature"},
{"id": 30302, "name": "Urban & Cityscapes"},
{"id": 30303, "name": "People & Portraits"},
{"id": 30304, "name": "Art & Abstract"},
{"id": 30305, "name": "Travel & Adventure"},
{"id": 30306, "name": "Animals & Wildlife"},
{"id": 30307, "name": "Sports & Action"},
{"id": 30308, "name": "Food & Cuisine"},
{"id": 30309, "name": "Fashion & Beauty"},
{"id": 30310, "name": "Technology & Science"},
{"id": 30311, "name": "Historical & Cultural"},
{"id": 30312, "name": "Aerial & Drone"},
{"id": 30313, "name": "Black & White"},
{"id": 30314, "name": "Events & Celebrations"},
{"id": 30315, "name": "Business & Corporate"},
{"id": 30316, "name": "Health & Wellness"},
{"id": 30317, "name": "Transportation & Vehicles"},
{"id": 30318, "name": "Still Life & Objects"},
{"id": 30319, "name": "Architecture & Buildings"},
{"id": 30320, "name": "Landscapes & Seascapes"}
].sort(sortCategory),
304: [ // Document
{"id": 30401, "name": "PDF"},
{"id": 30402, "name": "Word Document"},
{"id": 30403, "name": "Spreadsheet"},
{"id": 30404, "name": "Powerpoint"},
{"id": 30405, "name": "Books"}
].sort(sortCategory)
};
export const subCategories3: Categories = {
30201: [ // Under Movies
{"id": 3020101, "name": "Action & Adventure"},
{"id": 3020102, "name": "Comedy"},
{"id": 3020103, "name": "Drama"},
{"id": 3020104, "name": "Fantasy & Science Fiction"},
{"id": 3020105, "name": "Horror & Thriller"},
{"id": 3020106, "name": "Documentaries"},
{"id": 3020107, "name": "Animated"},
{"id": 3020108, "name": "Family & Kids"},
{"id": 3020109, "name": "Romance"},
{"id": 3020110, "name": "Mystery & Crime"},
{"id": 3020111, "name": "Historical & War"},
{"id": 3020112, "name": "Musicals & Music Films"},
{"id": 3020113, "name": "Indie Films"},
{"id": 3020114, "name": "International Films"},
{"id": 3020115, "name": "Biographies & True Stories"},
{"id": 3020116, "name": "Other"}
].sort(sortCategory),
30202: [ // Under Series
{"id": 3020201, "name": "Dramas"},
{"id": 3020202, "name": "Comedies"},
{"id": 3020203, "name": "Reality & Competition"},
{"id": 3020204, "name": "Documentaries & Docuseries"},
{"id": 3020205, "name": "Sci-Fi & Fantasy"},
{"id": 3020206, "name": "Crime & Mystery"},
{"id": 3020207, "name": "Animated Series"},
{"id": 3020208, "name": "Kids & Family"},
{"id": 3020209, "name": "Historical & Period Pieces"},
{"id": 3020210, "name": "Action & Adventure"},
{"id": 3020211, "name": "Horror & Thriller"},
{"id": 3020212, "name": "Romance"},
{"id": 3020213, "name": "Anthologies"},
{"id": 3020214, "name": "International Series"},
{"id": 3020215, "name": "Miniseries"},
{"id": 3020216, "name": "Other"}
].sort(sortCategory),
30405: [ // Under Books
{"id": 3040501, "name": "Fiction"},
{"id": 3040502, "name": "Non-Fiction"},
{"id": 3040503, "name": "Science Fiction & Fantasy"},
{"id": 3040504, "name": "Biographies & Memoirs"},
{"id": 3040505, "name": "Children's Books"},
{"id": 3040506, "name": "Educational"},
{"id": 3040507, "name": "Self-Help"},
{"id": 3040508, "name": "Cookbooks, Food & Wine"},
{"id": 3040509, "name": "Mystery & Thriller"},
{"id": 3040510, "name": "History"},
{"id": 3040511, "name": "Poetry"},
{"id": 3040512, "name": "Art & Photography"},
{"id": 3040513, "name": "Religion & Spirituality"},
{"id": 3040514, "name": "Travel"},
{"id": 3040515, "name": "Comics & Graphic Novels"},
].sort(sortCategory),
30101: [ // Under Music
{"id": 3010101, "name": "Rock"},
{"id": 3010102, "name": "Pop"},
{"id": 3010103, "name": "Classical"},
{"id": 3010104, "name": "Jazz"},
{"id": 3010105, "name": "Electronic"},
{"id": 3010106, "name": "Country"},
{"id": 3010107, "name": "Hip Hop/Rap"},
{"id": 3010108, "name": "Blues"},
{"id": 3010109, "name": "R&B/Soul"},
{"id": 3010110, "name": "Reggae"},
{"id": 3010111, "name": "Folk"},
{"id": 3010112, "name": "Metal"},
{"id": 3010113, "name": "World Music"},
{"id": 3010114, "name": "Latin"},
{"id": 3010115, "name": "Indie"},
{"id": 3010116, "name": "Punk"},
{"id": 3010117, "name": "Soundtracks"},
{"id": 3010118, "name": "Children's Music"},
{"id": 3010119, "name": "New Age"},
{"id": 3010120, "name": "Classical Crossover"}
].sort(sortCategory)
};
export const icons = {
1: softwareIcon,
2: gamingIcon,
3: mediaIcon,
4: softwareIcon,
302: videoIcon,
301: audioIcon,
304: documentIcon
}

View File

@ -0,0 +1,57 @@
import audioIcon from "../../assets/icons/audio.webp";
import bookIcon from "../../assets/icons/book.webp";
import documentIcon from "../../assets/icons/document.webp";
import gamingIcon from "../../assets/icons/gaming.webp";
import imageIcon from "../../assets/icons/image.webp";
import softwareIcon from "../../assets/icons/software.webp";
import unknownIcon from "../../assets/icons/unknown.webp";
import videoIcon from "../../assets/icons/video.webp";
import {
audioSubCategories,
bookSubCategories,
documentSubCategories,
imageSubCategories,
softwareSubCategories,
videoSubCategories,
} from "./2ndCategories.ts";
import { musicSubCategories } from "./3rdCategories.ts";
import {
Categories,
Category,
CategoryData,
} from "../../components/common/CategoryList/CategoryList.tsx";
import {
getAllCategoriesWithIcons,
sortCategory,
} from "./CategoryFunctions.ts";
export const firstCategories: Category[] = [
{ id: 1, name: "Software", icon: softwareIcon },
{ id: 2, name: "Gaming", icon: gamingIcon },
{ id: 3, name: "Audio", icon: audioIcon },
{ id: 4, name: "Video", icon: videoIcon },
{ id: 5, name: "Image", icon: imageIcon },
{ id: 6, name: "Document", icon: documentIcon },
{ id: 7, name: "Book", icon: bookIcon },
{ id: 99, name: "Other", icon: unknownIcon },
].sort(sortCategory);
export const secondCategories: Categories = {
1: softwareSubCategories.sort(sortCategory),
3: audioSubCategories.sort(sortCategory),
4: videoSubCategories.sort(sortCategory),
5: imageSubCategories.sort(sortCategory),
6: documentSubCategories.sort(sortCategory),
7: bookSubCategories.sort(sortCategory),
};
export const thirdCategories: Categories = {
301: musicSubCategories,
};
export const allCategoryData: CategoryData = {
category: firstCategories,
subCategories: [secondCategories, thirdCategories],
};
export const iconCategories = getAllCategoriesWithIcons();

View File

@ -0,0 +1,88 @@
export const softwareSubCategories = [
{ id: 101, name: "OS" },
{ id: 102, name: "Application" },
{ id: 103, name: "Source Code" },
{ id: 104, name: "Plugin" },
{ id: 199, name: "Other" },
];
export const audioSubCategories = [
{ id: 301, name: "Music" },
{ id: 302, name: "Podcast" },
{ id: 303, name: "Audiobook" },
{ id: 304, name: "Sound Effect" },
{ id: 305, name: "Lecture or Speech" },
{ id: 306, name: "Radio Show" },
{ id: 307, name: "Ambient Sound" },
{ id: 308, name: "Language Learning Material" },
{ id: 309, name: "Comedy & Satire" },
{ id: 310, name: "Documentary" },
{ id: 311, name: "Guided Meditation & Yoga" },
{ id: 312, name: "Live Performance" },
{ id: 313, name: "Nature Sound" },
{ id: 314, name: "Soundtrack" },
{ id: 315, name: "Interview" },
{ id: 399, name: "Other" },
];
export const videoSubCategories = [
{ id: 404, name: "Education" },
{ id: 405, name: "Lifestyle" },
{ id: 406, name: "Gaming" },
{ id: 407, name: "Technology" },
{ id: 408, name: "Sports" },
{ id: 409, name: "News & Politics" },
{ id: 410, name: "Cooking & Food" },
{ id: 411, name: "Animation" },
{ id: 412, name: "Science" },
{ id: 413, name: "Health & Wellness" },
{ id: 414, name: "DIY & Crafts" },
{ id: 415, name: "Kids & Family" },
{ id: 416, name: "Comedy" },
{ id: 417, name: "Travel & Adventure" },
{ id: 418, name: "Art & Design" },
{ id: 419, name: "Nature & Environment" },
{ id: 420, name: "Business & Finance" },
{ id: 421, name: "Personal Development" },
{ id: 423, name: "History" },
{ id: 499, name: "Other" },
];
export const imageSubCategories = [
{ id: 501, name: "Nature" },
{ id: 502, name: "Urban & Cityscapes" },
{ id: 503, name: "People & Portraits" },
{ id: 504, name: "Art & Abstract" },
{ id: 505, name: "Travel & Adventure" },
{ id: 506, name: "Animals & Wildlife" },
{ id: 507, name: "Sports & Action" },
{ id: 508, name: "Food & Cuisine" },
{ id: 509, name: "Fashion & Beauty" },
{ id: 510, name: "Technology & Science" },
{ id: 511, name: "Historical & Cultural" },
{ id: 512, name: "Aerial & Drone" },
{ id: 513, name: "Black & White" },
{ id: 514, name: "Events & Celebrations" },
{ id: 515, name: "Business & Corporate" },
{ id: 516, name: "Health & Wellness" },
{ id: 517, name: "Transportation & Vehicles" },
{ id: 518, name: "Still Life & Objects" },
{ id: 519, name: "Architecture & Buildings" },
{ id: 520, name: "Landscapes & Seascapes" },
{ id: 599, name: "Other" },
];
export const documentSubCategories = [
{ id: 601, name: "PDF" },
{ id: 602, name: "Word Document" },
{ id: 603, name: "Spreadsheet" },
{ id: 604, name: "Powerpoint" },
{ id: 699, name: "Other" },
];
export const bookSubCategories = [
{ id: 701, name: "Audiobook" },
{ id: 702, name: "Comic" },
{ id: 703, name: "Magazine" },
{ id: 799, name: "Other" },
];

View File

@ -0,0 +1,23 @@
export const musicSubCategories = [
{ id: 30101, name: "Rock" },
{ id: 30102, name: "Pop" },
{ id: 30103, name: "Classical" },
{ id: 30104, name: "Jazz" },
{ id: 30105, name: "Electronic" },
{ id: 30106, name: "Country" },
{ id: 30107, name: "Hip Hop/Rap" },
{ id: 30108, name: "Blues" },
{ id: 30109, name: "R&B/Soul" },
{ id: 30110, name: "Reggae" },
{ id: 30111, name: "Folk" },
{ id: 30112, name: "Metal" },
{ id: 30113, name: "World Music" },
{ id: 30114, name: "Latin" },
{ id: 30115, name: "Indie" },
{ id: 30116, name: "Punk" },
{ id: 30117, name: "Soundtracks" },
{ id: 30118, name: "Children's Music" },
{ id: 30119, name: "New Age" },
{ id: 30120, name: "Classical Crossover" },
{ id: 30199, name: "Other" },
];

View File

@ -0,0 +1,91 @@
import {
Category,
getCategoriesFromObject,
} from "../../components/common/CategoryList/CategoryList.tsx";
import { allCategoryData, iconCategories } from "./1stCategories.ts";
export const sortCategory = (a: Category, b: Category) => {
if (a.name === "Other") return 1;
else if (b.name === "Other") return -1;
else return a.name.localeCompare(b.name);
};
type Direction = "forward" | "backward";
const findCategory = (categoryID: number) => {
return allCategoryData.category.find(category => {
return category.id === categoryID;
});
};
const findSubCategory = (
categoryID: number,
direction: Direction = "forward"
) => {
const subCategoriesList = allCategoryData.subCategories;
if (direction === "backward") subCategoriesList.reverse();
for (const subCategories of subCategoriesList) {
for (const subCategoryID in subCategories) {
const returnValue = subCategories[subCategoryID].find(categoryObj => {
return categoryObj.id === categoryID;
});
if (returnValue) return returnValue;
}
}
};
export const findCategoryData = (
categoryID: number,
direction: Direction = "forward"
) => {
return direction === "forward"
? findCategory(categoryID) || findSubCategory(categoryID, "forward")
: findSubCategory(categoryID, "backward") || findCategory(categoryID);
};
export const findAllCategoryData = (
categories: string[],
direction: Direction = "forward"
) => {
let foundIcons: Category[] = [];
if (direction === "backward") categories.reverse();
categories.map(category => {
if (category) {
const icon = findCategoryData(+category, "backward");
if (icon) foundIcons.push(icon);
}
});
return foundIcons;
};
export const getCategoriesWithIcons = (categories: Category[]) => {
return categories.filter(category => {
return category.icon;
});
};
export const getAllCategoriesWithIcons = () => {
const categoriesWithIcons: Category[] = [];
allCategoryData.category.map(category => {
if (category.icon) categoriesWithIcons.push(category);
});
const subCategoriesList = allCategoryData.subCategories;
for (const subCategories of subCategoriesList) {
for (const subCategoryID in subCategories) {
const categoryWithIcon = subCategories[subCategoryID].map(categoryObj => {
if (categoryObj.icon) categoriesWithIcons.push(categoryObj);
});
}
}
return categoriesWithIcons;
};
export const getIconsFromObject = (fileObj: any) => {
const categories = getCategoriesFromObject(fileObj);
const icons = categories
.map(categoryID => {
return iconCategories.find(category => category.id === +categoryID)?.icon;
})
.reverse();
return icons.find(icon => icon !== undefined);
};

View File

@ -11,7 +11,3 @@ export const QSHARE_FILE_BASE = useTestIdentifiers
export const QSHARE_COMMENT_BASE = useTestIdentifiers export const QSHARE_COMMENT_BASE = useTestIdentifiers
? "qcomment_v1_MYTEST_" ? "qcomment_v1_MYTEST_"
: "qcomment_v1_qshare_"; : "qcomment_v1_qshare_";

View File

@ -1 +1,3 @@
export const titleFormatter = /[^a-zA-Z0-9\s-_!?()&'",.~;:|]/g; export const minPriceSuperlike = 10;
export const titleFormatter = /[^a-zA-Z0-9\s-_!?()&'",.;:|—~@#$%^*+=<>]/g;
export const titleFormatterOnSave = /[^a-zA-Z0-9\s-_!()&',.;—~@#$%^+=]/g;

View File

@ -1,108 +1,127 @@
import React from 'react' import React from "react";
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from "react-redux";
import { import {
addVideos, addFiles,
addToHashMap, addToHashMap,
setCountNewVideos, setCountNewFiles,
upsertVideos, upsertFiles,
upsertVideosBeginning, upsertFilesBeginning,
Video, Video,
upsertFilteredVideos upsertFilteredFiles,
} from '../state/features/videoSlice' } from "../state/features/fileSlice.ts";
import { import {
setIsLoadingGlobal, setUserAvatarHash setIsLoadingGlobal,
} from '../state/features/globalSlice' setUserAvatarHash,
import { RootState } from '../state/store' setTotalFilesPublished,
import { fetchAndEvaluateVideos } from '../utils/fetchVideos' setTotalNamesPublished,
import { QSHARE_PLAYLIST_BASE, QSHARE_FILE_BASE } from '../constants/Identifiers.ts' setFilesPerNamePublished,
import { RequestQueue } from '../utils/queue' } from "../state/features/globalSlice";
import { queue } from '../wrappers/GlobalWrapper' import { RootState } from "../state/store";
import { fetchAndEvaluateVideos } from "../utils/fetchVideos";
import {
QSHARE_PLAYLIST_BASE,
QSHARE_FILE_BASE,
} from "../constants/Identifiers.ts";
import { RequestQueue } from "../utils/queue";
import { queue } from "../wrappers/GlobalWrapper";
import { getCategoriesFetchString } from "../components/common/CategoryList/CategoryList.tsx";
export const useFetchFiles = () => { export const useFetchFiles = () => {
const dispatch = useDispatch() const dispatch = useDispatch();
const hashMapVideos = useSelector( const hashMapFiles = useSelector(
(state: RootState) => state.video.hashMapVideos (state: RootState) => state.file.hashMapFiles
) );
const videos = useSelector((state: RootState) => state.video.videos) const videos = useSelector((state: RootState) => state.file.files);
const userAvatarHash = useSelector( const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash (state: RootState) => state.global.userAvatarHash
) );
const filteredVideos = useSelector( const filteredVideos = useSelector(
(state: RootState) => state.video.filteredVideos (state: RootState) => state.file.filteredFiles
) );
const checkAndUpdateVideo = React.useCallback( const totalFilesPublished = useSelector(
(state: RootState) => state.global.totalFilesPublished
);
const totalNamesPublished = useSelector(
(state: RootState) => state.global.totalNamesPublished
);
const filesPerNamePublished = useSelector(
(state: RootState) => state.global.filesPerNamePublished
);
const checkAndUpdateFile = React.useCallback(
(video: Video) => { (video: Video) => {
const existingVideo = hashMapVideos[video.id] const existingVideo = hashMapFiles[video.id];
if (!existingVideo) { if (!existingVideo) {
return true return true;
} else if ( } else if (
video?.updated && video?.updated &&
existingVideo?.updated && existingVideo?.updated &&
(!existingVideo?.updated || video?.updated) > existingVideo?.updated (!existingVideo?.updated || video?.updated) > existingVideo?.updated
) { ) {
return true return true;
} else { } else {
return false return false;
} }
}, },
[hashMapVideos] [hashMapFiles]
) );
const getAvatar = React.useCallback(async (author: string) => { const getAvatar = React.useCallback(async (author: string) => {
try { try {
let url = await qortalRequest({ let url = await qortalRequest({
action: 'GET_QDN_RESOURCE_URL', action: "GET_QDN_RESOURCE_URL",
name: author, name: author,
service: 'THUMBNAIL', service: "THUMBNAIL",
identifier: 'qortal_avatar' identifier: "qortal_avatar",
});
dispatch(
setUserAvatarHash({
name: author,
url,
}) })
);
dispatch(setUserAvatarHash({
name: author,
url
}))
} catch (error) {} } catch (error) {}
}, []) }, []);
const getVideo = async (user: string, videoId: string, content: any, retries: number = 0) => { const getFile = async (
user: string,
videoId: string,
content: any,
retries: number = 0
) => {
try { try {
const res = await fetchAndEvaluateVideos({ const res = await fetchAndEvaluateVideos({
user, user,
videoId, videoId,
content content,
}) });
dispatch(addToHashMap(res)) dispatch(addToHashMap(res));
} catch (error) { } catch (error) {
retries= retries + 1 retries = retries + 1;
if (retries < 2) { // 3 is the maximum number of retries here, you can adjust it to your needs if (retries < 2) {
queue.push(() => getVideo(user, videoId, content, retries + 1)); // 3 is the maximum number of retries here, you can adjust it to your needs
queue.push(() => getFile(user, videoId, content, retries + 1));
} else { } else {
console.error('Failed to get video after 3 attempts', error); console.error("Failed to get video after 3 attempts", error);
} }
} }
};
const getNewFiles = React.useCallback(async () => {
}
const getNewVideos = React.useCallback(async () => {
try { try {
dispatch(setIsLoadingGlobal(true)) dispatch(setIsLoadingGlobal(true));
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QSHARE_FILE_BASE}&limit=20&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true`;
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QSHARE_FILE_BASE}&limit=20&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true`
const response = await fetch(url, { const response = await fetch(url, {
method: 'GET', method: "GET",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}) });
const responseData = await response.json() const responseData = await response.json();
// const responseData = await qortalRequest({ // const responseData = await qortalRequest({
// action: "SEARCH_QDN_RESOURCES", // action: "SEARCH_QDN_RESOURCES",
@ -116,16 +135,16 @@ export const useFetchFiles = () => {
// exactMatchNames: true, // exactMatchNames: true,
// name: names // name: names
// }) // })
const latestVideo = videos[0] const latestVideo = videos[0];
if (!latestVideo) return if (!latestVideo) return;
const findVideo = responseData?.findIndex( const findVideo = responseData?.findIndex(
(item: any) => item?.identifier === latestVideo?.id (item: any) => item?.identifier === latestVideo?.id
) );
let fetchAll = responseData let fetchAll = responseData;
let willFetchAll = true let willFetchAll = true;
if (findVideo !== -1) { if (findVideo !== -1) {
willFetchAll = false willFetchAll = false;
fetchAll = responseData.slice(0, findVideo) fetchAll = responseData.slice(0, findVideo);
} }
const structureData = fetchAll.map((video: any): Video => { const structureData = fetchAll.map((video: any): Video => {
@ -138,98 +157,82 @@ export const useFetchFiles = () => {
created: video?.created, created: video?.created,
updated: video?.updated, updated: video?.updated,
user: video.name, user: video.name,
videoImage: '', videoImage: "",
id: video.identifier id: video.identifier,
} };
}) });
if (!willFetchAll) { if (!willFetchAll) {
dispatch(upsertVideosBeginning(structureData)) dispatch(upsertFilesBeginning(structureData));
} }
if (willFetchAll) { if (willFetchAll) {
dispatch(addVideos(structureData)) dispatch(addFiles(structureData));
} }
setTimeout(() => { setTimeout(() => {
dispatch(setCountNewVideos(0)) dispatch(setCountNewFiles(0));
}, 1000) }, 1000);
for (const content of structureData) { for (const content of structureData) {
if (content.user && content.id) { if (content.user && content.id) {
const res = checkAndUpdateVideo(content) const res = checkAndUpdateFile(content);
if (res) { if (res) {
queue.push(() => getVideo(content.user, content.id, content)); queue.push(() => getFile(content.user, content.id, content));
} }
} }
} }
} catch (error) { } catch (error) {
} finally { } finally {
dispatch(setIsLoadingGlobal(false)) dispatch(setIsLoadingGlobal(false));
} }
}, [videos, hashMapVideos]) }, [videos, hashMapFiles]);
const getVideos = React.useCallback(async (filters = {}, reset?:boolean, resetFilers?: boolean,limit?: number) => { const getFiles = React.useCallback(
async (
filters = {},
reset?: boolean,
resetFilers?: boolean,
limit?: number
) => {
try { try {
const {name = '', const {
category = '', name = "",
subcategory = '', categories = [],
subcategory2 = '', keywords = "",
subcategory3 = '', type = "",
keywords = '', }: any = resetFilers ? {} : filters;
type = '' }: any = resetFilers ? {} : filters let offset = videos.length;
let offset = videos.length
if (reset) { if (reset) {
offset = 0 offset = 0;
} }
const videoLimit = limit || 50 const videoLimit = limit || 50;
let defaultUrl = `/arbitrary/resources/search?mode=ALL&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}&limit=${videoLimit}`; let defaultUrl = `/arbitrary/resources/search?mode=ALL&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}&limit=${videoLimit}`;
if (name) { if (name) {
defaultUrl += `&name=${name}`; defaultUrl += `&name=${name}`;
} }
if (category) { if (categories.length > 0) {
// Start with the category defaultUrl += "&description=" + getCategoriesFetchString(categories);
let description = `cat:${category}`;
// Check and append subcategory
if (subcategory) {
description += `;sub:${subcategory}`;
}
// Check and append subcategory2
if (subcategory2) {
description += `;sub2:${subcategory2}`;
}
// Check and append subcategory3
if (subcategory3) {
description += `;sub3:${subcategory3}`;
}
// Append the description to the URL
defaultUrl += `&description=${description}`;
} }
if (keywords) { if (keywords) {
defaultUrl = defaultUrl + `&query=${keywords}` defaultUrl = defaultUrl + `&query=${keywords}`;
} }
if(type === 'playlists'){ if (type === "playlists") {
defaultUrl = defaultUrl + `&service=PLAYLIST` defaultUrl = defaultUrl + `&service=PLAYLIST`;
defaultUrl = defaultUrl + `&identifier=${QSHARE_PLAYLIST_BASE}` defaultUrl = defaultUrl + `&identifier=${QSHARE_PLAYLIST_BASE}`;
} else { } else {
defaultUrl = defaultUrl + `&service=DOCUMENT` defaultUrl = defaultUrl + `&service=DOCUMENT`;
defaultUrl = defaultUrl + `&identifier=${QSHARE_FILE_BASE}` defaultUrl = defaultUrl + `&identifier=${QSHARE_FILE_BASE}`;
} }
// const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}&limit=${videoLimit}&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}` // const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}&limit=${videoLimit}&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}`
const url = defaultUrl const url = defaultUrl;
const response = await fetch(url, { const response = await fetch(url, {
method: 'GET', method: "GET",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}) });
const responseData = await response.json() const responseData = await response.json();
// const responseData = await qortalRequest({ // const responseData = await qortalRequest({
// action: "SEARCH_QDN_RESOURCES", // action: "SEARCH_QDN_RESOURCES",
@ -255,47 +258,45 @@ export const useFetchFiles = () => {
created: video?.created, created: video?.created,
updated: video?.updated, updated: video?.updated,
user: video.name, user: video.name,
videoImage: '', videoImage: "",
id: video.identifier id: video.identifier,
} };
}) });
if (reset) { if (reset) {
dispatch(addVideos(structureData)) dispatch(addFiles(structureData));
} else { } else {
dispatch(upsertVideos(structureData)) dispatch(upsertFiles(structureData));
} }
for (const content of structureData) { for (const content of structureData) {
if (content.user && content.id) { if (content.user && content.id) {
const res = checkAndUpdateVideo(content) const res = checkAndUpdateFile(content);
if (res) { if (res) {
queue.push(() => getVideo(content.user, content.id, content)); queue.push(() => getFile(content.user, content.id, content));
} }
} }
} }
} catch (error) { } catch (error) {
console.log({error}) console.log({ error });
} finally { } finally {
} }
}, [videos, hashMapVideos]) },
[videos, hashMapFiles]
);
const getVideosFiltered = React.useCallback(async (filterValue: string) => { const getFilesFiltered = React.useCallback(
async (filterValue: string) => {
try { try {
const offset = filteredVideos.length const offset = filteredVideos.length;
const replaceSpacesWithUnderscore = filterValue.replace(/ /g, '_'); const replaceSpacesWithUnderscore = filterValue.replace(/ /g, "_");
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${replaceSpacesWithUnderscore}&identifier=${QSHARE_FILE_BASE}&limit=10&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}` const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${replaceSpacesWithUnderscore}&identifier=${QSHARE_FILE_BASE}&limit=10&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}`;
const response = await fetch(url, { const response = await fetch(url, {
method: 'GET', method: "GET",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}) });
const responseData = await response.json() const responseData = await response.json();
// const responseData = await qortalRequest({ // const responseData = await qortalRequest({
// action: "SEARCH_QDN_RESOURCES", // action: "SEARCH_QDN_RESOURCES",
@ -321,38 +322,37 @@ export const useFetchFiles = () => {
created: video?.created, created: video?.created,
updated: video?.updated, updated: video?.updated,
user: video.name, user: video.name,
videoImage: '', videoImage: "",
id: video.identifier id: video.identifier,
} };
}) });
dispatch(upsertFilteredVideos(structureData)) dispatch(upsertFilteredFiles(structureData));
for (const content of structureData) { for (const content of structureData) {
if (content.user && content.id) { if (content.user && content.id) {
const res = checkAndUpdateVideo(content) const res = checkAndUpdateFile(content);
if (res) { if (res) {
queue.push(() => getVideo(content.user, content.id, content)); queue.push(() => getFile(content.user, content.id, content));
} }
} }
} }
} catch (error) { } catch (error) {
} finally { } finally {
} }
}, [filteredVideos, hashMapVideos]) },
[filteredVideos, hashMapFiles]
);
const checkNewVideos = React.useCallback(async () => { const checkNewFiles = React.useCallback(async () => {
try { try {
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QSHARE_FILE_BASE}&limit=20&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true`;
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QSHARE_FILE_BASE}&limit=20&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true`
const response = await fetch(url, { const response = await fetch(url, {
method: 'GET', method: "GET",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}) });
const responseData = await response.json() const responseData = await response.json();
// const responseData = await qortalRequest({ // const responseData = await qortalRequest({
// action: "SEARCH_QDN_RESOURCES", // action: "SEARCH_QDN_RESOURCES",
// mode: "ALL", // mode: "ALL",
@ -365,29 +365,57 @@ export const useFetchFiles = () => {
// exactMatchNames: true, // exactMatchNames: true,
// name: names // name: names
// }) // })
const latestVideo = videos[0] const latestVideo = videos[0];
if (!latestVideo) return if (!latestVideo) return;
const findVideo = responseData?.findIndex( const findVideo = responseData?.findIndex(
(item: any) => item?.identifier === latestVideo?.id (item: any) => item?.identifier === latestVideo?.id
) );
if (findVideo === -1) { if (findVideo === -1) {
dispatch(setCountNewVideos(responseData.length)) dispatch(setCountNewFiles(responseData.length));
return return;
} }
const newArray = responseData.slice(0, findVideo) const newArray = responseData.slice(0, findVideo);
dispatch(setCountNewVideos(newArray.length)) dispatch(setCountNewFiles(newArray.length));
return return;
} catch (error) {} } catch (error) {}
}, [videos]) }, [videos]);
const getFilesCount = React.useCallback(async () => {
try {
let url = `/arbitrary/resources/search?mode=ALL&includemetadata=false&limit=0&service=DOCUMENT&identifier=${QSHARE_FILE_BASE}`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const responseData = await response.json();
const totalFilesPublished = responseData.length;
const uniqueNames = new Set(responseData.map(video => video.name));
const totalNamesPublished = uniqueNames.size;
const filesPerNamePublished = (
totalFilesPublished / totalNamesPublished
).toFixed(2);
dispatch(setTotalFilesPublished(totalFilesPublished));
dispatch(setTotalNamesPublished(totalNamesPublished));
dispatch(setFilesPerNamePublished(filesPerNamePublished));
} catch (error) {
console.log({ error });
} finally {
}
}, []);
return { return {
getFiles: getVideos, getFiles,
checkAndUpdateVideo, checkAndUpdateFile,
getVideo, getFile,
hashMapVideos, hashMapFiles,
getNewFiles: getNewVideos, getNewFiles,
checkNewFiles: checkNewVideos, checkNewFiles,
getFilesFiltered: getVideosFiltered getFilesFiltered,
} getFilesCount,
} };
};

View File

@ -0,0 +1,85 @@
import { styled } from "@mui/system";
import { Box, Grid, Typography, Checkbox } from "@mui/material";
export const FilePlayerContainer = styled(Box)(({ theme }) => ({
maxWidth: "95%",
width: "1000px",
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
}));
export const FileTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Raleway",
fontSize: "20px",
color: theme.palette.text.primary,
userSelect: "none",
wordBreak: "break-word",
}));
export const FileDescription = styled(Typography)(({ theme }) => ({
fontFamily: "Raleway",
fontSize: "16px",
color: theme.palette.text.primary,
userSelect: "none",
wordBreak: "break-word",
}));
export const Spacer = ({ height }: any) => {
return (
<Box
sx={{
height: height,
}}
/>
);
};
export const StyledCardHeaderComment = styled(Box)({
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
gap: "5px",
padding: "7px 0px",
});
export const StyledCardCol = styled(Box)({
display: "flex",
overflow: "hidden",
flexDirection: "column",
gap: "2px",
alignItems: "flex-start",
width: "100%",
});
export const StyledCardColComment = styled(Box)({
display: "flex",
overflow: "hidden",
flexDirection: "column",
gap: "2px",
alignItems: "flex-start",
width: "100%",
});
export const AuthorTextComment = styled(Typography)({
fontFamily: "Raleway, sans-serif",
fontSize: "16px",
lineHeight: "1.2",
});
export const FileAttachmentContainer = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
gap: "20px",
padding: "5px 10px",
border: `1px solid ${theme.palette.text.primary}`,
}));
export const FileAttachmentFont = styled(Typography)(({ theme }) => ({
fontFamily: "Mulish",
color: theme.palette.text.primary,
fontSize: "16px",
letterSpacing: 0,
fontWeight: 400,
userSelect: "none",
whiteSpace: "nowrap",
}));

View File

@ -1,73 +1,68 @@
import React, { useState, useMemo, useRef, useEffect } from "react"; import React, { useEffect, useMemo, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { setIsLoadingGlobal } from "../../state/features/globalSlice"; import { setIsLoadingGlobal } from "../../state/features/globalSlice";
import { Avatar, Box, Typography, useTheme } from "@mui/material"; import { Avatar, Box, Typography, useTheme } from "@mui/material";
import { VideoPlayer } from "../../components/common/VideoPlayer";
import { RootState } from "../../state/store"; import { RootState } from "../../state/store";
import { addToHashMap } from "../../state/features/videoSlice"; import { addToHashMap } from "../../state/features/fileSlice.ts";
import AttachFileIcon from "@mui/icons-material/AttachFile"; import AttachFileIcon from "@mui/icons-material/AttachFile";
import DownloadIcon from "@mui/icons-material/Download"; import DownloadIcon from "@mui/icons-material/Download";
import mockImg from "../../test/mockimg.jpg";
import { import {
AuthorTextComment, AuthorTextComment,
FileAttachmentContainer, FileAttachmentContainer,
FileAttachmentFont, FileAttachmentFont,
FileDescription,
FilePlayerContainer,
FileTitle,
Spacer, Spacer,
StyledCardColComment, StyledCardColComment,
StyledCardHeaderComment, StyledCardHeaderComment,
VideoDescription, } from "./FileContent-styles.tsx";
VideoPlayerContainer, import { formatDate } from "../../utils/time";
VideoTitle,
} from "./VideoContent-styles";
import { setUserAvatarHash } from "../../state/features/globalSlice";
import {
formatDate,
formatDateSeconds,
formatTimestampSeconds,
} from "../../utils/time";
import { NavbarName } from "../../components/layout/Navbar/Navbar-styles";
import { CommentSection } from "../../components/common/Comments/CommentSection"; import { CommentSection } from "../../components/common/Comments/CommentSection";
import {
CrowdfundSubTitle,
CrowdfundSubTitleRow,
} from "../../components/PublishFile/Upload-styles.tsx";
import { QSHARE_FILE_BASE } from "../../constants/Identifiers.ts"; import { QSHARE_FILE_BASE } from "../../constants/Identifiers.ts";
import { Playlists } from "../../components/Playlists/Playlists";
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml"; import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
import FileElement from "../../components/common/FileElement"; import FileElement from "../../components/common/FileElement";
import {categories, subCategories, subCategories2, subCategories3} from "../../constants/Categories.ts"; import {
allCategoryData,
iconCategories,
} from "../../constants/Categories/1stCategories.ts";
import {
Category,
getCategoriesFromObject,
} from "../../components/common/CategoryList/CategoryList.tsx";
import {
findAllCategoryData,
findCategoryData,
getCategoriesWithIcons,
getIconsFromObject,
} from "../../constants/Categories/CategoryFunctions.ts";
export function formatBytes(bytes, decimals = 2) { export function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes'; if (bytes === 0) return "0 Bytes";
const k = 1024; const k = 1024;
const dm = decimals < 0 ? 0 : decimals; const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
} }
export const FileContent = () => {
export const VideoContent = () => {
const { name, id } = useParams(); const { name, id } = useParams();
const [isExpandedDescription, setIsExpandedDescription] = const [isExpandedDescription, setIsExpandedDescription] =
useState<boolean>(false); useState<boolean>(false);
const [descriptionHeight, setDescriptionHeight] = const [descriptionHeight, setDescriptionHeight] = useState<null | number>(
useState<null | number>(null); null
);
const [icon, setIcon] = useState<string>("");
const userAvatarHash = useSelector( const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash (state: RootState) => state.global.userAvatarHash
); );
const contentRef = useRef(null); const contentRef = useRef(null);
const avatarUrl = useMemo(() => { const avatarUrl = useMemo(() => {
let url = ""; let url = "";
if (name && userAvatarHash[name]) { if (name && userAvatarHash[name]) {
@ -79,15 +74,15 @@ export const VideoContent = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme(); const theme = useTheme();
const [videoData, setVideoData] = useState<any>(null); const [fileData, setFileData] = useState<any>(null);
const [playlistData, setPlaylistData] = useState<any>(null); const [playlistData, setPlaylistData] = useState<any>(null);
const hashMapVideos = useSelector( const hashMapVideos = useSelector(
(state: RootState) => state.video.hashMapVideos (state: RootState) => state.file.hashMapFiles
); );
const videoReference = useMemo(() => { const videoReference = useMemo(() => {
if (!videoData) return null; if (!fileData) return null;
const { videoReference } = videoData; const { videoReference } = fileData;
if ( if (
videoReference?.identifier && videoReference?.identifier &&
videoReference?.name && videoReference?.name &&
@ -97,13 +92,13 @@ export const VideoContent = () => {
} else { } else {
return null; return null;
} }
}, [videoData]); }, [fileData]);
const videoCover = useMemo(() => { const videoCover = useMemo(() => {
if (!videoData) return null; if (!fileData) return null;
const { videoImage } = videoData; const { videoImage } = fileData;
return videoImage || null; return videoImage || null;
}, [videoData]); }, [fileData]);
const dispatch = useDispatch(); const dispatch = useDispatch();
const getVideoData = React.useCallback(async (name: string, id: string) => { const getVideoData = React.useCallback(async (name: string, id: string) => {
@ -147,8 +142,7 @@ export const VideoContent = () => {
...resourceData, ...resourceData,
...responseData, ...responseData,
}; };
setFileData(combinedData);
setVideoData(combinedData);
dispatch(addToHashMap(combinedData)); dispatch(addToHashMap(combinedData));
checkforPlaylist(name, id, combinedData?.code); checkforPlaylist(name, id, combinedData?.code);
} }
@ -230,7 +224,7 @@ export const VideoContent = () => {
const existingVideo = hashMapVideos[id]; const existingVideo = hashMapVideos[id];
if (existingVideo) { if (existingVideo) {
setVideoData(existingVideo); setFileData(existingVideo);
checkforPlaylist(name, id, existingVideo?.code); checkforPlaylist(name, id, existingVideo?.code);
} else { } else {
getVideoData(name, id); getVideoData(name, id);
@ -272,25 +266,51 @@ export const VideoContent = () => {
useEffect(() => { useEffect(() => {
if (contentRef.current) { if (contentRef.current) {
const height = contentRef.current.offsetHeight; const height = contentRef.current.offsetHeight;
if (height > 100) { // Assuming 100px is your threshold if (height > 100) {
setDescriptionHeight(100) // Assuming 100px is your threshold
setDescriptionHeight(100);
} }
} }
}, [videoData]); if (fileData) {
//const icon = getIconsFromObject(fileData)[0]?.icon || null;
const icon = getIconsFromObject(fileData);
setIcon(icon);
}
}, [fileData]);
const categoriesDisplay = useMemo(() => { const categoriesDisplay = useMemo(() => {
const category = categories?.find((item)=> item?.id === videoData?.category) if (fileData) {
if(!category) return null const categoryList = getCategoriesFromObject(fileData);
const subcategory = subCategories[category?.id]?.find(item=> item?.id === videoData?.subcategory)
if(!subcategory) return category?.name
const subcategory2 = subCategories2[subcategory?.id]?.find(item => item.id === videoData?.subcategory2)
if(!subcategory2) return `${category?.name} > ${subcategory?.name}`
const subcategory3 = subCategories3[subcategory2?.id]?.find(item => item.id === videoData?.subcategory3)
if(!subcategory3) return `${category?.name} > ${subcategory?.name} > ${subcategory2?.name}`
return `${category?.name} > ${subcategory?.name} > ${subcategory2?.name} > ${subcategory3?.name}`
}, [videoData])
const categoryNames = categoryList.map((categoryID, index) => {
let categoryName: Category;
if (index === 0) {
categoryName = allCategoryData.category.find(
item => item?.id === +categoryList[0]
);
} else {
const subCategories = allCategoryData.subCategories[index - 1];
const selectedSubCategory = subCategories[categoryList[index - 1]];
if (selectedSubCategory) {
categoryName = selectedSubCategory.find(
item => item?.id === +categoryList[index]
);
}
}
return categoryName?.name;
});
const filteredCategoryNames = categoryNames.filter(name => name);
let categoryDisplay = "";
const separator = " > ";
filteredCategoryNames.map((name, index) => {
categoryDisplay +=
index !== filteredCategoryNames.length - 1 ? name + separator : name;
});
return categoryDisplay;
}
return "no videodata";
}, [fileData]);
return ( return (
<Box <Box
@ -301,24 +321,42 @@ export const VideoContent = () => {
padding: "20px 10px", padding: "20px 10px",
}} }}
> >
<VideoPlayerContainer <FilePlayerContainer
sx={{ sx={{
marginBottom: "30px", marginBottom: "30px",
}} }}
> >
<Spacer height="15px" /> <Spacer height="15px" />
<div
<VideoTitle style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
}}
>
{icon ? (
<img
src={icon}
width="50px"
style={{
borderRadius: "5px",
marginRight: "10px",
}}
/>
) : (
<AttachFileIcon />
)}
<FileTitle
variant="h1" variant="h1"
color="textPrimary" color="textPrimary"
sx={{ sx={{
textAlign: "center", textAlign: "center",
}} }}
> >
{videoData?.title} {fileData?.title}
</VideoTitle> </FileTitle>
{videoData?.created && ( </div>
{fileData?.created && (
<Typography <Typography
variant="h6" variant="h6"
sx={{ sx={{
@ -326,7 +364,7 @@ export const VideoContent = () => {
}} }}
color={theme.palette.text.primary} color={theme.palette.text.primary}
> >
{formatDate(videoData.created)} {formatDate(fileData.created)}
</Typography> </Typography>
)} )}
@ -367,11 +405,15 @@ export const VideoContent = () => {
</Box> </Box>
<Spacer height="15px" /> <Spacer height="15px" />
<Box> <Box>
<Typography sx={{ <Typography
fontWeight: 'bold', sx={{
fontSize: '16px', fontWeight: "bold",
userSelect: 'none' fontSize: "16px",
}}>{categoriesDisplay}</Typography> userSelect: "none",
}}
>
{categoriesDisplay}
</Typography>
</Box> </Box>
<Spacer height="15px" /> <Spacer height="15px" />
<Box <Box
@ -380,10 +422,16 @@ export const VideoContent = () => {
borderRadius: "5px", borderRadius: "5px",
padding: "5px", padding: "5px",
width: "100%", width: "100%",
cursor: !descriptionHeight ? "default" : isExpandedDescription ? "default" : "pointer", cursor: !descriptionHeight
? "default"
: isExpandedDescription
? "default"
: "pointer",
position: "relative", position: "relative",
}} }}
className={!descriptionHeight ? "": isExpandedDescription ? "" : "hover-click"} className={
!descriptionHeight ? "" : isExpandedDescription ? "" : "hover-click"
}
> >
{descriptionHeight && !isExpandedDescription && ( {descriptionHeight && !isExpandedDescription && (
<Box <Box
@ -404,24 +452,32 @@ export const VideoContent = () => {
<Box <Box
ref={contentRef} ref={contentRef}
sx={{ sx={{
height: !descriptionHeight ? 'auto' : isExpandedDescription ? "auto" : "100px", height: !descriptionHeight
? "auto"
: isExpandedDescription
? "auto"
: "100px",
overflow: "hidden", overflow: "hidden",
}} }}
> >
{videoData?.htmlDescription ? ( {fileData?.htmlDescription ? (
<DisplayHtml html={videoData?.htmlDescription} /> <DisplayHtml html={fileData?.htmlDescription} />
) : ( ) : (
<VideoDescription variant="body1" color="textPrimary" sx={{ <FileDescription
cursor: 'default' variant="body1"
}}> color="textPrimary"
{videoData?.fullDescription} sx={{
</VideoDescription> cursor: "default",
}}
>
{fileData?.fullDescription}
</FileDescription>
)} )}
</Box> </Box>
{descriptionHeight && ( {descriptionHeight && (
<Typography <Typography
onClick={() => { onClick={() => {
setIsExpandedDescription((prev) => !prev); setIsExpandedDescription(prev => !prev);
}} }}
sx={{ sx={{
fontWeight: "bold", fontWeight: "bold",
@ -434,44 +490,43 @@ export const VideoContent = () => {
{isExpandedDescription ? "Show less" : "...more"} {isExpandedDescription ? "Show less" : "...more"}
</Typography> </Typography>
)} )}
</Box> </Box>
<Box sx={{ <Box
width: '100%', sx={{
display: 'flex', width: "100%",
alignItems: 'flex-start', display: "flex",
flexDirection: 'column', alignItems: "flex-start",
gap: '25px', flexDirection: "column",
marginTop: '25px' gap: "25px",
}}> marginTop: "25px",
{videoData?.files?.map((file)=> { }}
>
{fileData?.files?.map((file, index) => {
return ( return (
<FileAttachmentContainer sx={{ <FileAttachmentContainer
width: '100%', sx={{
display: 'flex', width: "100%",
justifyContent: 'space-between' display: "flex",
}}> justifyContent: "space-between",
}}
<FileAttachmentFont> key={file.toString() + index}
{file.filename} >
</FileAttachmentFont> <FileAttachmentFont>{file.filename}</FileAttachmentFont>
<Box sx={{ <Box
sx={{
display: 'flex', display: "flex",
gap: '25px', gap: "25px",
alignItems: 'center', alignItems: "center",
}}
}}> >
<FileAttachmentFont> <FileAttachmentFont>
{formatBytes(file?.size || 0)} {formatBytes(file?.size || 0)}
</FileAttachmentFont> </FileAttachmentFont>
<FileElement <FileElement
fileInfo={{...file, fileInfo={{
...file,
filename: file?.filename, filename: file?.filename,
mimeType: file?.mimetype mimeType: file?.mimetype,
}} }}
jsonId={id} jsonId={id}
title={file?.filename} title={file?.filename}
@ -485,11 +540,10 @@ export const VideoContent = () => {
</FileElement> </FileElement>
</Box> </Box>
</FileAttachmentContainer> </FileAttachmentContainer>
) );
})} })}
</Box> </Box>
</VideoPlayerContainer> </FilePlayerContainer>
<Box <Box
sx={{ sx={{

View File

@ -1,77 +1,79 @@
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from "react";
import { useNavigate } from 'react-router-dom' import { useNavigate } from "react-router-dom";
import { useSelector } from 'react-redux' import { useSelector } from "react-redux";
import { RootState } from '../../state/store' import { RootState } from "../../state/store";
import { Avatar, Box, Button, Typography, useTheme } from "@mui/material";
import { useFetchFiles } from "../../hooks/useFetchFiles.tsx";
import LazyLoad from "../../components/common/LazyLoad";
import { import {
Avatar, BottomParent,
Box, NameContainer,
Button, VideoCard,
Typography, VideoCardName,
useTheme VideoCardTitle,
} from '@mui/material' FileContainer,
import { useFetchFiles } from '../../hooks/useFetchFiles.tsx' VideoUploadDate,
import LazyLoad from '../../components/common/LazyLoad' } from "./FileList-styles.tsx";
import { BottomParent, NameContainer, VideoCard, VideoCardName, VideoCardTitle, VideoContainer, VideoUploadDate } from './FileList-styles.tsx' import ResponsiveImage from "../../components/ResponsiveImage";
import ResponsiveImage from '../../components/ResponsiveImage' import { formatDate, formatTimestampSeconds } from "../../utils/time";
import { formatDate, formatTimestampSeconds } from '../../utils/time' import { ChannelCard, ChannelTitle } from "./Home-styles";
import { ChannelCard, ChannelTitle } from './Home-styles'
interface VideoListProps { interface VideoListProps {
mode?: string mode?: string;
} }
export const Channels = ({ mode }: VideoListProps) => { export const Channels = ({ mode }: VideoListProps) => {
const theme = useTheme() const theme = useTheme();
const navigate = useNavigate() const navigate = useNavigate();
const publishNames = useSelector((state: RootState)=> state.global.publishNames) const publishNames = useSelector(
(state: RootState) => state.global.publishNames
);
const userAvatarHash = useSelector( const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash (state: RootState) => state.global.userAvatarHash
) );
return ( return (
<Box sx={{ <Box
width: '100%', sx={{
display: 'flex', width: "100%",
flexDirection: 'column', display: "flex",
alignItems: 'center', flexDirection: "column",
minHeight: '50vh' alignItems: "center",
}}> minHeight: "50vh",
<VideoContainer> }}
{publishNames && publishNames?.slice(0, 10).map((name)=> { >
let avatarUrl = '' <FileContainer>
{publishNames &&
publishNames?.slice(0, 10).map(name => {
let avatarUrl = "";
if (userAvatarHash[name]) { if (userAvatarHash[name]) {
avatarUrl = userAvatarHash[name] avatarUrl = userAvatarHash[name];
} }
return ( return (
<Box <Box
sx={{ sx={{
display: 'flex', display: "flex",
flex: 0, flex: 0,
alignItems: 'center', alignItems: "center",
width: 'auto', width: "auto",
position: 'relative', position: "relative",
' @media (max-width: 450px)': { " @media (max-width: 450px)": {
width: '100%' width: "100%",
} },
}} }}
key={name} key={name}
> >
<ChannelCard <ChannelCard
onClick={() => { onClick={() => {
navigate(`/channel/${name}`) navigate(`/channel/${name}`);
}} }}
> >
<ChannelTitle>{name}</ChannelTitle> <ChannelTitle>{name}</ChannelTitle>
<ResponsiveImage src={avatarUrl} width={50} height={50} /> <ResponsiveImage src={avatarUrl} width={50} height={50} />
</ChannelCard> </ChannelCard>
</Box> </Box>
) );
})} })}
</VideoContainer> </FileContainer>
</Box> </Box>
) );
} };

View File

@ -1,7 +1,15 @@
import { styled } from "@mui/system"; import { styled } from "@mui/system";
import { Box, Grid, Typography, Checkbox, TextField, InputLabel, Autocomplete } from "@mui/material"; import {
Box,
Grid,
Typography,
Checkbox,
TextField,
InputLabel,
Autocomplete,
} from "@mui/material";
export const VideoContainer = styled(Box)(({ theme }) => ({ export const FileContainer = styled(Box)(({ theme }) => ({
position: "relative", position: "relative",
display: "flex", display: "flex",
padding: "15px", padding: "15px",
@ -9,7 +17,7 @@ export const VideoContainer = styled(Box)(({ theme }) => ({
gap: "20px", gap: "20px",
flexWrap: "wrap", flexWrap: "wrap",
justifyContent: "flex-start", justifyContent: "flex-start",
width: '100%' width: "100%",
})); }));
export const StoresRow = styled(Grid)(({ theme }) => ({ export const StoresRow = styled(Grid)(({ theme }) => ({
@ -21,8 +29,8 @@ export const StoresRow = styled(Grid)(({ theme }) => ({
width: "auto", width: "auto",
position: "relative", position: "relative",
"@media (max-width: 450px)": { "@media (max-width: 450px)": {
width: "100%" width: "100%",
} },
})); }));
export const VideoCard = styled(Grid)(({ theme }) => ({ export const VideoCard = styled(Grid)(({ theme }) => ({
@ -30,7 +38,7 @@ export const VideoCard = styled(Grid)(({ theme }) => ({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
height: "320px", height: "320px",
width: '300px', width: "300px",
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
borderRadius: "8px", borderRadius: "8px",
padding: "10px 15px", padding: "10px 15px",
@ -49,8 +57,8 @@ export const VideoCard = styled(Grid)(({ theme }) => ({
boxShadow: boxShadow:
theme.palette.mode === "dark" 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)" ? "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;" : "rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;",
} },
})); }));
export const StoreCardInfo = styled(Grid)(({ theme }) => ({ export const StoreCardInfo = styled(Grid)(({ theme }) => ({
@ -58,7 +66,7 @@ export const StoreCardInfo = styled(Grid)(({ theme }) => ({
flexDirection: "column", flexDirection: "column",
gap: "10px", gap: "10px",
padding: "5px", padding: "5px",
marginTop: "15px" marginTop: "15px",
})); }));
export const VideoImageContainer = styled(Grid)(({ theme }) => ({})); export const VideoImageContainer = styled(Grid)(({ theme }) => ({}));
@ -67,9 +75,9 @@ export const VideoCardImage = styled("img")(({ theme }) => ({
maxWidth: "300px", maxWidth: "300px",
minWidth: "150px", minWidth: "150px",
borderRadius: "5px", borderRadius: "5px",
height: '150px', height: "150px",
objectFit: 'fill', objectFit: "fill",
width: '266px', width: "266px",
})); }));
const DoubleLine = styled(Typography)` const DoubleLine = styled(Typography)`
@ -77,14 +85,14 @@ const DoubleLine = styled(Typography)`
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
overflow: hidden; overflow: hidden;
` `;
export const VideoCardTitle = styled(DoubleLine)(({ theme }) => ({ export const VideoCardTitle = styled(DoubleLine)(({ theme }) => ({
fontFamily: "Cairo", fontFamily: "Cairo",
fontSize: "16px", fontSize: "16px",
letterSpacing: "0.4px", letterSpacing: "0.4px",
color: theme.palette.text.primary, color: theme.palette.text.primary,
userSelect: "none" userSelect: "none",
})); }));
export const VideoCardName = styled(Typography)(({ theme }) => ({ export const VideoCardName = styled(Typography)(({ theme }) => ({
fontFamily: "Cairo", fontFamily: "Cairo",
@ -102,19 +110,19 @@ export const VideoCardName = styled(Typography)(({ theme }) => ({
fontSize: "12px", fontSize: "12px",
letterSpacing: "0.4px", letterSpacing: "0.4px",
color: theme.palette.text.primary, color: theme.palette.text.primary,
userSelect: "none" userSelect: "none",
})); }));
export const BottomParent = styled(Box)(({ theme }) => ({ export const BottomParent = styled(Box)(({ theme }) => ({
display: 'flex', display: "flex",
alignItems: 'flex-start', alignItems: "flex-start",
flexDirection: 'column' flexDirection: "column",
})); }));
export const VideoCardDescription = styled(Typography)(({ theme }) => ({ export const VideoCardDescription = styled(Typography)(({ theme }) => ({
fontFamily: "Karla", fontFamily: "Karla",
fontSize: "20px", fontSize: "20px",
letterSpacing: "0px", letterSpacing: "0px",
color: theme.palette.text.primary, color: theme.palette.text.primary,
userSelect: "none" userSelect: "none",
})); }));
export const StoreCardOwner = styled(Typography)(({ theme }) => ({ export const StoreCardOwner = styled(Typography)(({ theme }) => ({
@ -124,7 +132,7 @@ export const StoreCardOwner = styled(Typography)(({ theme }) => ({
position: "absolute", position: "absolute",
bottom: "5px", bottom: "5px",
right: "10px", right: "10px",
userSelect: "none" userSelect: "none",
})); }));
export const StoreCardYouOwn = styled(Box)(({ theme }) => ({ export const StoreCardYouOwn = styled(Box)(({ theme }) => ({
@ -136,7 +144,7 @@ export const StoreCardYouOwn = styled(Box)(({ theme }) => ({
gap: "5px", gap: "5px",
fontFamily: "Livvic", fontFamily: "Livvic",
fontSize: "15px", fontSize: "15px",
color: theme.palette.text.primary color: theme.palette.text.primary,
})); }));
export const MyStoresRow = styled(Grid)(({ theme }) => ({ export const MyStoresRow = styled(Grid)(({ theme }) => ({
@ -144,16 +152,16 @@ export const MyStoresRow = styled(Grid)(({ theme }) => ({
flexDirection: "row", flexDirection: "row",
justifyContent: "flex-end", justifyContent: "flex-end",
padding: "5px", padding: "5px",
width: "100%" width: "100%",
})); }));
export const NameContainer = styled(Box)(({ theme }) => ({ export const NameContainer = styled(Box)(({ theme }) => ({
display: "flex", display: "flex",
flexDirection: "row", flexDirection: "row",
justifyContent: "flex-start", justifyContent: "flex-start",
alignItems: 'center', alignItems: "center",
gap: '10px', gap: "10px",
marginBottom: '10px' marginBottom: "10px",
})); }));
export const MyStoresCard = styled(Box)(({ theme }) => ({ export const MyStoresCard = styled(Box)(({ theme }) => ({
@ -166,14 +174,14 @@ export const MyStoresCard = styled(Box)(({ theme }) => ({
padding: "5px 10px", padding: "5px 10px",
fontFamily: "Raleway", fontFamily: "Raleway",
fontSize: "18px", fontSize: "18px",
color: theme.palette.text.primary color: theme.palette.text.primary,
})); }));
export const MyStoresCheckbox = styled(Checkbox)(({ theme }) => ({ export const MyStoresCheckbox = styled(Checkbox)(({ theme }) => ({
color: "#c0d4ff", color: "#c0d4ff",
"&.Mui-checked": { "&.Mui-checked": {
color: "#6596ff" color: "#6596ff",
} },
})); }));
export const FiltersCol = styled(Grid)(({ theme }) => ({ export const FiltersCol = styled(Grid)(({ theme }) => ({
@ -183,13 +191,13 @@ export const FiltersCol = styled(Grid)(({ theme }) => ({
padding: "20px 15px", padding: "20px 15px",
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
borderTop: `1px solid ${theme.palette.background.paper}`, borderTop: `1px solid ${theme.palette.background.paper}`,
borderRight: `1px solid ${theme.palette.background.paper}` borderRight: `1px solid ${theme.palette.background.paper}`,
})); }));
export const FiltersContainer = styled(Box)(({ theme }) => ({ export const FiltersContainer = styled(Box)(({ theme }) => ({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "space-between" justifyContent: "space-between",
})); }));
export const FiltersRow = styled(Box)(({ theme }) => ({ export const FiltersRow = styled(Box)(({ theme }) => ({
@ -199,7 +207,7 @@ export const FiltersRow = styled(Box)(({ theme }) => ({
width: "100%", width: "100%",
padding: "0 15px", padding: "0 15px",
fontSize: "16px", fontSize: "16px",
userSelect: "none" userSelect: "none",
})); }));
export const FiltersTitle = styled(Typography)(({ theme }) => ({ export const FiltersTitle = styled(Typography)(({ theme }) => ({
@ -210,74 +218,73 @@ export const FiltersTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Raleway", fontFamily: "Raleway",
fontSize: "17px", fontSize: "17px",
color: theme.palette.text.primary, color: theme.palette.text.primary,
userSelect: "none" userSelect: "none",
})); }));
export const FiltersCheckbox = styled(Checkbox)(({ theme }) => ({ export const FiltersCheckbox = styled(Checkbox)(({ theme }) => ({
color: "#c0d4ff", color: "#c0d4ff",
"&.Mui-checked": { "&.Mui-checked": {
color: "#6596ff" color: "#6596ff",
} },
})); }));
export const FilterSelect = styled(Autocomplete)(({ theme }) => ({ export const FilterSelect = styled(Autocomplete)(({ theme }) => ({
"& #categories-select": { "& #categories-select": {
padding: "7px" padding: "7px",
}, },
"& .MuiSelect-placeholder": { "& .MuiSelect-placeholder": {
fontFamily: "Raleway", fontFamily: "Raleway",
fontSize: "17px", fontSize: "17px",
color: theme.palette.text.primary, color: theme.palette.text.primary,
userSelect: "none" userSelect: "none",
}, },
"& MuiFormLabel-root": { "& MuiFormLabel-root": {
fontFamily: "Raleway", fontFamily: "Raleway",
fontSize: "17px", fontSize: "17px",
color: theme.palette.text.primary, color: theme.palette.text.primary,
userSelect: "none" userSelect: "none",
} },
})); }));
export const FilterSelectMenuItems = styled(TextField)(({ theme }) => ({ export const FilterSelectMenuItems = styled(TextField)(({ theme }) => ({
fontFamily: "Raleway", fontFamily: "Raleway",
fontSize: "17px", fontSize: "17px",
color: theme.palette.text.primary, color: theme.palette.text.primary,
userSelect: "none" userSelect: "none",
})); }));
export const FiltersSubContainer = styled(Box)(({ theme }) => ({ export const FiltersSubContainer = styled(Box)(({ theme }) => ({
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
flexDirection: "column", flexDirection: "column",
gap: "5px" gap: "5px",
})); }));
export const FilterDropdownLabel = styled(InputLabel)(({ theme }) => ({ export const FilterDropdownLabel = styled(InputLabel)(({ theme }) => ({
fontFamily: "Raleway", fontFamily: "Raleway",
fontSize: "16px", fontSize: "16px",
color: theme.palette.text.primary color: theme.palette.text.primary,
})); }));
export const IconsBox = styled(Box)({ export const IconsBox = styled(Box)({
display: 'flex', display: "flex",
gap: "3px", gap: "3px",
position: 'absolute', position: "absolute",
top: '-20px', top: "-20px",
right: '-5px', right: "-5px",
transition: 'all 0.3s ease-in-out', transition: "all 0.3s ease-in-out",
}); });
export const BlockIconContainer = styled(Box)({ export const BlockIconContainer = styled(Box)({
display: 'flex', display: "flex",
boxShadow: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;", boxShadow: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;",
backgroundColor: '#fbfbfb', backgroundColor: "#fbfbfb",
color: "#c25252", color: "#c25252",
padding: '2px', padding: "2px",
borderRadius: '3px', borderRadius: "3px",
transition: 'all 0.3s ease-in-out', transition: "all 0.3s ease-in-out",
"&:hover": { "&:hover": {
cursor: 'pointer', cursor: "pointer",
transform: "scale(1.1)", transform: "scale(1.1)",
} },
}) });

View File

@ -1,326 +1,44 @@
import React, { useCallback, useEffect, useRef, useState } from "react"; import { Avatar, Box, Skeleton, Tooltip } from "@mui/material";
import { useNavigate } from "react-router-dom";
import ReactDOM from "react-dom";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "../../state/store";
import AttachFileIcon from '@mui/icons-material/AttachFile';
import {
Avatar,
Box,
Button,
FormControl,
Grid,
Input,
InputLabel,
MenuItem,
OutlinedInput,
Select,
SelectChangeEvent,
Skeleton,
Tooltip,
Typography,
useTheme,
} from "@mui/material";
import { useFetchFiles } from "../../hooks/useFetchFiles.tsx";
import LazyLoad from "../../components/common/LazyLoad";
import { import {
BlockIconContainer, BlockIconContainer,
BottomParent, BottomParent,
FilterSelect,
FiltersCheckbox,
FiltersCol,
FiltersContainer,
FiltersRow,
FiltersSubContainer,
FiltersTitle,
IconsBox, IconsBox,
NameContainer, NameContainer,
VideoCard, VideoCard,
VideoCardName, VideoCardName,
VideoCardTitle, VideoCardTitle,
VideoContainer, FileContainer,
VideoUploadDate, VideoUploadDate,
} from "./FileList-styles.tsx"; } from "./FileList-styles.tsx";
import ResponsiveImage from "../../components/ResponsiveImage"; import EditIcon from "@mui/icons-material/Edit";
import { formatDate, formatTimestampSeconds } from "../../utils/time";
import { Subtitle, SubtitleContainer } from "./Home-styles";
import { ExpandMoreSVG } from "../../assets/svgs/ExpandMoreSVG";
import { import {
addVideos,
blockUser, blockUser,
changeFilterType, setEditFile,
changeSelectedCategoryVideos, Video,
changeSelectedSubCategoryVideos, } from "../../state/features/fileSlice.ts";
changeSelectedSubCategoryVideos2,
changeSelectedSubCategoryVideos3,
changefilterName,
changefilterSearch,
clearVideoList,
setEditPlaylist,
setEditVideo,
} from "../../state/features/videoSlice";
import { Playlists } from "../../components/Playlists/Playlists";
import { PlaylistSVG } from "../../assets/svgs/PlaylistSVG";
import BlockIcon from "@mui/icons-material/Block"; import BlockIcon from "@mui/icons-material/Block";
import EditIcon from '@mui/icons-material/Edit'; import AttachFileIcon from "@mui/icons-material/AttachFile";
import { formatBytes } from "../VideoContent/VideoContent"; import { formatBytes } from "../FileContent/FileContent.tsx";
import {categories, icons, subCategories, subCategories2, subCategories3} from "../../constants/Categories.ts"; import { formatDate } from "../../utils/time.ts";
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../state/store.ts";
import { useNavigate } from "react-router-dom";
import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts";
interface VideoListProps { interface FileListProps {
mode?: string; files: Video[];
} }
export const FileList = ({ mode }: VideoListProps) => { export const FileList = ({ files }: FileListProps) => {
const theme = useTheme(); const hashMapFiles = useSelector(
const prevVal = useRef(""); (state: RootState) => state.file.hashMapFiles
const isFiltering = useSelector(
(state: RootState) => state.video.isFiltering
); );
const filterValue = useSelector(
(state: RootState) => state.video.filterValue
);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [showIcons, setShowIcons] = useState(null); const [showIcons, setShowIcons] = useState(null);
const filterType = useSelector((state: RootState) => state.video.filterType);
const setFilterType = (payload) => {
dispatch(changeFilterType(payload));
};
const filterSearch = useSelector(
(state: RootState) => state.video.filterSearch
);
const setFilterSearch = (payload) => {
dispatch(changefilterSearch(payload));
};
const filterName = useSelector((state: RootState) => state.video.filterName);
const setFilterName = (payload) => {
dispatch(changefilterName(payload));
};
const selectedCategoryVideos = useSelector(
(state: RootState) => state.video.selectedCategoryVideos
);
const setSelectedCategoryVideos = (payload) => {
dispatch(changeSelectedCategoryVideos(payload));
};
const selectedSubCategoryVideos = useSelector(
(state: RootState) => state.video.selectedSubCategoryVideos
);
const selectedSubCategoryVideos2 = useSelector(
(state: RootState) => state.video.selectedSubCategoryVideos2
);
const selectedSubCategoryVideos3 = useSelector(
(state: RootState) => state.video.selectedSubCategoryVideos3
);
const setSelectedSubCategoryVideos = (payload) => {
dispatch(changeSelectedSubCategoryVideos(payload));
};
const setSelectedSubCategoryVideos2 = (payload) => {
dispatch(changeSelectedSubCategoryVideos2(payload));
};
const setSelectedSubCategoryVideos3 = (payload) => {
dispatch(changeSelectedSubCategoryVideos3(payload));
};
const dispatch = useDispatch();
const filteredVideos = useSelector(
(state: RootState) => state.video.filteredVideos
);
const username = useSelector((state: RootState) => state.auth?.user?.name); const username = useSelector((state: RootState) => state.auth?.user?.name);
const isFilterMode = useRef(false); const dispatch = useDispatch();
const firstFetch = useRef(false);
const afterFetch = useRef(false);
const isFetchingFiltered = useRef(false);
const isFetching = useRef(false);
const hashMapVideos = useSelector(
(state: RootState) => state.video.hashMapVideos
);
const countNewVideos = useSelector(
(state: RootState) => state.video.countNewVideos
);
const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash
);
const { videos: globalVideos } = useSelector(
(state: RootState) => state.video
);
const navigate = useNavigate(); const navigate = useNavigate();
const { getFiles, getNewFiles, checkNewFiles, getFilesFiltered } =
useFetchFiles();
const getFilesHandler = React.useCallback(
async (reset?: boolean, resetFilers?: boolean) => {
if (!firstFetch.current || !afterFetch.current) return;
if (isFetching.current) return;
isFetching.current = true;
console.log({
category: selectedCategoryVideos?.id,
subcategory: selectedSubCategoryVideos?.id,
subcategory2: selectedSubCategoryVideos2?.id,
subcategory3: selectedSubCategoryVideos3?.id,
})
await getFiles(
{
name: filterName,
category: selectedCategoryVideos?.id,
subcategory: selectedSubCategoryVideos?.id,
subcategory2: selectedSubCategoryVideos2?.id,
subcategory3: selectedSubCategoryVideos3?.id,
keywords: filterSearch,
type: filterType,
},
reset ? true : false,
resetFilers
);
isFetching.current = false;
},
[
getFiles,
filterValue,
getFilesFiltered,
isFiltering,
filterName,
selectedCategoryVideos,
selectedSubCategoryVideos,
selectedSubCategoryVideos2,
selectedSubCategoryVideos3,
filterSearch,
filterType,
]
);
const searchOnEnter = e => {
if (e.keyCode == 13) {
getFilesHandler(true);
}
};
useEffect(() => {
if (isFiltering && filterValue !== prevVal?.current) {
prevVal.current = filterValue;
getFilesHandler();
}
}, [filterValue, isFiltering, filteredVideos]);
const getFilesHandlerMount = React.useCallback(async () => {
if (firstFetch.current) return;
firstFetch.current = true;
setIsLoading(true);
await getFiles();
afterFetch.current = true;
isFetching.current = false;
setIsLoading(false);
}, [getFiles]);
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;
getFilesHandlerMount();
} else {
firstFetch.current = true;
afterFetch.current = true;
}
}, [getFilesHandlerMount, globalVideos]);
const filtersToDefault = async () => {
setFilterType("videos");
setFilterSearch("");
setFilterName("");
setSelectedCategoryVideos(null);
setSelectedSubCategoryVideos(null);
ReactDOM.flushSync(() => {
getFilesHandler(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 handleOptionSubCategoryChangeVideos2 = (
event: SelectChangeEvent<string>,
subcategories: any[]
) => {
const optionId = event.target.value;
const selectedOption = subcategories.find(
(option) => option.id === +optionId
);
setSelectedSubCategoryVideos2(selectedOption || null);
};
const handleOptionSubCategoryChangeVideos3 = (
event: SelectChangeEvent<string>,
subcategories: any[]
) => {
const optionId = event.target.value;
const selectedOption = subcategories.find(
(option) => option.id === +optionId
);
setSelectedSubCategoryVideos3(selectedOption || null);
};
const blockUserFunc = async (user: string) => { const blockUserFunc = async (user: string) => {
if (user === "Q-Share") return; if (user === "Q-Share") return;
@ -332,381 +50,22 @@ export const FileList = ({ mode }: VideoListProps) => {
}); });
if (response === true) { if (response === true) {
dispatch(blockUser(user)) dispatch(blockUser(user));
} }
} catch (error) {} } catch (error) {}
}; };
return ( return (
<Grid container sx={{ width: "100%" }}> <FileContainer>
<FiltersCol item xs={12} md={2} sm={3}> {files.map((file: any, index: number) => {
<FiltersContainer> const existingFile = hashMapFiles[file?.id];
<Input
id="standard-adornment-name"
onChange={(e) => {
setFilterSearch(e.target.value);
}}
onKeyDown={searchOnEnter}
value={filterSearch}
placeholder="Search"
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);
}}
onKeyDown={searchOnEnter}
value={filterName}
placeholder="User's Name (Exact)"
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",
}}
/>
<FiltersTitle>
Categories
<ExpandMoreSVG
color={theme.palette.text.primary}
height={"22"}
width={"22"}
/>
</FiltersTitle>
<FiltersSubContainer>
<FormControl sx={{ width: "100%" }}>
<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>
)}
{selectedSubCategoryVideos &&
subCategories2[selectedSubCategoryVideos?.id] && (
<FormControl fullWidth sx={{ marginBottom: 2 }}>
<InputLabel
sx={{
fontSize: "16px",
}}
id="Sub-2x-Category"
>
Sub-2x-Category
</InputLabel>
<Select
labelId="Sub-2x-Category"
input={<OutlinedInput label="Sub-2x-Category" />}
value={selectedSubCategoryVideos2?.id || ""}
onChange={(e) =>
handleOptionSubCategoryChangeVideos2(
e,
subCategories2[selectedSubCategoryVideos?.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
},
},
}}
>
{subCategories2[selectedSubCategoryVideos.id].map(
(option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
)
)}
</Select>
</FormControl>
)}
{selectedSubCategoryVideos2 &&
subCategories3[selectedSubCategoryVideos2?.id] && (
<FormControl fullWidth sx={{ marginBottom: 2 }}>
<InputLabel
sx={{
fontSize: "16px",
}}
id="Sub-2x-Category"
>
Sub-3x-Category
</InputLabel>
<Select
labelId="Sub-3x-Category"
input={<OutlinedInput label="Sub-sx-Category" />}
value={selectedSubCategoryVideos3?.id || ""}
onChange={(e) =>
handleOptionSubCategoryChangeVideos3(
e,
subCategories3[selectedSubCategoryVideos2?.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
},
},
}}
>
{subCategories3[selectedSubCategoryVideos2.id].map(
(option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
)
)}
</Select>
</FormControl>
)}
</Box>
</FormControl>
</FiltersSubContainer>
{/* <FiltersTitle>
Type
<ExpandMoreSVG
color={theme.palette.text.primary}
height={"22"}
width={"22"}
/>
</FiltersTitle>
<FiltersSubContainer>
<FiltersRow>
Videos
<FiltersCheckbox
checked={filterType === "videos"}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setFilterType("videos");
}}
inputProps={{ "aria-label": "controlled" }}
/>
</FiltersRow>
<FiltersRow>
Playlists
<FiltersCheckbox
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={() => {
getFilesHandler(true);
}}
sx={{
marginTop: "20px",
}}
variant="contained"
>
Search
</Button>
</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>
<VideoContainer>
{videos.map((video: any, index: number) => {
const existingVideo = hashMapVideos[video?.id];
let hasHash = false; let hasHash = false;
let videoObj = video; let fileObj = file;
if (existingVideo) { if (existingFile) {
videoObj = existingVideo; fileObj = existingFile;
hasHash = true; hasHash = true;
} }
const icon = getIconsFromObject(fileObj);
const category = categories?.find(item => item?.id === videoObj?.category);
const subcategory = subCategories[category?.id]?.find(item => item?.id === videoObj?.subcategory);
const subcategory2 = subCategories2[subcategory?.id]?.find(item => item.id === videoObj?.subcategory2);
const subcategory3 = subCategories3[subcategory2?.id]?.find(item => item.id === videoObj?.subcategory3);
const catId = category?.id || null;
const subId = subcategory?.id || null;
const sub2Id = subcategory2?.id || null;
const sub3Id = subcategory3?.id || null;
const icon = icons[sub3Id] || icons[sub2Id] || icons[subId] || icons[catId] || null;
return ( return (
<Box <Box
sx={{ sx={{
@ -714,40 +73,37 @@ export const FileList = ({ mode }: VideoListProps) => {
alignItems: "center", alignItems: "center",
width: "100%", width: "100%",
height: "75px", height: "75px",
position:"relative" position: "relative",
}} }}
key={videoObj.id} key={fileObj.id}
onMouseEnter={() => setShowIcons(videoObj.id)} onMouseEnter={() => setShowIcons(fileObj.id)}
onMouseLeave={() => setShowIcons(null)} onMouseLeave={() => setShowIcons(null)}
> >
{hasHash ? ( {hasHash ? (
<> <>
<IconsBox <IconsBox
sx={{ sx={{
opacity: showIcons === videoObj.id ? 1 : 0, opacity: showIcons === fileObj.id ? 1 : 0,
zIndex: 2, zIndex: 2,
}} }}
> >
{videoObj?.user === username && ( {fileObj?.user === username && (
<Tooltip title="Edit video properties" placement="top"> <Tooltip title="Edit video properties" placement="top">
<BlockIconContainer> <BlockIconContainer>
<EditIcon <EditIcon
onClick={() => { onClick={() => {
dispatch(setEditVideo(videoObj)); dispatch(setEditFile(fileObj));
}} }}
/> />
</BlockIconContainer> </BlockIconContainer>
</Tooltip> </Tooltip>
)} )}
<Tooltip title="Block user content" placement="top"> <Tooltip title="Block user content" placement="top">
<BlockIconContainer> <BlockIconContainer>
<BlockIcon <BlockIcon
onClick={() => { onClick={() => {
blockUserFunc(videoObj?.user); blockUserFunc(fileObj?.user);
}} }}
/> />
</BlockIconContainer> </BlockIconContainer>
@ -755,52 +111,61 @@ export const FileList = ({ mode }: VideoListProps) => {
</IconsBox> </IconsBox>
<VideoCard <VideoCard
onClick={() => { onClick={() => {
navigate(`/share/${videoObj?.user}/${videoObj?.id}`); navigate(`/share/${fileObj?.user}/${fileObj?.id}`);
}} }}
sx={{ sx={{
height: '100%', height: "100%",
width: '100%', width: "100%",
display: 'flex', display: "flex",
gap: '25px', gap: "25px",
flexDirection: 'row', flexDirection: "row",
justifyContent: 'space-between' justifyContent: "space-between",
}} }}
> >
<Box
<Box sx={{ sx={{
display: "flex",
display: 'flex', gap: "25px",
gap: '25px', alignItems: "center",
alignItems: 'center' }}
}}> >
{icon ? <img src={icon} width="50px" style={{ {icon ? (
borderRadius: '5px' <img
}}/> : ( src={icon}
width="50px"
style={{
borderRadius: "5px",
}}
/>
) : (
<AttachFileIcon /> <AttachFileIcon />
)} )}
<VideoCardTitle sx={{ <VideoCardTitle
width: '100px' sx={{
}}> width: "100px",
{formatBytes(videoObj?.files.reduce((acc, cur) => acc + (cur?.size || 0), 0))} }}
>
{formatBytes(
fileObj?.files.reduce(
(acc, cur) => acc + (cur?.size || 0),
0
)
)}
</VideoCardTitle> </VideoCardTitle>
<VideoCardTitle>{videoObj.title}</VideoCardTitle> <VideoCardTitle>{fileObj.title}</VideoCardTitle>
</Box> </Box>
<BottomParent> <BottomParent>
<NameContainer <NameContainer
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
navigate(`/channel/${videoObj?.user}`); navigate(`/channel/${fileObj?.user}`);
}} }}
> >
<Avatar <Avatar
sx={{ height: 24, width: 24 }} sx={{ height: 24, width: 24 }}
src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`} src={`/arbitrary/THUMBNAIL/${fileObj?.user}/qortal_avatar`}
alt={`${videoObj?.user}'s avatar`} alt={`${fileObj?.user}'s avatar`}
/> />
<VideoCardName <VideoCardName
sx={{ sx={{
@ -809,13 +174,13 @@ export const FileList = ({ mode }: VideoListProps) => {
}, },
}} }}
> >
{videoObj?.user} {fileObj?.user}
</VideoCardName> </VideoCardName>
</NameContainer> </NameContainer>
{videoObj?.created && ( {fileObj?.created && (
<VideoUploadDate> <VideoUploadDate>
{formatDate(videoObj.created)} {formatDate(fileObj.created)}
</VideoUploadDate> </VideoUploadDate>
)} )}
</BottomParent> </BottomParent>
@ -834,18 +199,9 @@ export const FileList = ({ mode }: VideoListProps) => {
}} }}
/> />
)} )}
</Box> </Box>
); );
})} })}
</VideoContainer> </FileContainer>
<LazyLoad
onLoadMore={getFilesHandler}
isLoading={isLoading}
></LazyLoad>
</Box>
</Grid>
</Grid>
); );
}; };

View File

@ -1,8 +1,8 @@
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from 'react-router-dom' import { useNavigate, useParams } from "react-router-dom";
import { useSelector } from 'react-redux' import { useSelector } from "react-redux";
import { RootState } from '../../state/store' import { RootState } from "../../state/store";
import AttachFileIcon from '@mui/icons-material/AttachFile'; import AttachFileIcon from "@mui/icons-material/AttachFile";
import { import {
Avatar, Avatar,
@ -10,61 +10,71 @@ import {
Button, Button,
Skeleton, Skeleton,
Typography, Typography,
useTheme useTheme,
} from '@mui/material' } from "@mui/material";
import { useFetchFiles } from '../../hooks/useFetchFiles.tsx' import { useFetchFiles } from "../../hooks/useFetchFiles.tsx";
import LazyLoad from '../../components/common/LazyLoad' import LazyLoad from "../../components/common/LazyLoad";
import { BottomParent, NameContainer, VideoCard, VideoCardName, VideoCardTitle, VideoContainer, VideoUploadDate } from './FileList-styles.tsx' import {
import ResponsiveImage from '../../components/ResponsiveImage' BottomParent,
import { formatDate, formatTimestampSeconds } from '../../utils/time' NameContainer,
import { Video } from '../../state/features/videoSlice' VideoCard,
import { queue } from '../../wrappers/GlobalWrapper' VideoCardName,
import { QSHARE_FILE_BASE } from '../../constants/Identifiers.ts' VideoCardTitle,
import { formatBytes } from '../VideoContent/VideoContent' FileContainer,
import {categories, icons, subCategories, subCategories2, subCategories3} from "../../constants/Categories.ts"; VideoUploadDate,
} from "./FileList-styles.tsx";
import ResponsiveImage from "../../components/ResponsiveImage";
import { formatDate, formatTimestampSeconds } from "../../utils/time";
import { Video } from "../../state/features/fileSlice.ts";
import { queue } from "../../wrappers/GlobalWrapper";
import { QSHARE_FILE_BASE } from "../../constants/Identifiers.ts";
import { formatBytes } from "../FileContent/FileContent.tsx";
import {
firstCategories,
secondCategories,
thirdCategories,
fourthCategories,
iconCategories,
} from "../../constants/Categories/1stCategories.ts";
import { getCategoriesFromObject } from "../../components/common/CategoryList/CategoryList.tsx";
import {
findAllCategoryData,
findCategoryData,
getCategoriesWithIcons,
getIconsFromObject,
} from "../../constants/Categories/CategoryFunctions.ts";
interface VideoListProps { interface VideoListProps {
mode?: string mode?: string;
} }
export const FileListComponentLevel = ({ mode }: VideoListProps) => { export const FileListComponentLevel = ({ mode }: VideoListProps) => {
const { name: paramName } = useParams() const { name: paramName } = useParams();
const theme = useTheme() const theme = useTheme();
const [isLoading, setIsLoading] = useState<boolean>(true) const [isLoading, setIsLoading] = useState<boolean>(true);
const firstFetch = useRef(false) const firstFetch = useRef(false);
const afterFetch = useRef(false) const afterFetch = useRef(false);
const hashMapVideos = useSelector( const hashMapVideos = useSelector(
(state: RootState) => state.video.hashMapVideos (state: RootState) => state.file.hashMapFiles
) );
const countNewVideos = useSelector( const [videos, setVideos] = React.useState<Video[]>([]);
(state: RootState) => state.video.countNewVideos
)
const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash
)
const [videos, setVideos] = React.useState<Video[]>([]) const navigate = useNavigate();
const { getFile, getNewFiles, checkNewFiles, checkAndUpdateFile } =
const navigate = useNavigate() useFetchFiles();
const {
getVideo,
getNewFiles,
checkNewFiles,
checkAndUpdateVideo
} = useFetchFiles()
const getVideos = React.useCallback(async () => { const getVideos = React.useCallback(async () => {
try { try {
const offset = videos.length const offset = videos.length;
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QSHARE_FILE_BASE}_&limit=50&includemetadata=false&reverse=true&excludeblocked=true&name=${paramName}&exactmatchnames=true&offset=${offset}` const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QSHARE_FILE_BASE}_&limit=50&includemetadata=false&reverse=true&excludeblocked=true&name=${paramName}&exactmatchnames=true&offset=${offset}`;
const response = await fetch(url, { const response = await fetch(url, {
method: 'GET', method: "GET",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}) });
const responseData = await response.json() const responseData = await response.json();
const structureData = responseData.map((video: any): Video => { const structureData = responseData.map((video: any): Video => {
return { return {
@ -76,98 +86,74 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
created: video?.created, created: video?.created,
updated: video?.updated, updated: video?.updated,
user: video.name, user: video.name,
videoImage: '', videoImage: "",
id: video.identifier id: video.identifier,
} };
}) });
const copiedVideos: Video[] = [...videos] const copiedVideos: Video[] = [...videos];
structureData.forEach((video: Video) => { structureData.forEach((video: Video) => {
const index = videos.findIndex((p) => p.id === video.id) const index = videos.findIndex(p => p.id === video.id);
if (index !== -1) { if (index !== -1) {
copiedVideos[index] = video copiedVideos[index] = video;
} else { } else {
copiedVideos.push(video) copiedVideos.push(video);
} }
}) });
setVideos(copiedVideos) setVideos(copiedVideos);
for (const content of structureData) { for (const content of structureData) {
if (content.user && content.id) { if (content.user && content.id) {
const res = checkAndUpdateVideo(content) const res = checkAndUpdateFile(content);
if (res) { if (res) {
queue.push(() => getVideo(content.user, content.id, content)); queue.push(() => getFile(content.user, content.id, content));
} }
} }
} }
} catch (error) { } catch (error) {
} finally { } finally {
} }
}, [videos, hashMapVideos]) }, [videos, hashMapVideos]);
const getVideosHandler = React.useCallback(async () => { const getVideosHandler = React.useCallback(async () => {
if(!firstFetch.current || !afterFetch.current) return if (!firstFetch.current || !afterFetch.current) return;
await getVideos() await getVideos();
}, [getVideos]) }, [getVideos]);
const getVideosHandlerMount = React.useCallback(async () => { const getVideosHandlerMount = React.useCallback(async () => {
if(firstFetch.current) return if (firstFetch.current) return;
firstFetch.current = true firstFetch.current = true;
await getVideos() await getVideos();
afterFetch.current = true afterFetch.current = true;
setIsLoading(false) setIsLoading(false);
}, [getVideos]) }, [getVideos]);
useEffect(() => { useEffect(() => {
if (!firstFetch.current) { if (!firstFetch.current) {
getVideosHandlerMount() getVideosHandlerMount();
} }
}, [getVideosHandlerMount]);
}, [getVideosHandlerMount ])
return ( return (
<Box sx={{ <Box
width: '100%', sx={{
display: 'flex', width: "100%",
flexDirection: 'column', display: "flex",
alignItems: 'center' flexDirection: "column",
}}> alignItems: "center",
}}
>
<VideoContainer> <FileContainer>
{videos.map((video: any, index: number) => { {videos.map((file: any, index: number) => {
const existingVideo = hashMapVideos[video?.id]; const existingFile = hashMapVideos[file?.id];
let hasHash = false; let hasHash = false;
let videoObj = video; let fileObj = file;
if (existingVideo) { if (existingFile) {
videoObj = existingVideo; fileObj = existingFile;
hasHash = true; hasHash = true;
} }
const icon = getIconsFromObject(fileObj);
const category = categories?.find(item => item?.id === videoObj?.category);
const subcategory = subCategories[category?.id]?.find(item => item?.id === videoObj?.subcategory);
const subcategory2 = subCategories2[subcategory?.id]?.find(item => item.id === videoObj?.subcategory2);
const subcategory3 = subCategories3[subcategory2?.id]?.find(item => item.id === videoObj?.subcategory3);
const catId = category?.id || null;
const subId = subcategory?.id || null;
const sub2Id = subcategory2?.id || null;
const sub3Id = subcategory3?.id || null;
const icon = icons[sub3Id] || icons[sub2Id] || icons[subId] || icons[catId] || null;
return ( return (
<Box <Box
@ -176,61 +162,68 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
alignItems: "center", alignItems: "center",
width: "100%", width: "100%",
height: "75px", height: "75px",
position:"relative" position: "relative",
}} }}
key={videoObj.id} key={fileObj.id}
> >
{hasHash ? ( {hasHash ? (
<> <>
<VideoCard <VideoCard
onClick={() => { onClick={() => {
navigate(`/share/${videoObj?.user}/${videoObj?.id}`); navigate(`/share/${fileObj?.user}/${fileObj?.id}`);
}} }}
sx={{ sx={{
height: '100%', height: "100%",
width: '100%', width: "100%",
display: 'flex', display: "flex",
gap: '25px', gap: "25px",
flexDirection: 'row', flexDirection: "row",
justifyContent: 'space-between' justifyContent: "space-between",
}} }}
> >
<Box
<Box sx={{ sx={{
display: "flex",
display: 'flex', gap: "25px",
gap: '25px', alignItems: "center",
alignItems: 'center' }}
}}> >
{icon ? <img src={icon} width="50px" style={{ {icon ? (
borderRadius: '5px' <img
}}/> : ( src={icon}
width="50px"
style={{
borderRadius: "5px",
}}
/>
) : (
<AttachFileIcon /> <AttachFileIcon />
)} )}
<VideoCardTitle sx={{ <VideoCardTitle
width: '100px' sx={{
}}> width: "100px",
{formatBytes(videoObj?.files.reduce((acc, cur) => acc + (cur?.size || 0), 0))} }}
>
{formatBytes(
fileObj?.files.reduce(
(acc, cur) => acc + (cur?.size || 0),
0
)
)}
</VideoCardTitle> </VideoCardTitle>
<VideoCardTitle>{videoObj.title}</VideoCardTitle> <VideoCardTitle>{fileObj.title}</VideoCardTitle>
</Box> </Box>
<BottomParent> <BottomParent>
<NameContainer <NameContainer
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
navigate(`/channel/${videoObj?.user}`); navigate(`/channel/${fileObj?.user}`);
}} }}
> >
<Avatar <Avatar
sx={{ height: 24, width: 24 }} sx={{ height: 24, width: 24 }}
src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`} src={`/arbitrary/THUMBNAIL/${fileObj?.user}/qortal_avatar`}
alt={`${videoObj?.user}'s avatar`} alt={`${fileObj?.user}'s avatar`}
/> />
<VideoCardName <VideoCardName
sx={{ sx={{
@ -239,13 +232,13 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
}, },
}} }}
> >
{videoObj?.user} {fileObj?.user}
</VideoCardName> </VideoCardName>
</NameContainer> </NameContainer>
{videoObj?.created && ( {fileObj?.created && (
<VideoUploadDate> <VideoUploadDate>
{formatDate(videoObj.created)} {formatDate(fileObj.created)}
</VideoUploadDate> </VideoUploadDate>
)} )}
</BottomParent> </BottomParent>
@ -264,14 +257,11 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
}} }}
/> />
)} )}
</Box> </Box>
); );
})} })}
</VideoContainer> </FileContainer>
<LazyLoad onLoadMore={getVideosHandler} isLoading={isLoading}></LazyLoad> <LazyLoad onLoadMore={getVideosHandler} isLoading={isLoading}></LazyLoad>
</Box> </Box>
) );
} };

View File

@ -1,15 +1,323 @@
import React from 'react' import React, { useEffect, useRef, useState } from "react";
import { FileList } from './FileList.tsx' import ReactDOM from "react-dom";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../state/store";
import { FileList } from "./FileList.tsx";
import { Box, Button, Grid, Input, useTheme } from "@mui/material";
import { useFetchFiles } from "../../hooks/useFetchFiles.tsx";
import LazyLoad from "../../components/common/LazyLoad";
import { FiltersCol, FiltersContainer } from "./FileList-styles.tsx";
import { SubtitleContainer } from "./Home-styles";
import {
changefilterName,
changefilterSearch,
changeFilterType,
} from "../../state/features/fileSlice.ts";
import { allCategoryData } from "../../constants/Categories/1stCategories.ts";
import {
CategoryList,
CategoryListRef,
} from "../../components/common/CategoryList/CategoryList.tsx";
import { StatsData } from "../../components/StatsData.tsx";
import { useSelector } from 'react-redux' interface HomeProps {
import { RootState } from '../../state/store' mode?: string;
}
export const Home = ({ mode }: HomeProps) => {
const theme = useTheme();
const prevVal = useRef("");
const categoryListRef = useRef<CategoryListRef>(null);
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 totalFilesPublished = useSelector(
(state: RootState) => state.global.totalFilesPublished
);
const totalNamesPublished = useSelector(
(state: RootState) => state.global.totalNamesPublished
);
const filesPerNamePublished = useSelector(
(state: RootState) => state.global.filesPerNamePublished
);
const setFilterType = payload => {
dispatch(changeFilterType(payload));
};
const filterSearch = useSelector(
(state: RootState) => state.file.filterSearch
);
export const Home = () => { 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 isFetchingFiltered = useRef(false);
const isFetching = useRef(false);
const countNewFiles = useSelector(
(state: RootState) => state.file.countNewFiles
);
const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash
);
const { files: globalVideos } = useSelector((state: RootState) => state.file);
const setSelectedCategoryFiles = payload => {};
const dispatch = useDispatch();
const filteredFiles = useSelector(
(state: RootState) => state.file.filteredFiles
);
const {
getFiles,
checkAndUpdateFile,
getFile,
hashMapFiles,
getNewFiles,
checkNewFiles,
getFilesFiltered,
getFilesCount,
} = useFetchFiles();
const getFilesHandler = React.useCallback(
async (reset?: boolean, resetFilers?: boolean) => {
if (!firstFetch.current || !afterFetch.current) return;
if (isFetching.current) return;
isFetching.current = true;
const selectedCategories =
categoryListRef.current.getSelectedCategories() || [];
await getFiles(
{
name: filterName,
categories: selectedCategories,
keywords: filterSearch,
type: filterType,
},
reset,
resetFilers
);
isFetching.current = false;
},
[
getFiles,
filterValue,
getFilesFiltered,
isFiltering,
filterName,
filterSearch,
filterType,
]
);
const searchOnEnter = e => {
if (e.keyCode == 13) {
getFilesHandler(true);
}
};
useEffect(() => {
if (isFiltering && filterValue !== prevVal?.current) {
prevVal.current = filterValue;
getFilesHandler();
}
}, [filterValue, isFiltering, filteredFiles, getFilesCount]);
const getFilesHandlerMount = React.useCallback(async () => {
if (firstFetch.current) return;
firstFetch.current = true;
setIsLoading(true);
await getFiles();
afterFetch.current = true;
isFetching.current = false;
setIsLoading(false);
}, [getFiles]);
let videos = globalVideos;
if (isFiltering) {
videos = filteredFiles;
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;
getFilesHandlerMount();
} else {
firstFetch.current = true;
afterFetch.current = true;
}
}, [getFilesHandlerMount, globalVideos]);
const filtersToDefault = async () => {
setFilterType("videos");
setFilterSearch("");
setFilterName("");
categoryListRef.current?.clearCategories();
ReactDOM.flushSync(() => {
getFilesHandler(true, true);
});
};
return ( return (
<> <Grid container sx={{ width: "100%" }}>
<FileList /> <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={{
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);
}}
onKeyDown={searchOnEnter}
value={filterName}
placeholder="User's Name (Exact)"
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",
}}
/>
<CategoryList categoryData={allCategoryData} ref={categoryListRef} />
) <Button
} onClick={() => {
filtersToDefault();
}}
sx={{
marginTop: "20px",
}}
variant="contained"
>
reset
</Button>
<Button
onClick={() => {
getFilesHandler(true);
}}
sx={{
marginTop: "20px",
}}
variant="contained"
>
Search
</Button>
</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>
<FileList files={videos} />
<LazyLoad
onLoadMore={getFilesHandler}
isLoading={isLoading}
></LazyLoad>
</Box>
</Grid>
</Grid>
);
};

View File

@ -1,64 +1,69 @@
import React, { useMemo } from 'react' import React, { useMemo } from "react";
import { FileListComponentLevel } from '../Home/FileListComponentLevel.tsx' import { FileListComponentLevel } from "../Home/FileListComponentLevel.tsx";
import { HeaderContainer, ProfileContainer } from './Profile-styles' import { HeaderContainer, ProfileContainer } from "./Profile-styles";
import { AuthorTextComment, StyledCardColComment, StyledCardHeaderComment } from '../VideoContent/VideoContent-styles' import {
import { Avatar, Box, useTheme } from '@mui/material' AuthorTextComment,
import { useParams } from 'react-router-dom' StyledCardColComment,
import { useSelector } from 'react-redux' StyledCardHeaderComment,
import { setUserAvatarHash } from '../../state/features/globalSlice' } from "../FileContent/FileContent-styles.tsx";
import { RootState } from '../../state/store' import { Avatar, Box, useTheme } from "@mui/material";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { setUserAvatarHash } from "../../state/features/globalSlice";
import { RootState } from "../../state/store";
export const IndividualProfile = () => { export const IndividualProfile = () => {
const { name: paramName } = useParams() const { name: paramName } = useParams();
const userAvatarHash = useSelector( const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash (state: RootState) => state.global.userAvatarHash
) );
const theme = useTheme() const theme = useTheme();
const avatarUrl = useMemo(() => { const avatarUrl = useMemo(() => {
let url = '' let url = "";
if (paramName && userAvatarHash[paramName]) { if (paramName && userAvatarHash[paramName]) {
url = userAvatarHash[paramName] url = userAvatarHash[paramName];
} }
return url return url;
}, [userAvatarHash, paramName]) }, [userAvatarHash, paramName]);
return ( return (
<ProfileContainer> <ProfileContainer>
<HeaderContainer> <HeaderContainer>
<Box sx={{ <Box
cursor: 'pointer' sx={{
}} > cursor: "pointer",
}}
>
<StyledCardHeaderComment <StyledCardHeaderComment
sx={{ sx={{
'& .MuiCardHeader-content': { "& .MuiCardHeader-content": {
overflow: 'hidden' overflow: "hidden",
} },
}} }}
> >
<Box> <Box>
<Avatar src={`/arbitrary/THUMBNAIL/${paramName}/qortal_avatar`} alt={`${paramName}'s avatar`} /> <Avatar
src={`/arbitrary/THUMBNAIL/${paramName}/qortal_avatar`}
alt={`${paramName}'s avatar`}
/>
</Box> </Box>
<StyledCardColComment> <StyledCardColComment>
<AuthorTextComment <AuthorTextComment
color={ color={
theme.palette.mode === 'light' theme.palette.mode === "light"
? theme.palette.text.secondary ? theme.palette.text.secondary
: '#d6e8ff' : "#d6e8ff"
} }
> >
{paramName} {paramName}
</AuthorTextComment> </AuthorTextComment>
</StyledCardColComment> </StyledCardColComment>
</StyledCardHeaderComment> </StyledCardHeaderComment>
</Box> </Box>
</HeaderContainer> </HeaderContainer>
<FileListComponentLevel /> <FileListComponentLevel />
</ProfileContainer> </ProfileContainer>
);
) };
}

View File

@ -1,81 +0,0 @@
import { styled } from "@mui/system";
import { Box, Grid, Typography, Checkbox } from "@mui/material";
export const VideoPlayerContainer = styled(Box)(({ theme }) => ({
maxWidth: '95%',
width: '1000px',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
}));
export const VideoTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Raleway",
fontSize: "20px",
color: theme.palette.text.primary,
userSelect: "none",
wordBreak: "break-word"
}));
export const VideoDescription = styled(Typography)(({ theme }) => ({
fontFamily: "Raleway",
fontSize: "16px",
color: theme.palette.text.primary,
userSelect: "none",
wordBreak: "break-word"
}));
export const Spacer = ({height}: any)=> {
return <Box sx={{
height: height
}} />
}
export const StyledCardHeaderComment = styled(Box)({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
gap: '5px',
padding: '7px 0px'
})
export const StyledCardCol = styled(Box)({
display: 'flex',
overflow: 'hidden',
flexDirection: 'column',
gap: '2px',
alignItems: 'flex-start',
width: '100%'
})
export const StyledCardColComment = styled(Box)({
display: 'flex',
overflow: 'hidden',
flexDirection: 'column',
gap: '2px',
alignItems: 'flex-start',
width: '100%'
})
export const AuthorTextComment = styled(Typography)({
fontFamily: 'Raleway, sans-serif',
fontSize: '16px',
lineHeight: '1.2'
})
export const FileAttachmentContainer = styled(Box)(({ theme }) =>({
display: "flex",
alignItems: "center",
gap: "20px",
padding: "5px 10px",
border: `1px solid ${theme.palette.text.primary}`,
}));
export const FileAttachmentFont = styled(Typography)(({ theme }) => ({
fontFamily: "Mulish",
color: theme.palette.text.primary,
fontSize: "16px",
letterSpacing: 0,
fontWeight: 400,
userSelect: "none",
whiteSpace: 'nowrap'
}));

View File

@ -0,0 +1,188 @@
import { createSlice } from "@reduxjs/toolkit";
import { RootState } from "../store";
interface GlobalState {
files: Video[];
filteredFiles: Video[];
hashMapFiles: Record<string, Video>;
countNewFiles: number;
isFiltering: boolean;
filterValue: string;
filterType: string;
filterSearch: string;
filterName: string;
selectedCategoryFiles: any[];
editFileProperties: any;
editPlaylistProperties: any;
}
const initialState: GlobalState = {
files: [],
filteredFiles: [],
hashMapFiles: {},
countNewFiles: 0,
isFiltering: false,
filterValue: "",
filterType: "videos",
filterSearch: "",
filterName: "",
selectedCategoryFiles: [null, null, null, null],
editFileProperties: null,
editPlaylistProperties: null,
};
export interface Video {
title: string;
description: string;
created: number | string;
user: string;
service?: string;
videoImage?: string;
id: string;
category?: string;
categoryName?: string;
tags?: string[];
updated?: number | string;
isValid?: boolean;
code?: string;
}
export const fileSlice = createSlice({
name: "file",
initialState,
reducers: {
setEditFile: (state, action) => {
state.editFileProperties = action.payload;
},
setEditPlaylist: (state, action) => {
state.editPlaylistProperties = action.payload;
},
changeFilterType: (state, action) => {
state.filterType = action.payload;
},
changefilterSearch: (state, action) => {
state.filterSearch = action.payload;
},
changefilterName: (state, action) => {
state.filterName = action.payload;
},
setCountNewFiles: (state, action) => {
state.countNewFiles = action.payload;
},
addFiles: (state, action) => {
state.files = action.payload;
},
addFilteredFiles: (state, action) => {
state.filteredFiles = action.payload;
},
removeFile: (state, action) => {
const idToDelete = action.payload;
state.files = state.files.filter(item => item.id !== idToDelete);
state.filteredFiles = state.filteredFiles.filter(
item => item.id !== idToDelete
);
},
addFileToBeginning: (state, action) => {
state.files.unshift(action.payload);
},
clearFileList: state => {
state.files = [];
},
updateFile: (state, action) => {
const { id } = action.payload;
const index = state.files.findIndex(video => video.id === id);
if (index !== -1) {
state.files[index] = { ...action.payload };
}
const index2 = state.filteredFiles.findIndex(video => video.id === id);
if (index2 !== -1) {
state.filteredFiles[index2] = { ...action.payload };
}
},
addToHashMap: (state, action) => {
const video = action.payload;
state.hashMapFiles[video.id] = video;
},
updateInHashMap: (state, action) => {
const { id } = action.payload;
const video = action.payload;
state.hashMapFiles[id] = { ...video };
},
removeFromHashMap: (state, action) => {
const idToDelete = action.payload;
delete state.hashMapFiles[idToDelete];
},
addArrayToHashMap: (state, action) => {
const videos = action.payload;
videos.forEach((video: Video) => {
state.hashMapFiles[video.id] = video;
});
},
upsertFiles: (state, action) => {
action.payload.forEach((video: Video) => {
const index = state.files.findIndex(p => p.id === video.id);
if (index !== -1) {
state.files[index] = video;
} else {
state.files.push(video);
}
});
},
upsertFilteredFiles: (state, action) => {
action.payload.forEach((video: Video) => {
const index = state.filteredFiles.findIndex(p => p.id === video.id);
if (index !== -1) {
state.filteredFiles[index] = video;
} else {
state.filteredFiles.push(video);
}
});
},
upsertFilesBeginning: (state, action) => {
action.payload.reverse().forEach((video: Video) => {
const index = state.files.findIndex(p => p.id === video.id);
if (index !== -1) {
state.files[index] = video;
} else {
state.files.unshift(video);
}
});
},
setIsFiltering: (state, action) => {
state.isFiltering = action.payload;
},
setFilterValue: (state, action) => {
state.filterValue = action.payload;
},
blockUser: (state, action) => {
const username = action.payload;
state.files = state.files.filter(item => item.user !== username);
},
},
});
export const {
setCountNewFiles,
addFiles,
addFilteredFiles,
removeFile,
addFileToBeginning,
updateFile,
addToHashMap,
updateInHashMap,
removeFromHashMap,
addArrayToHashMap,
upsertFiles,
upsertFilteredFiles,
upsertFilesBeginning,
setIsFiltering,
setFilterValue,
clearFileList,
changeFilterType,
changefilterSearch,
changefilterName,
blockUser,
setEditFile,
setEditPlaylist,
} = fileSlice.actions;
export default fileSlice.reducer;

View File

@ -1,54 +1,68 @@
import { createSlice } from '@reduxjs/toolkit' import { createSlice } from "@reduxjs/toolkit";
interface GlobalState { interface GlobalState {
isLoadingGlobal: boolean isLoadingGlobal: boolean;
downloads: any downloads: any;
userAvatarHash: Record<string, string> userAvatarHash: Record<string, string>;
publishNames: string[] | null publishNames: string[] | null;
videoPlaying: any | null videoPlaying: any | null;
totalFilesPublished: number;
totalNamesPublished: number;
filesPerNamePublished: number;
} }
const initialState: GlobalState = { const initialState: GlobalState = {
isLoadingGlobal: false, isLoadingGlobal: false,
downloads: {}, downloads: {},
userAvatarHash: {}, userAvatarHash: {},
publishNames: null, publishNames: null,
videoPlaying: null videoPlaying: null,
} totalFilesPublished: null,
totalNamesPublished: null,
filesPerNamePublished: null,
};
export const globalSlice = createSlice({ export const globalSlice = createSlice({
name: 'global', name: "global",
initialState, initialState,
reducers: { reducers: {
setIsLoadingGlobal: (state, action) => { setIsLoadingGlobal: (state, action) => {
state.isLoadingGlobal = action.payload state.isLoadingGlobal = action.payload;
}, },
setAddToDownloads: (state, action) => { setAddToDownloads: (state, action) => {
const download = action.payload const download = action.payload;
state.downloads[download.identifier] = download state.downloads[download.identifier] = download;
}, },
updateDownloads: (state, action) => { updateDownloads: (state, action) => {
const { identifier } = action.payload const { identifier } = action.payload;
const download = action.payload const download = action.payload;
state.downloads[identifier] = { state.downloads[identifier] = {
...state.downloads[identifier], ...state.downloads[identifier],
...download ...download,
} };
}, },
setUserAvatarHash: (state, action) => { setUserAvatarHash: (state, action) => {
const avatar = action.payload const avatar = action.payload;
if (avatar?.name && avatar?.url) { if (avatar?.name && avatar?.url) {
state.userAvatarHash[avatar?.name] = avatar?.url state.userAvatarHash[avatar?.name] = avatar?.url;
} }
}, },
addPublishNames: (state, action) => { addPublishNames: (state, action) => {
state.publishNames = action.payload state.publishNames = action.payload;
}, },
setVideoPlaying: (state, action) => { setVideoPlaying: (state, action) => {
state.videoPlaying = action.payload state.videoPlaying = action.payload;
}, },
} setTotalFilesPublished: (state, action) => {
}) state.totalFilesPublished = action.payload;
},
setTotalNamesPublished: (state, action) => {
state.totalNamesPublished = action.payload;
},
setFilesPerNamePublished: (state, action) => {
state.filesPerNamePublished = action.payload;
},
},
});
export const { export const {
setIsLoadingGlobal, setIsLoadingGlobal,
@ -56,7 +70,10 @@ export const {
updateDownloads, updateDownloads,
setUserAvatarHash, setUserAvatarHash,
addPublishNames, addPublishNames,
setVideoPlaying setVideoPlaying,
} = globalSlice.actions setTotalFilesPublished,
setTotalNamesPublished,
setFilesPerNamePublished,
} = globalSlice.actions;
export default globalSlice.reducer export default globalSlice.reducer;

View File

@ -1,216 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
import { RootState } from '../store'
interface GlobalState {
videos: Video[]
filteredVideos: Video[]
hashMapVideos: Record<string, Video>
countNewVideos: number
isFiltering: boolean
filterValue: string
filterType: string
filterSearch: string
filterName: string
selectedCategoryVideos: any
selectedSubCategoryVideos: any
selectedSubCategoryVideos2: any
selectedSubCategoryVideos3: any
editVideoProperties: any
editPlaylistProperties: any
}
const initialState: GlobalState = {
videos: [],
filteredVideos: [],
hashMapVideos: {},
countNewVideos: 0,
isFiltering: false,
filterValue: '',
filterType: 'videos',
filterSearch: '',
filterName: '',
selectedCategoryVideos: null,
selectedSubCategoryVideos: null,
selectedSubCategoryVideos2: null,
selectedSubCategoryVideos3: null,
editVideoProperties: null,
editPlaylistProperties: null
}
export interface Video {
title: string
description: string
created: number | string
user: string
service?: string
videoImage?: string
id: string
category?: string
categoryName?: string
tags?: string[]
updated?: number | string
isValid?: boolean
code?: string
}
export const videoSlice = createSlice({
name: 'video',
initialState,
reducers: {
setEditVideo: (state, action) => {
state.editVideoProperties = action.payload
},
setEditPlaylist: (state, action) => {
state.editPlaylistProperties = action.payload
},
changeFilterType: (state, action) => {
state.filterType = action.payload
},
changefilterSearch: (state, action) => {
state.filterSearch = action.payload
},
changefilterName: (state, action) => {
state.filterName = action.payload
},
changeSelectedCategoryVideos: (state, action) => {
state.selectedCategoryVideos = action.payload
},
changeSelectedSubCategoryVideos: (state, action) => {
state.selectedSubCategoryVideos = action.payload
},
changeSelectedSubCategoryVideos2: (state, action) => {
state.selectedSubCategoryVideos2 = action.payload
},
changeSelectedSubCategoryVideos3: (state, action) => {
state.selectedSubCategoryVideos3 = action.payload
},
setCountNewVideos: (state, action) => {
state.countNewVideos = action.payload
},
addVideos: (state, action) => {
state.videos = action.payload
},
addFilteredVideos: (state, action) => {
state.filteredVideos = action.payload
},
removeVideo: (state, action) => {
const idToDelete = action.payload
state.videos = state.videos.filter((item) => item.id !== idToDelete)
state.filteredVideos = state.filteredVideos.filter(
(item) => item.id !== idToDelete
)
},
addVideoToBeginning: (state, action) => {
state.videos.unshift(action.payload)
},
clearVideoList: (state) => {
state.videos = []
},
updateVideo: (state, action) => {
const { id } = action.payload
const index = state.videos.findIndex((video) => video.id === id)
if (index !== -1) {
state.videos[index] = { ...action.payload }
}
const index2 = state.filteredVideos.findIndex((video) => video.id === id)
if (index2 !== -1) {
state.filteredVideos[index2] = { ...action.payload }
}
},
addToHashMap: (state, action) => {
const video = action.payload
state.hashMapVideos[video.id] = video
},
updateInHashMap: (state, action) => {
const { id } = action.payload
const video = action.payload
state.hashMapVideos[id] = { ...video }
},
removeFromHashMap: (state, action) => {
const idToDelete = action.payload
delete state.hashMapVideos[idToDelete]
},
addArrayToHashMap: (state, action) => {
const videos = action.payload
videos.forEach((video: Video) => {
state.hashMapVideos[video.id] = video
})
},
upsertVideos: (state, action) => {
action.payload.forEach((video: Video) => {
const index = state.videos.findIndex((p) => p.id === video.id)
if (index !== -1) {
state.videos[index] = video
} else {
state.videos.push(video)
}
})
},
upsertFilteredVideos: (state, action) => {
action.payload.forEach((video: Video) => {
const index = state.filteredVideos.findIndex((p) => p.id === video.id)
if (index !== -1) {
state.filteredVideos[index] = video
} else {
state.filteredVideos.push(video)
}
})
},
upsertVideosBeginning: (state, action) => {
action.payload.reverse().forEach((video: Video) => {
const index = state.videos.findIndex((p) => p.id === video.id)
if (index !== -1) {
state.videos[index] = video
} else {
state.videos.unshift(video)
}
})
},
setIsFiltering: (state, action) => {
state.isFiltering = action.payload
},
setFilterValue: (state, action) => {
state.filterValue = action.payload
},
blockUser: (state, action) => {
const username = action.payload
state.videos = state.videos.filter((item) => item.user !== username)
}
}
})
export const {
setCountNewVideos,
addVideos,
addFilteredVideos,
removeVideo,
addVideoToBeginning,
updateVideo,
addToHashMap,
updateInHashMap,
removeFromHashMap,
addArrayToHashMap,
upsertVideos,
upsertFilteredVideos,
upsertVideosBeginning,
setIsFiltering,
setFilterValue,
clearVideoList,
changeFilterType,
changefilterSearch,
changefilterName,
changeSelectedCategoryVideos,
changeSelectedSubCategoryVideos,
changeSelectedSubCategoryVideos2,
changeSelectedSubCategoryVideos3,
blockUser,
setEditVideo,
setEditPlaylist
} = videoSlice.actions
export default videoSlice.reducer

View File

@ -1,27 +1,27 @@
import { configureStore } from '@reduxjs/toolkit' import { configureStore } from "@reduxjs/toolkit";
import notificationsReducer from './features/notificationsSlice' import notificationsReducer from "./features/notificationsSlice";
import authReducer from './features/authSlice' import authReducer from "./features/authSlice";
import globalReducer from './features/globalSlice' import globalReducer from "./features/globalSlice";
import videoReducer from './features/videoSlice' import fileReducer from "./features/fileSlice.ts";
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
notifications: notificationsReducer, notifications: notificationsReducer,
auth: authReducer, auth: authReducer,
global: globalReducer, global: globalReducer,
video: videoReducer, file: fileReducer,
}, },
middleware: (getDefaultMiddleware) => middleware: getDefaultMiddleware =>
getDefaultMiddleware({ getDefaultMiddleware({
serializableCheck: false serializableCheck: false,
}), }),
preloadedState: undefined // optional, can be any valid state object preloadedState: undefined, // optional, can be any valid state object
}) });
// Define the RootState type, which is the type of the entire Redux state tree. // Define the RootState type, which is the type of the entire Redux state tree.
// This is useful when you need to access the state in a component or elsewhere. // This is useful when you need to access the state in a component or elsewhere.
export type RootState = ReturnType<typeof store.getState> export type RootState = ReturnType<typeof store.getState>;
// Define the AppDispatch type, which is the type of the Redux store's dispatch function. // Define the AppDispatch type, which is the type of the Redux store's dispatch function.
// This is useful when you need to dispatch an action in a component or elsewhere. // This is useful when you need to dispatch an action in a component or elsewhere.
export type AppDispatch = typeof store.dispatch export type AppDispatch = typeof store.dispatch;

View File

@ -0,0 +1,20 @@
export const objectIsNull = (variable: object) => {
return Object.is(variable, null);
};
export const objectIsUndefined = (variable: object) => {
return Object.is(variable, undefined);
};
export const printVar = (variable: object) => {
if (objectIsNull(variable)) {
console.log("variable is NULL");
return;
}
if (objectIsUndefined(variable)) {
console.log("variable is UNDEFINED");
return;
}
const [key, value] = Object.entries(variable)[0];
console.log(key, " is: ", value);
};