Browse Source

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
pull/2/head
Qortal Dev 6 months ago
parent
commit
9d483cf65d
  1. 10
      .prettierrc
  2. 20
      package-lock.json
  3. 1
      package.json
  4. 18
      src/App.tsx
  5. BIN
      src/assets/icons/book.webp
  6. BIN
      src/assets/icons/document.webp
  7. BIN
      src/assets/icons/image.webp
  8. BIN
      src/assets/icons/unknown.webp
  9. 628
      src/components/EditFile/EditFile.tsx
  10. 190
      src/components/EditPlaylist/EditPlaylist.tsx
  11. 326
      src/components/PlaylistListEdit/PlaylistListEdit.tsx
  12. 370
      src/components/PublishFile/PublishFile.tsx
  13. 61
      src/components/StatsData.tsx
  14. 9
      src/components/common/CategoryList/CategoryList-styles.tsx
  15. 284
      src/components/common/CategoryList/CategoryList.tsx
  16. 0
      src/components/common/CategoryList/CategorySelect.tsx
  17. 392
      src/components/common/MultiplePublish/MultiplePublishAll.tsx
  18. 95
      src/components/layout/Navbar/Navbar.tsx
  19. 221
      src/constants/Categories.ts
  20. 57
      src/constants/Categories/1stCategories.ts
  21. 88
      src/constants/Categories/2ndCategories.ts
  22. 23
      src/constants/Categories/3rdCategories.ts
  23. 91
      src/constants/Categories/CategoryFunctions.ts
  24. 8
      src/constants/Identifiers.ts
  25. 4
      src/constants/Misc.ts
  26. 570
      src/hooks/useFetchFiles.tsx
  27. 85
      src/pages/FileContent/FileContent-styles.tsx
  28. 370
      src/pages/FileContent/FileContent.tsx
  29. 126
      src/pages/Home/Channels.tsx
  30. 149
      src/pages/Home/FileList-styles.tsx
  31. 944
      src/pages/Home/FileList.tsx
  32. 374
      src/pages/Home/FileListComponentLevel.tsx
  33. 330
      src/pages/Home/Home.tsx
  34. 71
      src/pages/IndividualProfile/IndividualProfile.tsx
  35. 81
      src/pages/VideoContent/VideoContent-styles.tsx
  36. 188
      src/state/features/fileSlice.ts
  37. 69
      src/state/features/globalSlice.ts
  38. 216
      src/state/features/videoSlice.ts
  39. 24
      src/state/store.ts
  40. 20
      src/utils/utilFunctions.ts

10
.prettierrc

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

@ -17,6 +17,7 @@
"dompurify": "^3.0.6",
"localforage": "^1.10.0",
"moment": "^2.29.4",
"prettier": "^3.2.4",
"quill-image-resize-module-react": "^3.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@ -3429,6 +3430,20 @@
"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": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@ -6574,6 +6589,11 @@
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true
},
"prettier": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz",
"integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ=="
},
"prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",

1
package.json

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

18
src/App.tsx

@ -8,7 +8,7 @@ import { Provider } from "react-redux";
import GlobalWrapper from "./wrappers/GlobalWrapper";
import Notification from "./components/common/Notification/Notification";
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 { IndividualProfile } from "./pages/IndividualProfile/IndividualProfile";
@ -22,14 +22,14 @@ function App() {
<ThemeProvider theme={theme === "light" ? lightTheme : darkTheme}>
<Notification />
<DownloadWrapper>
<GlobalWrapper setTheme={(val: string) => setTheme(val)}>
<CssBaseline />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/share/:name/:id" element={<VideoContent />} />
<Route path="/channel/:name" element={<IndividualProfile />} />
</Routes>
</GlobalWrapper>
<GlobalWrapper setTheme={(val: string) => setTheme(val)}>
<CssBaseline />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/share/:name/:id" element={<FileContent />} />
<Route path="/channel/:name" element={<IndividualProfile />} />
</Routes>
</GlobalWrapper>
</DownloadWrapper>
</ThemeProvider>
</Provider>

BIN
src/assets/icons/book.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
src/assets/icons/document.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 28 KiB

BIN
src/assets/icons/image.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
src/assets/icons/unknown.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

628
src/components/EditFile/EditFile.tsx

@ -1,4 +1,4 @@
import React, {useEffect, useState} from "react";
import React, { useEffect, useRef, useState } from "react";
import {
CrowdfundActionButton,
CrowdfundActionButtonRow,
@ -6,34 +6,32 @@ import {
ModalBody,
NewCrowdfundTitle,
} from "./Upload-styles";
import {
Box,
FormControl,
InputLabel,
MenuItem,
Modal,
OutlinedInput,
Select,
SelectChangeEvent,
Typography,
useTheme,
} from "@mui/material";
import { Box, Modal, Typography, useTheme } from "@mui/material";
import RemoveIcon from "@mui/icons-material/Remove";
import ShortUniqueId from "short-unique-id";
import {useDispatch, useSelector} from "react-redux";
import {useDropzone} from "react-dropzone";
import {setNotification} from "../../state/features/notificationsSlice";
import {objectToBase64} from "../../utils/toBase64";
import {RootState} from "../../state/store";
import {setEditVideo, updateInHashMap, updateVideo,} from "../../state/features/videoSlice";
import {QSHARE_FILE_BASE,} from "../../constants/Identifiers.ts";
import {MultiplePublish} from "../common/MultiplePublish/MultiplePublishAll";
import {TextEditor} from "../common/TextEditor/TextEditor";
import {extractTextFromHTML} from "../common/TextEditor/utils";
import {categories, subCategories, subCategories2, subCategories3} from "../../constants/Categories.ts";
import {titleFormatter} from "../../constants/Misc.ts";
import { useDispatch, useSelector } from "react-redux";
import { useDropzone } from "react-dropzone";
import { setNotification } from "../../state/features/notificationsSlice";
import { objectToBase64 } from "../../utils/toBase64";
import { RootState } from "../../state/store";
import {
setEditFile,
updateFile,
updateInHashMap,
} from "../../state/features/fileSlice.ts";
import { QSHARE_FILE_BASE } from "../../constants/Identifiers.ts";
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll";
import { TextEditor } from "../common/TextEditor/TextEditor";
import { extractTextFromHTML } from "../common/TextEditor/utils";
import { allCategoryData } from "../../constants/Categories/1stCategories.ts";
import { titleFormatter } from "../../constants/Misc.ts";
import {
CategoryList,
CategoryListRef,
getCategoriesFromObject,
} from "../common/CategoryList/CategoryList.tsx";
const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 });
@ -52,8 +50,8 @@ interface VideoFile {
title: string;
description: string;
coverImage?: string;
identifier?:string;
filename?:string
identifier?: string;
filename?: string;
}
export const EditFile = () => {
const theme = useTheme();
@ -62,8 +60,8 @@ export const EditFile = () => {
const userAddress = useSelector(
(state: RootState) => state.auth?.user?.address
);
const editVideoProperties = useSelector(
(state: RootState) => state.video.editVideoProperties
const editFileProperties = useSelector(
(state: RootState) => state.file.editFileProperties
);
const [publishes, setPublishes] = useState<any>(null);
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
@ -75,154 +73,59 @@ export const EditFile = () => {
const [coverImage, setCoverImage] = useState<string>("");
const [file, setFile] = useState(null);
const [files, setFiles] = useState<VideoFile[]>([]);
const [editCategories, setEditCategories] = useState<string[]>([]);
const categoryListRef = useRef<CategoryListRef>(null);
const { getRootProps, getInputProps } = useDropzone({
maxFiles: 10,
maxSize: 419430400, // 400 MB in bytes
onDrop: (acceptedFiles, rejectedFiles) => {
const formatArray = acceptedFiles.map(item => {
return {
file: item,
title: "",
description: "",
coverImage: "",
};
});
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 { getRootProps, getInputProps } = useDropzone({
maxFiles: 10,
maxSize: 419430400, // 400 MB in bytes
onDrop: (acceptedFiles, rejectedFiles) => {
const formatArray = acceptedFiles.map((item) => {
return {
file: item,
title: "",
description: "",
coverImage: "",
};
});
setFiles((prev) => [...prev, ...formatArray]);
let errorString = null;
rejectedFiles.forEach(({ file, errors }) => {
errors.forEach((error) => {
if (error.code === "file-too-large") {
errorString = "File must be under 400mb";
}
console.log(`Error with file ${file.name}: ${error.message}`);
});
});
if (errorString) {
const notificationObj = {
msg: errorString,
alertType: "error",
};
dispatch(setNotification(notificationObj));
}
},
});
// 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]);
setFiles(prev => [...prev, ...formatArray]);
useEffect(() => {
if (editVideoProperties) {
setTitle(editVideoProperties?.title || "");
setFiles(editVideoProperties?.files || [])
if(editVideoProperties?.htmlDescription){
setDescription(editVideoProperties?.htmlDescription);
} else if(editVideoProperties?.fullDescription) {
const paragraph = `<p>${editVideoProperties?.fullDescription}</p>`
setDescription(paragraph);
let errorString = null;
rejectedFiles.forEach(({ file, errors }) => {
errors.forEach(error => {
if (error.code === "file-too-large") {
errorString = "File must be under 400mb";
}
console.log(`Error with file ${file.name}: ${error.message}`);
});
});
if (errorString) {
const notificationObj = {
msg: errorString,
alertType: "error",
};
dispatch(setNotification(notificationObj));
}
},
});
if (editVideoProperties?.category) {
const selectedOption = categories.find(
(option) => option.id === +editVideoProperties.category
);
setSelectedCategoryVideos(selectedOption || null);
}
if (
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);
useEffect(() => {
if (editFileProperties) {
setTitle(editFileProperties?.title || "");
setFiles(editFileProperties?.files || []);
if (editFileProperties?.htmlDescription) {
setDescription(editFileProperties?.htmlDescription);
} else if (editFileProperties?.fullDescription) {
const paragraph = `<p>${editFileProperties?.fullDescription}</p>`;
setDescription(paragraph);
}
setEditCategories(getCategoriesFromObject(editFileProperties));
}
}, [editVideoProperties]);
}, [editFileProperties]);
const onClose = () => {
dispatch(setEditVideo(null));
dispatch(setEditFile(null));
setVideoPropertiesToSetToRedux(null);
setFile(null);
setTitle("");
@ -232,12 +135,13 @@ export const EditFile = () => {
async function publishQDNResource() {
try {
const categoryList = categoryListRef.current?.getSelectedCategories();
if (!title) throw new Error("Please enter a title");
if (!description) throw new Error("Please enter a description");
if (!selectedCategoryVideos) throw new Error("Please select a category");
if (!editVideoProperties) return;
if (!categoryList[0]) throw new Error("Please select a category");
if (!editFileProperties) return;
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");
let errorMsg = "";
let name = "";
@ -249,7 +153,7 @@ export const EditFile = () => {
"Cannot publish without access to your name. Please authenticate.";
}
if (editVideoProperties?.user !== username) {
if (editFileProperties?.user !== username) {
errorMsg = "Cannot publish another user's resource";
}
@ -262,44 +166,37 @@ export const EditFile = () => {
);
return;
}
let fileReferences = []
let fileReferences = [];
let listOfPublishes = [];
const fullDescription = extractTextFromHTML(description);
const category = selectedCategoryVideos.id;
const subcategory = selectedSubCategoryVideos?.id || "";
const subcategory2 = selectedSubCategoryVideos2?.id || "";
const subcategory3 = selectedSubCategoryVideos3?.id || "";
const sanitizeTitle = title
.replace(/[^a-zA-Z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.trim()
.toLowerCase();
.replace(/[^a-zA-Z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.trim()
.toLowerCase();
for (const publish of files) {
if(publish?.identifier){
fileReferences.push(publish)
continue
if (publish?.identifier) {
fileReferences.push(publish);
continue;
}
const file = publish.file;
const id = uid();
const identifier = `${QSHARE_FILE_BASE}${sanitizeTitle.slice(0, 30)}_${id}`;
let fileExtension = "";
const fileExtensionSplit = file?.name?.split(".");
if (fileExtensionSplit?.length > 1) {
fileExtension = fileExtensionSplit?.pop() || "";
}
let firstPartName = fileExtensionSplit[0]
let firstPartName = fileExtensionSplit[0];
let filename = firstPartName.slice(0, 15);
// Step 1: Replace all white spaces with underscores
// Replace all forms of whitespace (including non-standard ones) with underscores
@ -311,18 +208,16 @@ export const EditFile = () => {
""
);
if(fileExtension){
filename = `${alphanumericString.trim()}.${fileExtension}`
if (fileExtension) {
filename = `${alphanumericString.trim()}.${fileExtension}`;
} else {
filename = alphanumericString
filename = alphanumericString;
}
let metadescription =
`**cat:${category};sub:${subcategory};sub2:${subcategory2};sub3:${subcategory3}**` +
`**${categoryListRef.current?.getCategoriesFetchString()}**` +
fullDescription.slice(0, 150);
const requestBodyVideo: any = {
action: "PUBLISH_QDN_RESOURCE",
name: name,
@ -339,27 +234,24 @@ export const EditFile = () => {
filename: file.name,
identifier,
name,
service: 'FILE',
service: "FILE",
mimetype: file.type,
size: file.size
})
size: file.size,
});
}
const fileObject: any = {
title,
version: editVideoProperties.version,
version: editFileProperties.version,
fullDescription,
htmlDescription: description,
commentsId: editVideoProperties.commentsId,
category,
subcategory,
subcategory2,
subcategory3,
files: fileReferences
commentsId: editFileProperties.commentsId,
...categoryListRef.current?.categoriesToObject(),
files: fileReferences,
};
let metadescription =
`**cat:${category};sub:${subcategory};sub2:${subcategory2}**` +
`**${categoryListRef.current?.getCategoriesFetchString()}**` +
fullDescription.slice(0, 150);
const crowdfundObjectToBase64 = await objectToBase64(fileObject);
@ -372,7 +264,7 @@ export const EditFile = () => {
data64: crowdfundObjectToBase64,
title: title.slice(0, 50),
description: metadescription,
identifier: editVideoProperties.id,
identifier: editFileProperties.id,
tag1: QSHARE_FILE_BASE,
filename: `video_metadata.json`,
};
@ -385,7 +277,7 @@ export const EditFile = () => {
setPublishes(multiplePublish);
setIsOpenMultiplePublish(true);
setVideoPropertiesToSetToRedux({
...editVideoProperties,
...editFileProperties,
...fileObject,
});
} 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 (
<>
<Modal
open={!!editVideoProperties}
open={!!editFileProperties}
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
@ -503,34 +353,36 @@ export const EditFile = () => {
<Typography>Click to add more files</Typography>
</Box>
{files.map((file, index) => {
const isExistingFile = !!file?.identifier
return (
<React.Fragment key={index}>
<Box
const isExistingFile = !!file?.identifier;
return (
<React.Fragment key={index}>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography>
{isExistingFile ? file.filename : file?.file?.name}
</Typography>
<RemoveIcon
onClick={() => {
setFiles(prev => {
const copyPrev = [...prev];
copyPrev.splice(index, 1);
return copyPrev;
});
}}
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
cursor: "pointer",
}}
>
<Typography>{isExistingFile? file.filename : file?.file?.name}</Typography>
<RemoveIcon
onClick={() => {
setFiles((prev) => {
const copyPrev = [...prev];
copyPrev.splice(index, 1);
return copyPrev;
});
}}
sx={{
cursor: "pointer",
}}
/>
</Box>
</React.Fragment>
);
})}
/>
</Box>
</React.Fragment>
);
})}
<Box
sx={{
display: "flex",
@ -538,164 +390,56 @@ export const EditFile = () => {
alignItems: "flex-start",
}}
>
{files?.length > 0 && (
<>
<Box sx={{
display: 'flex',
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>
{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 && (
{files?.length > 0 && (
<>
<CustomInputField
name="title"
label="Title of share"
variant="filled"
value={title}
onChange={(e) => {
const value = e.target.value;
const formattedValue = value.replace(titleFormatter, "");
setTitle(formattedValue);
}}
inputProps={{ maxLength: 180 }}
required
/>
<Typography
<Box
sx={{
fontSize: "18px",
display: "flex",
flexDirection: "column",
gap: "20px",
width: "100%",
}}
>
Description of share
</Typography>
<TextEditor
inlineContent={description}
setInlineContent={(value) => {
setDescription(value);
}}
/>
<CategoryList
categoryData={allCategoryData}
initialCategories={editCategories}
columns={3}
ref={categoryListRef}
/>
</Box>
</>
)}
</Box>
{files?.length > 0 && (
<>
<CustomInputField
name="title"
label="Title of share"
variant="filled"
value={title}
onChange={e => {
const value = e.target.value;
const formattedValue = value.replace(titleFormatter, "");
setTitle(formattedValue);
}}
inputProps={{ maxLength: 180 }}
required
/>
<Typography
sx={{
fontSize: "18px",
}}
>
Description of share
</Typography>
<TextEditor
inlineContent={description}
setInlineContent={value => {
setDescription(value);
}}
/>
</>
)}
</>
<CrowdfundActionButtonRow>
@ -730,22 +474,22 @@ export const EditFile = () => {
{isOpenMultiplePublish && (
<MultiplePublish
isOpen={isOpenMultiplePublish}
onError={(messageNotification)=> {
onError={messageNotification => {
setIsOpenMultiplePublish(false);
setPublishes(null)
if(messageNotification){
setPublishes(null);
if (messageNotification) {
dispatch(
setNotification({
msg: messageNotification,
alertType: 'error'
})
)
setNotification({
msg: messageNotification,
alertType: "error",
})
);
}
}}
onSubmit={() => {
setIsOpenMultiplePublish(false);
const clonedCopy = structuredClone(videoPropertiesToSetToRedux);
dispatch(updateVideo(clonedCopy));
dispatch(updateFile(clonedCopy));
dispatch(updateInHashMap(clonedCopy));
dispatch(
setNotification({

190
src/components/EditPlaylist/EditPlaylist.tsx

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

326
src/components/PlaylistListEdit/PlaylistListEdit.tsx

@ -7,8 +7,8 @@ import {
import { Box, Button, Input, Typography, useTheme } from "@mui/material";
import { useNavigate } from "react-router-dom";
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import { removeVideo } from "../../state/features/videoSlice";
import AddIcon from '@mui/icons-material/Add';
import { removeFile } from "../../state/features/fileSlice.ts";
import AddIcon from "@mui/icons-material/Add";
import { QSHARE_FILE_BASE } from "../../constants/Identifiers.ts";
import { useSelector } from "react-redux";
import { RootState } from "../../state/store";
@ -17,194 +17,194 @@ export const PlaylistListEdit = ({ playlistData, removeVideo, addVideo }) => {
const navigate = useNavigate();
const username = useSelector((state: RootState) => state.auth?.user?.name);
const [searchResults, setSearchResults] = useState([])
const [filterSearch, setFilterSearch] = useState("")
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 [searchResults, setSearchResults] = useState([]);
const [filterSearch, setFilterSearch] = useState("");
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 response = await fetch(url, {
method: 'GET',
method: "GET",
headers: {
'Content-Type': 'application/json'
}
})
const responseDataSearchVid = await response.json()
setSearchResults(responseDataSearchVid)
}
"Content-Type": "application/json",
},
});
const responseDataSearchVid = await response.json();
setSearchResults(responseDataSearchVid);
};
return (
<Box sx={{
display: 'flex',
gap: '10px',
width: '100%',
justifyContent: 'center'
}}>
<Box
<Box
sx={{
display: "flex",
flexDirection: "column",
maxWidth: "300px",
gap: "10px",
width: "100%",
justifyContent: "center",
}}
>
<CrowdfundSubTitleRow>
<CrowdfundSubTitle>Playlist</CrowdfundSubTitle>
</CrowdfundSubTitleRow>
<CardContentContainerComment
<Box
sx={{
marginTop: "25px",
height: "450px",
overflow: 'auto'
display: "flex",
flexDirection: "column",
maxWidth: "300px",
width: "100%",
}}
>
{playlistData?.videos?.map((vid, index) => {
return (
<Box
key={vid?.identifier}
sx={{
display: "flex",
gap: "10px",
width: "100%",
alignItems: "center",
padding: "10px",
borderRadius: "5px",
userSelect: "none",
}}
>
<Typography
sx={{
fontSize: "14px",
}}
>
{index + 1}
</Typography>
<Typography
<CrowdfundSubTitleRow>
<CrowdfundSubTitle>Playlist</CrowdfundSubTitle>
</CrowdfundSubTitleRow>
<CardContentContainerComment
sx={{
marginTop: "25px",
height: "450px",
overflow: "auto",
}}
>
{playlistData?.videos?.map((vid, index) => {
return (
<Box
key={vid?.identifier}
sx={{
fontSize: "18px",
wordBreak: 'break-word'
display: "flex",
gap: "10px",
width: "100%",
alignItems: "center",
padding: "10px",
borderRadius: "5px",
userSelect: "none",
}}
>
{vid?.metadata?.title}
</Typography>
<DeleteOutlineIcon
onClick={() => {
removeVideo(index);
}}
sx={{
cursor: "pointer",
}}
/>
</Box>
);
})}
</CardContentContainerComment>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
maxWidth: "300px",
width: "100%",
}}
>
<CrowdfundSubTitleRow>
<CrowdfundSubTitle>Add videos to playlist</CrowdfundSubTitle>
</CrowdfundSubTitleRow>
<CardContentContainerComment
<Typography
sx={{
fontSize: "14px",
}}
>
{index + 1}
</Typography>
<Typography
sx={{
fontSize: "18px",
wordBreak: "break-word",
}}
>
{vid?.metadata?.title}
</Typography>
<DeleteOutlineIcon
onClick={() => {
removeVideo(index);
}}
sx={{
cursor: "pointer",
}}
/>
</Box>
);
})}
</CardContentContainerComment>
</Box>
<Box
sx={{
marginTop: "25px",
height: "450px",
overflow: 'auto'
display: "flex",
flexDirection: "column",
maxWidth: "300px",
width: "100%",
}}
>
<Box sx={{
display: 'flex',
gap: '10px'
}}>
<Input
id="standard-adornment-name"
onChange={(e) => {
setFilterSearch(e.target.value);
}}
value={filterSearch}
placeholder="Search by title"
<CrowdfundSubTitleRow>
<CrowdfundSubTitle>Add videos to playlist</CrowdfundSubTitle>
</CrowdfundSubTitleRow>
<CardContentContainerComment
sx={{
marginTop: "25px",
height: "450px",
overflow: "auto",
}}
>
<Box
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",
}}
/>
<Button
onClick={() => {
search();
display: "flex",
gap: "10px",
}}
variant="contained"
>
Search
</Button>
</Box>
{searchResults?.map((vid, index) => {
return (
<Box
key={vid?.identifier}
<Input
id="standard-adornment-name"
onChange={e => {
setFilterSearch(e.target.value);
}}
value={filterSearch}
placeholder="Search by title"
sx={{
display: "flex",
gap: "10px",
width: "100%",
alignItems: "center",
padding: "10px",
borderRadius: "5px",
userSelect: "none",
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",
}}
/>
<Button
onClick={() => {
search();
}}
variant="contained"
>
<Typography
sx={{
fontSize: "14px",
}}
>
{index + 1}
</Typography>
<Typography
Search
</Button>
</Box>
{searchResults?.map((vid, index) => {
return (
<Box
key={vid?.identifier}
sx={{
fontSize: "18px",
wordBreak: 'break-word'
display: "flex",
gap: "10px",
width: "100%",
alignItems: "center",
padding: "10px",
borderRadius: "5px",
userSelect: "none",
}}
>
{vid?.metadata?.title}
</Typography>
<AddIcon
onClick={() => {
addVideo(vid);
}}
sx={{
cursor: "pointer",
}}
/>
</Box>
);
})}
</CardContentContainerComment>
</Box>
<Typography
sx={{
fontSize: "14px",
}}
>
{index + 1}
</Typography>
<Typography
sx={{
fontSize: "18px",
wordBreak: "break-word",
}}
>
{vid?.metadata?.title}
</Typography>
<AddIcon
onClick={() => {
addVideo(vid);
}}
sx={{
cursor: "pointer",
}}
/>
</Box>
);
})}
</CardContentContainerComment>
</Box>
</Box>
);
};

370
src/components/PublishFile/PublishFile.tsx

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

61
src/components/StatsData.tsx

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

9
src/components/common/CategoryList/CategoryList-styles.tsx

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

284
src/components/common/CategoryList/CategoryList.tsx

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

0
src/components/common/CategoryList/CategorySelect.tsx

392
src/components/common/MultiplePublish/MultiplePublishAll.tsx

@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
Box,
Button,
CircularProgress,
Modal,
Typography,
useTheme,
Box,
Button,
CircularProgress,
Modal,
Typography,
useTheme,
} from "@mui/material";
import React, { useCallback, useEffect, useState, useRef } from "react";
import { CircleSVG } from "../../../assets/svgs/CircleSVG";
@ -13,199 +13,213 @@ import { EmptyCircleSVG } from "../../../assets/svgs/EmptyCircleSVG";
import { styled } from "@mui/system";
interface Publish {
resources: any[];
action: string;
resources: any[];
action: string;
}
interface MultiplePublishProps {
publishes: Publish;
isOpen: boolean;
onSubmit: ()=> void
onError: (message?: string)=> void
publishes: Publish;
isOpen: boolean;
onSubmit: () => void;
onError: (message?: string) => void;
}
export const MultiplePublish = ({ publishes, isOpen, onSubmit, onError}: MultiplePublishProps) => {
const theme = useTheme();
const listOfSuccessfulPublishesRef = useRef([])
const [listOfSuccessfulPublishes, setListOfSuccessfulPublishes] = useState<
any[]
>([]);
const [listOfUnsuccessfulPublishes, setListOfUnSuccessfulPublishes] = useState<
any[]
>([]);
const [currentlyInPublish, setCurrentlyInPublish] = useState(null);
const hasStarted = useRef(false);
const publish = useCallback(async (pub: any) => {
const lengthOfResources = pub?.resources?.length
const lengthOfTimeout = lengthOfResources * 30000
return await qortalRequestWithTimeout(pub, lengthOfTimeout);
}, []);
const [isPublishing, setIsPublishing] = useState(true)
const handlePublish = useCallback(
async (pub: any) => {
try {
setCurrentlyInPublish(pub?.identifier);
setIsPublishing(true)
const res = await publish(pub);
onSubmit()
setListOfUnSuccessfulPublishes([])
} catch (error: any) {
const unsuccessfulPublishes = error?.error?.unsuccessfulPublishes || []
if(error?.error === 'User declined request'){
onError()
return
}
if(error?.error === 'The request timed out'){
onError("The request timed out")
return
}
if(unsuccessfulPublishes?.length > 0){
setListOfUnSuccessfulPublishes(unsuccessfulPublishes)
}
} finally {
setIsPublishing(false)
}
},
[publish]
);
const retry = ()=> {
let newlistOfMultiplePublishes: any[] = [];
listOfUnsuccessfulPublishes?.forEach((item)=> {
const findPub = publishes?.resources.find((res: any)=> res?.identifier === item.identifier)
if(findPub){
newlistOfMultiplePublishes.push(findPub)
}
})
const multiplePublish = {
...publishes,
resources: newlistOfMultiplePublishes
};
handlePublish(multiplePublish)
}
export const MultiplePublish = ({
publishes,
isOpen,
onSubmit,
onError,
}: MultiplePublishProps) => {
const theme = useTheme();
const listOfSuccessfulPublishesRef = useRef([]);
const [listOfSuccessfulPublishes, setListOfSuccessfulPublishes] = useState<
any[]
>([]);
const [listOfUnsuccessfulPublishes, setListOfUnSuccessfulPublishes] =
useState<any[]>([]);
const [currentlyInPublish, setCurrentlyInPublish] = useState(null);
const hasStarted = useRef(false);
const publish = useCallback(async (pub: any) => {
const lengthOfResources = pub?.resources?.length;
const lengthOfTimeout = lengthOfResources * 30000;
return await qortalRequestWithTimeout(pub, lengthOfTimeout);
}, []);
const [isPublishing, setIsPublishing] = useState(true);
const handlePublish = useCallback(
async (pub: any) => {
try {
setCurrentlyInPublish(pub?.identifier);
setIsPublishing(true);
const res = await publish(pub);
onSubmit();
setListOfUnSuccessfulPublishes([]);
} catch (error: any) {
const unsuccessfulPublishes = error?.error?.unsuccessfulPublishes || [];
if (error?.error === "User declined request") {
onError();
return;
}
const startPublish = useCallback(
async (pubs: any) => {
await handlePublish(pubs);
},
[handlePublish, onSubmit, listOfSuccessfulPublishes, publishes]
);
if (error?.error === "The request timed out") {
onError("The request timed out");
useEffect(() => {
if (publishes && !hasStarted.current) {
hasStarted.current = true;
startPublish(publishes);
return;
}
}, [startPublish, publishes, listOfSuccessfulPublishes]);
if (unsuccessfulPublishes?.length > 0) {
setListOfUnSuccessfulPublishes(unsuccessfulPublishes);
}
} finally {
setIsPublishing(false);
}
},
[publish]
);
const retry = () => {
let newlistOfMultiplePublishes: any[] = [];
listOfUnsuccessfulPublishes?.forEach(item => {
const findPub = publishes?.resources.find(
(res: any) => res?.identifier === item.identifier
);
if (findPub) {
newlistOfMultiplePublishes.push(findPub);
}
});
const multiplePublish = {
...publishes,
resources: newlistOfMultiplePublishes,
};
handlePublish(multiplePublish);
};
const startPublish = useCallback(
async (pubs: any) => {
await handlePublish(pubs);
},
[handlePublish, onSubmit, listOfSuccessfulPublishes, publishes]
);
return (
<Modal
open={isOpen}
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<ModalBody
sx={{
minHeight: "50vh",
}}
useEffect(() => {
if (publishes && !hasStarted.current) {
hasStarted.current = true;
startPublish(publishes);
}
}, [startPublish, publishes, listOfSuccessfulPublishes]);
return (
<Modal
open={isOpen}
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<ModalBody
sx={{
minHeight: "50vh",
}}
>
{publishes?.resources?.map((publish: any) => {
const unpublished = listOfUnsuccessfulPublishes.map(
item => item?.identifier
);
return (
<Box
sx={{
display: "flex",
gap: "20px",
justifyContent: "space-between",
alignItems: "center",
}}
key={publish?.identifier}
>
{publishes?.resources?.map((publish: any) => {
const unpublished = listOfUnsuccessfulPublishes.map(item => item?.identifier)
return (
<Box
sx={{
display: "flex",
gap: "20px",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography>{publish?.identifier}</Typography>
{!isPublishing && hasStarted.current ? (
<>
{!unpublished.includes(publish.identifier) ? (
<CircleSVG
color={theme.palette.text.primary}
height="24px"
width="24px"
/>
) : (
<EmptyCircleSVG
color={theme.palette.text.primary}
height="24px"
width="24px"
/>
)}
</>
): <CircularProgress size={16} color="secondary"/>}
</Box>
);
})}
{!isPublishing && listOfUnsuccessfulPublishes.length > 0 && (
<>
<Typography sx={{
marginTop: '20px',
fontSize: '16px'
}}>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>
</Modal>
);
<Typography>{publish?.identifier}</Typography>
{!isPublishing && hasStarted.current ? (
<>
{!unpublished.includes(publish.identifier) ? (
<CircleSVG
color={theme.palette.text.primary}
height="24px"
width="24px"
/>
) : (
<EmptyCircleSVG
color={theme.palette.text.primary}
height="24px"
width="24px"
/>
)}
</>
) : (
<CircularProgress size={16} color="secondary" />
)}
</Box>
);
})}
{!isPublishing && listOfUnsuccessfulPublishes.length > 0 && (
<>
<Typography
sx={{
marginTop: "20px",
fontSize: "16px",
}}
>
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>
</Modal>
);
};
export const ModalBody = styled(Box)(({ theme }) => ({
position: "absolute",
backgroundColor: theme.palette.background.default,
borderRadius: "4px",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "75%",
maxWidth: "900px",
padding: "15px 35px",
display: "flex",
flexDirection: "column",
gap: "17px",
overflowY: "auto",
maxHeight: "95vh",
boxShadow:
theme.palette.mode === "dark"
? "0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)"
: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px",
"&::-webkit-scrollbar-track": {
backgroundColor: theme.palette.background.paper,
},
"&::-webkit-scrollbar-track:hover": {
backgroundColor: theme.palette.background.paper,
},
"&::-webkit-scrollbar": {
width: "16px",
height: "10px",
backgroundColor: theme.palette.mode === "light" ? "#f6f8fa" : "#292d3e",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.mode === "light" ? "#d3d9e1" : "#575757",
borderRadius: "8px",
backgroundClip: "content-box",
border: "4px solid transparent",
},
"&::-webkit-scrollbar-thumb:hover": {
backgroundColor: theme.palette.mode === "light" ? "#b7bcc4" : "#474646",
},
}));
position: "absolute",
backgroundColor: theme.palette.background.default,
borderRadius: "4px",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "75%",
maxWidth: "900px",
padding: "15px 35px",
display: "flex",
flexDirection: "column",
gap: "17px",
overflowY: "auto",
maxHeight: "95vh",
boxShadow:
theme.palette.mode === "dark"
? "0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)"
: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px",
"&::-webkit-scrollbar-track": {
backgroundColor: theme.palette.background.paper,
},
"&::-webkit-scrollbar-track:hover": {
backgroundColor: theme.palette.background.paper,
},
"&::-webkit-scrollbar": {
width: "16px",
height: "10px",
backgroundColor: theme.palette.mode === "light" ? "#f6f8fa" : "#292d3e",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.mode === "light" ? "#d3d9e1" : "#575757",
borderRadius: "8px",
backgroundClip: "content-box",
border: "4px solid transparent",
},
"&::-webkit-scrollbar-thumb:hover": {
backgroundColor: theme.palette.mode === "light" ? "#b7bcc4" : "#474646",
},
}));

95
src/components/layout/Navbar/Navbar.tsx

@ -1,5 +1,12 @@
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 { BlockedNamesModal } from "../../common/BlockedNamesModal/BlockedNamesModal";
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 { useDispatch, useSelector } from "react-redux";
import {
addFilteredVideos,
addFilteredFiles,
setEditPlaylist,
setFilterValue,
setIsFiltering,
} from "../../../state/features/videoSlice";
} from "../../../state/features/fileSlice.ts";
import { RootState } from "../../../state/store";
import { useWindowSize } from "../../../hooks/useWindowSize";
import { PublishFile } from "../../PublishFile/PublishFile.tsx";
@ -67,9 +74,7 @@ const NavBar: React.FC<Props> = ({
const [anchorElNotification, setAnchorElNotification] =
React.useState<HTMLButtonElement | null>(null);
const filterValue = useSelector(
(state: RootState) => state.video.filterValue
);
const filterValue = useSelector((state: RootState) => state.file.filterValue);
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
const target = event.currentTarget as unknown as HTMLButtonElement | null;
@ -100,36 +105,42 @@ const NavBar: React.FC<Props> = ({
return (
<CustomAppBar position="sticky" elevation={2}>
<ThemeSelectRow>
<Box sx={{
display: 'flex',
height: '100%',
alignItems: 'center',
gap: '20px'
}}>
<LogoContainer
onClick={() => {
navigate("/");
dispatch(setIsFiltering(false));
dispatch(setFilterValue(""));
dispatch(addFilteredVideos([]));
searchValRef.current = "";
if (!inputRef.current) return;
inputRef.current.value = "";
<Box
sx={{
display: "flex",
height: "100%",
alignItems: "center",
gap: "20px",
}}
>
<img
src={QShareLogo}
style={{
width: "auto",
height: "55px",
padding: "2px",
<LogoContainer
onClick={() => {
navigate("/");
dispatch(setIsFiltering(false));
dispatch(setFilterValue(""));
dispatch(addFilteredFiles([]));
searchValRef.current = "";
if (!inputRef.current) return;
inputRef.current.value = "";
}}
/>
</LogoContainer>
<Typography sx={{
fontSize: '16px',
whiteSpace: 'nowrap'
}}>Sharing is caring</Typography>
>
<img
src={QShareLogo}
style={{
width: "auto",
height: "55px",
padding: "2px",
}}
/>
</LogoContainer>
<Typography
sx={{
fontSize: "16px",
whiteSpace: "nowrap",
}}
>
Sharing is caring
</Typography>
</Box>
</ThemeSelectRow>
<Box
@ -289,15 +300,15 @@ const NavBar: React.FC<Props> = ({
<Input
id="standard-adornment-name"
inputRef={inputRef}
onChange={(e) => {
onChange={e => {
searchValRef.current = e.target.value;
}}
onKeyDown={(event) => {
onKeyDown={event => {
if (event.key === "Enter" || event.keyCode === 13) {
if (!searchValRef.current) {
dispatch(setIsFiltering(false));
dispatch(setFilterValue(""));
dispatch(addFilteredVideos([]));
dispatch(addFilteredFiles([]));
searchValRef.current = "";
if (!inputRef.current) return;
inputRef.current.value = "";
@ -305,7 +316,7 @@ const NavBar: React.FC<Props> = ({
}
navigate("/");
dispatch(setIsFiltering(true));
dispatch(addFilteredVideos([]));
dispatch(addFilteredFiles([]));
dispatch(setFilterValue(searchValRef.current));
}
}}
@ -338,7 +349,7 @@ const NavBar: React.FC<Props> = ({
if (!searchValRef.current) {
dispatch(setIsFiltering(false));
dispatch(setFilterValue(""));
dispatch(addFilteredVideos([]));
dispatch(addFilteredFiles([]));
searchValRef.current = "";
if (!inputRef.current) return;
inputRef.current.value = "";
@ -346,7 +357,7 @@ const NavBar: React.FC<Props> = ({
}
navigate("/");
dispatch(setIsFiltering(true));
dispatch(addFilteredVideos([]));
dispatch(addFilteredFiles([]));
dispatch(setFilterValue(searchValRef.current));
}}
/>
@ -357,7 +368,7 @@ const NavBar: React.FC<Props> = ({
onClick={() => {
dispatch(setIsFiltering(false));
dispatch(setFilterValue(""));
dispatch(addFilteredVideos([]));
dispatch(addFilteredFiles([]));
searchValRef.current = "";
if (!inputRef.current) return;
inputRef.current.value = "";
@ -400,11 +411,9 @@ const NavBar: React.FC<Props> = ({
<AvatarContainer>
{isAuthenticated && userName && (
<>
<PublishFile />
<PublishFile />
</>
)}
</AvatarContainer>
<Popover

221
src/constants/Categories.ts

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

57
src/constants/Categories/1stCategories.ts

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

88
src/constants/Categories/2ndCategories.ts

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

23
src/constants/Categories/3rdCategories.ts

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

91
src/constants/Categories/CategoryFunctions.ts

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

8
src/constants/Identifiers.ts

@ -4,14 +4,10 @@ export const QSHARE_FILE_BASE = useTestIdentifiers
? "MYTEST_share_vid_"
: "qshare_file_";
export const QSHARE_PLAYLIST_BASE = useTestIdentifiers
export const QSHARE_PLAYLIST_BASE = useTestIdentifiers
? "MYTEST_share_playlist_"
: "qshare_playlist_";
export const QSHARE_COMMENT_BASE = useTestIdentifiers
export const QSHARE_COMMENT_BASE = useTestIdentifiers
? "qcomment_v1_MYTEST_"
: "qcomment_v1_qshare_";

4
src/constants/Misc.ts

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

570
src/hooks/useFetchFiles.tsx

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

85
src/pages/FileContent/FileContent-styles.tsx

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

370
src/pages/VideoContent/VideoContent.tsx → src/pages/FileContent/FileContent.tsx

@ -1,72 +1,67 @@
import React, { useState, useMemo, useRef, useEffect } from "react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { setIsLoadingGlobal } from "../../state/features/globalSlice";
import { Avatar, Box, Typography, useTheme } from "@mui/material";
import { VideoPlayer } from "../../components/common/VideoPlayer";
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 DownloadIcon from "@mui/icons-material/Download";
import mockImg from "../../test/mockimg.jpg";
import {
AuthorTextComment,
FileAttachmentContainer,
FileAttachmentFont,
FileDescription,
FilePlayerContainer,
FileTitle,
Spacer,
StyledCardColComment,
StyledCardHeaderComment,
VideoDescription,
VideoPlayerContainer,
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";
} from "./FileContent-styles.tsx";
import { formatDate } from "../../utils/time";
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 { Playlists } from "../../components/Playlists/Playlists";
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
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) {
if (bytes === 0) return '0 Bytes';
if (bytes === 0) return "0 Bytes";
const k = 1024;
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));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
}
export const VideoContent = () => {
export const FileContent = () => {
const { name, id } = useParams();
const [isExpandedDescription, setIsExpandedDescription] =
useState<boolean>(false);
const [descriptionHeight, setDescriptionHeight] =
useState<null | number>(null);
const [descriptionHeight, setDescriptionHeight] = useState<null | number>(
null
);
const [icon, setIcon] = useState<string>("");
const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash
);
const contentRef = useRef(null);
const avatarUrl = useMemo(() => {
let url = "";
@ -79,15 +74,15 @@ export const VideoContent = () => {
const navigate = useNavigate();
const theme = useTheme();
const [videoData, setVideoData] = useState<any>(null);
const [fileData, setFileData] = useState<any>(null);
const [playlistData, setPlaylistData] = useState<any>(null);
const hashMapVideos = useSelector(
(state: RootState) => state.video.hashMapVideos
(state: RootState) => state.file.hashMapFiles
);
const videoReference = useMemo(() => {
if (!videoData) return null;
const { videoReference } = videoData;
if (!fileData) return null;
const { videoReference } = fileData;
if (
videoReference?.identifier &&
videoReference?.name &&
@ -97,13 +92,13 @@ export const VideoContent = () => {
} else {
return null;
}
}, [videoData]);
}, [fileData]);
const videoCover = useMemo(() => {
if (!videoData) return null;
const { videoImage } = videoData;
if (!fileData) return null;
const { videoImage } = fileData;
return videoImage || null;
}, [videoData]);
}, [fileData]);
const dispatch = useDispatch();
const getVideoData = React.useCallback(async (name: string, id: string) => {
@ -147,8 +142,7 @@ export const VideoContent = () => {
...resourceData,
...responseData,
};
setVideoData(combinedData);
setFileData(combinedData);
dispatch(addToHashMap(combinedData));
checkforPlaylist(name, id, combinedData?.code);
}
@ -230,7 +224,7 @@ export const VideoContent = () => {
const existingVideo = hashMapVideos[id];
if (existingVideo) {
setVideoData(existingVideo);
setFileData(existingVideo);
checkforPlaylist(name, id, existingVideo?.code);
} else {
getVideoData(name, id);
@ -272,25 +266,51 @@ export const VideoContent = () => {
useEffect(() => {
if (contentRef.current) {
const height = contentRef.current.offsetHeight;
if (height > 100) { // Assuming 100px is your threshold
setDescriptionHeight(100)
if (height > 100) {
// Assuming 100px is your threshold
setDescriptionHeight(100);
}
}
}, [videoData]);
const categoriesDisplay = useMemo(()=> {
const category = categories?.find((item)=> item?.id === videoData?.category)
if(!category) return null
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])
if (fileData) {
//const icon = getIconsFromObject(fileData)[0]?.icon || null;
const icon = getIconsFromObject(fileData);
setIcon(icon);
}
}, [fileData]);
const categoriesDisplay = useMemo(() => {
if (fileData) {
const categoryList = getCategoriesFromObject(fileData);
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 (
<Box
@ -301,24 +321,42 @@ export const VideoContent = () => {
padding: "20px 10px",
}}
>
<VideoPlayerContainer
<FilePlayerContainer
sx={{
marginBottom: "30px",
}}
>
<Spacer height="15px" />
<VideoTitle
variant="h1"
color="textPrimary"
sx={{
textAlign: "center",
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
}}
>
{videoData?.title}
</VideoTitle>
{videoData?.created && (
{icon ? (
<img
src={icon}
width="50px"
style={{
borderRadius: "5px",
marginRight: "10px",
}}
/>
) : (
<AttachFileIcon />
)}
<FileTitle
variant="h1"
color="textPrimary"
sx={{
textAlign: "center",
}}
>
{fileData?.title}
</FileTitle>
</div>
{fileData?.created && (
<Typography
variant="h6"
sx={{
@ -326,7 +364,7 @@ export const VideoContent = () => {
}}
color={theme.palette.text.primary}
>
{formatDate(videoData.created)}
{formatDate(fileData.created)}
</Typography>
)}
@ -367,11 +405,15 @@ export const VideoContent = () => {
</Box>
<Spacer height="15px" />
<Box>
<Typography sx={{
fontWeight: 'bold',
fontSize: '16px',
userSelect: 'none'
}}>{categoriesDisplay}</Typography>
<Typography
sx={{
fontWeight: "bold",
fontSize: "16px",
userSelect: "none",
}}
>
{categoriesDisplay}
</Typography>
</Box>
<Spacer height="15px" />
<Box
@ -380,10 +422,16 @@ export const VideoContent = () => {
borderRadius: "5px",
padding: "5px",
width: "100%",
cursor: !descriptionHeight ? "default" : isExpandedDescription ? "default" : "pointer",
cursor: !descriptionHeight
? "default"
: isExpandedDescription
? "default"
: "pointer",
position: "relative",
}}
className={!descriptionHeight ? "": isExpandedDescription ? "" : "hover-click"}
className={
!descriptionHeight ? "" : isExpandedDescription ? "" : "hover-click"
}
>
{descriptionHeight && !isExpandedDescription && (
<Box
@ -402,95 +450,101 @@ export const VideoContent = () => {
/>
)}
<Box
ref={contentRef}
ref={contentRef}
sx={{
height: !descriptionHeight ? 'auto' : isExpandedDescription ? "auto" : "100px",
height: !descriptionHeight
? "auto"
: isExpandedDescription
? "auto"
: "100px",
overflow: "hidden",
}}
>
{videoData?.htmlDescription ? (
<DisplayHtml html={videoData?.htmlDescription} />
{fileData?.htmlDescription ? (
<DisplayHtml html={fileData?.htmlDescription} />
) : (
<VideoDescription variant="body1" color="textPrimary" sx={{
cursor: 'default'
}}>
{videoData?.fullDescription}
</VideoDescription>
<FileDescription
variant="body1"
color="textPrimary"
sx={{
cursor: "default",
}}
>
{fileData?.fullDescription}
</FileDescription>
)}
</Box>
{descriptionHeight && (
<Typography
onClick={() => {
setIsExpandedDescription((prev) => !prev);
}}
sx={{
fontWeight: "bold",
fontSize: "16px",
cursor: "pointer",
paddingLeft: "15px",
paddingTop: "15px",
}}
>
{isExpandedDescription ? "Show less" : "...more"}
</Typography>
<Typography
onClick={() => {
setIsExpandedDescription(prev => !prev);
}}
sx={{
fontWeight: "bold",
fontSize: "16px",
cursor: "pointer",
paddingLeft: "15px",
paddingTop: "15px",
}}
>
{isExpandedDescription ? "Show less" : "...more"}
</Typography>
)}
</Box>
<Box sx={{
width: '100%',
display: 'flex',
alignItems: 'flex-start',
flexDirection: 'column',
gap: '25px',
marginTop: '25px'
}}>
{videoData?.files?.map((file)=> {
<Box
sx={{
width: "100%",
display: "flex",
alignItems: "flex-start",
flexDirection: "column",
gap: "25px",
marginTop: "25px",
}}
>
{fileData?.files?.map((file, index) => {
return (
<FileAttachmentContainer sx={{
width: '100%',
display: 'flex',
justifyContent: 'space-between'
}}>
<FileAttachmentFont>
{file.filename}
</FileAttachmentFont>
<Box sx={{
display: 'flex',
gap: '25px',
alignItems: 'center',
}}>
<FileAttachmentFont>
{formatBytes(file?.size || 0)}
</FileAttachmentFont>
<FileElement
fileInfo={{...file,
filename: file?.filename,
mimeType: file?.mimetype
}}
jsonId={id}
title={file?.filename}
customStyles={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<DownloadIcon />
</FileElement>
</Box>
</FileAttachmentContainer>
)
<FileAttachmentContainer
sx={{
width: "100%",
display: "flex",
justifyContent: "space-between",
}}
key={file.toString() + index}
>
<FileAttachmentFont>{file.filename}</FileAttachmentFont>
<Box
sx={{
display: "flex",
gap: "25px",
alignItems: "center",
}}
>
<FileAttachmentFont>
{formatBytes(file?.size || 0)}
</FileAttachmentFont>
<FileElement
fileInfo={{
...file,
filename: file?.filename,
mimeType: file?.mimetype,
}}
jsonId={id}
title={file?.filename}
customStyles={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<DownloadIcon />
</FileElement>
</Box>
</FileAttachmentContainer>
);
})}
</Box>
</VideoPlayerContainer>
</Box>
</FilePlayerContainer>
<Box
sx={{
display: "flex",

126
src/pages/Home/Channels.tsx

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

149
src/pages/Home/FileList-styles.tsx

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

944
src/pages/Home/FileList.tsx

@ -1,326 +1,44 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
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 { Avatar, Box, Skeleton, Tooltip } from "@mui/material";
import {
BlockIconContainer,
BottomParent,
FilterSelect,
FiltersCheckbox,
FiltersCol,
FiltersContainer,
FiltersRow,
FiltersSubContainer,
FiltersTitle,
IconsBox,
NameContainer,
VideoCard,
VideoCardName,
VideoCardTitle,
VideoContainer,
FileContainer,
VideoUploadDate,
} from "./FileList-styles.tsx";
import ResponsiveImage from "../../components/ResponsiveImage";
import { formatDate, formatTimestampSeconds } from "../../utils/time";
import { Subtitle, SubtitleContainer } from "./Home-styles";
import { ExpandMoreSVG } from "../../assets/svgs/ExpandMoreSVG";
import EditIcon from "@mui/icons-material/Edit";
import {
addVideos,
blockUser,
changeFilterType,
changeSelectedCategoryVideos,
changeSelectedSubCategoryVideos,
changeSelectedSubCategoryVideos2,
changeSelectedSubCategoryVideos3,
changefilterName,
changefilterSearch,
clearVideoList,
setEditPlaylist,
setEditVideo,
} from "../../state/features/videoSlice";
import { Playlists } from "../../components/Playlists/Playlists";
import { PlaylistSVG } from "../../assets/svgs/PlaylistSVG";
setEditFile,
Video,
} from "../../state/features/fileSlice.ts";
import BlockIcon from "@mui/icons-material/Block";
import EditIcon from '@mui/icons-material/Edit';
import { formatBytes } from "../VideoContent/VideoContent";
import {categories, icons, subCategories, subCategories2, subCategories3} from "../../constants/Categories.ts";
import AttachFileIcon from "@mui/icons-material/AttachFile";
import { formatBytes } from "../FileContent/FileContent.tsx";
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 {
mode?: string;
interface FileListProps {
files: Video[];
}
export const FileList = ({ mode }: VideoListProps) => {
const theme = useTheme();
const prevVal = useRef("");
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 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
export const FileList = ({ files }: FileListProps) => {
const hashMapFiles = useSelector(
(state: RootState) => state.file.hashMapFiles
);
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 [showIcons, setShowIcons] = useState(null);
const username = useSelector((state: RootState) => state.auth?.user?.name);
const isFilterMode = useRef(false);
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 dispatch = useDispatch();
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) => {
if (user === "Q-Share") return;
@ -332,520 +50,158 @@ export const FileList = ({ mode }: VideoListProps) => {
});
if (response === true) {
dispatch(blockUser(user))
dispatch(blockUser(user));
}
} catch (error) {}
};
return (
<Grid container sx={{ width: "100%" }}>
<FiltersCol item xs={12} md={2} sm={3}>
<FiltersContainer>
<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);
}}
<FileContainer>
{files.map((file: any, index: number) => {
const existingFile = hashMapFiles[file?.id];
let hasHash = false;
let fileObj = file;
if (existingFile) {
fileObj = existingFile;
hasHash = true;
}
const icon = getIconsFromObject(fileObj);
return (
<Box
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",
display: "flex",
alignItems: "center",
width: "100%",
maxWidth: "1400px",
height: "75px",
position: "relative",
}}
key={fileObj.id}
onMouseEnter={() => setShowIcons(fileObj.id)}
onMouseLeave={() => setShowIcons(null)}
>
</SubtitleContainer>
<VideoContainer>
{videos.map((video: any, index: number) => {
const existingVideo = hashMapVideos[video?.id];
let hasHash = false;
let videoObj = video;
if (existingVideo) {
videoObj = existingVideo;
hasHash = true;
}
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 (
<Box
{hasHash ? (
<>
<IconsBox
sx={{
display: "flex",
alignItems: "center",
width: "100%",
height: "75px",
position:"relative"
opacity: showIcons === fileObj.id ? 1 : 0,
zIndex: 2,
}}
key={videoObj.id}
onMouseEnter={() => setShowIcons(videoObj.id)}
onMouseLeave={() => setShowIcons(null)}
>
{hasHash ? (
<>
<IconsBox
sx={{
opacity: showIcons === videoObj.id ? 1 : 0,
zIndex: 2,
}}
>
{videoObj?.user === username && (
<Tooltip title="Edit video properties" placement="top">
<BlockIconContainer>
<EditIcon
onClick={() => {
dispatch(setEditVideo(videoObj));
}}
/>
</BlockIconContainer>
</Tooltip>
)}
<Tooltip title="Block user content" placement="top">
{fileObj?.user === username && (
<Tooltip title="Edit video properties" placement="top">
<BlockIconContainer>
<BlockIcon
<EditIcon
onClick={() => {
blockUserFunc(videoObj?.user);
dispatch(setEditFile(fileObj));
}}
/>
</BlockIconContainer>
</Tooltip>
</IconsBox>
<VideoCard
onClick={() => {
navigate(`/share/${videoObj?.user}/${videoObj?.id}`);
}}
)}
<Tooltip title="Block user content" placement="top">
<BlockIconContainer>
<BlockIcon
onClick={() => {
blockUserFunc(fileObj?.user);
}}
/>
</BlockIconContainer>
</Tooltip>
</IconsBox>
<VideoCard
onClick={() => {
navigate(`/share/${fileObj?.user}/${fileObj?.id}`);
}}
sx={{
height: "100%",
width: "100%",
display: "flex",
gap: "25px",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<Box
sx={{
height: '100%',
width: '100%',
display: 'flex',
gap: '25px',
flexDirection: 'row',
justifyContent: 'space-between'
display: "flex",
gap: "25px",
alignItems: "center",
}}
>
<Box sx={{
display: 'flex',
gap: '25px',
alignItems: 'center'
}}>
{icon ? <img src={icon} width="50px" style={{
borderRadius: '5px'
}}/> : (
<AttachFileIcon />
{icon ? (
<img
src={icon}
width="50px"
style={{
borderRadius: "5px",
}}
/>
) : (
<AttachFileIcon />
)}
<VideoCardTitle
sx={{
width: "100px",
}}
>
{formatBytes(
fileObj?.files.reduce(
(acc, cur) => acc + (cur?.size || 0),
0
)
)}
<VideoCardTitle sx={{
width: '100px'
}}>
{formatBytes(videoObj?.files.reduce((acc, cur) => acc + (cur?.size || 0), 0))}
</VideoCardTitle>
<VideoCardTitle>{videoObj.title}</VideoCardTitle>
</Box>
<BottomParent>
<NameContainer
onClick={(e) => {
e.stopPropagation();
navigate(`/channel/${videoObj?.user}`);
</VideoCardTitle>
<VideoCardTitle>{fileObj.title}</VideoCardTitle>
</Box>
<BottomParent>
<NameContainer
onClick={e => {
e.stopPropagation();
navigate(`/channel/${fileObj?.user}`);
}}
>
<Avatar
sx={{ height: 24, width: 24 }}
src={`/arbitrary/THUMBNAIL/${fileObj?.user}/qortal_avatar`}
alt={`${fileObj?.user}'s avatar`}
/>
<VideoCardName
sx={{
":hover": {
textDecoration: "underline",
},
}}
>
<Avatar
sx={{ height: 24, width: 24 }}
src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`}
alt={`${videoObj?.user}'s avatar`}
/>
<VideoCardName
sx={{
":hover": {
textDecoration: "underline",
},
}}
>
{videoObj?.user}
</VideoCardName>
</NameContainer>
{videoObj?.created && (
<VideoUploadDate>
{formatDate(videoObj.created)}
</VideoUploadDate>
)}
</BottomParent>
</VideoCard>
</>
) : (
<Skeleton
variant="rectangular"
style={{
width: "100%",
height: "100%",
paddingBottom: "10px",
objectFit: "contain",
visibility: "visible",
borderRadius: "8px",
}}
/>
)}
</Box>
);
})}
</VideoContainer>
<LazyLoad
onLoadMore={getFilesHandler}
isLoading={isLoading}
></LazyLoad>
</Box>
</Grid>
</Grid>
{fileObj?.user}
</VideoCardName>
</NameContainer>
{fileObj?.created && (
<VideoUploadDate>
{formatDate(fileObj.created)}
</VideoUploadDate>
)}
</BottomParent>
</VideoCard>
</>
) : (
<Skeleton
variant="rectangular"
style={{
width: "100%",
height: "100%",
paddingBottom: "10px",
objectFit: "contain",
visibility: "visible",
borderRadius: "8px",
}}
/>
)}
</Box>
);
})}
</FileContainer>
);
};

374
src/pages/Home/FileListComponentLevel.tsx

@ -1,8 +1,8 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { RootState } from '../../state/store'
import AttachFileIcon from '@mui/icons-material/AttachFile';
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { RootState } from "../../state/store";
import AttachFileIcon from "@mui/icons-material/AttachFile";
import {
Avatar,
@ -10,62 +10,72 @@ import {
Button,
Skeleton,
Typography,
useTheme
} from '@mui/material'
import { useFetchFiles } from '../../hooks/useFetchFiles.tsx'
import LazyLoad from '../../components/common/LazyLoad'
import { BottomParent, NameContainer, VideoCard, VideoCardName, VideoCardTitle, VideoContainer, VideoUploadDate } from './FileList-styles.tsx'
import ResponsiveImage from '../../components/ResponsiveImage'
import { formatDate, formatTimestampSeconds } from '../../utils/time'
import { Video } from '../../state/features/videoSlice'
import { queue } from '../../wrappers/GlobalWrapper'
import { QSHARE_FILE_BASE } from '../../constants/Identifiers.ts'
import { formatBytes } from '../VideoContent/VideoContent'
import {categories, icons, subCategories, subCategories2, subCategories3} from "../../constants/Categories.ts";
useTheme,
} from "@mui/material";
import { useFetchFiles } from "../../hooks/useFetchFiles.tsx";
import LazyLoad from "../../components/common/LazyLoad";
import {
BottomParent,
NameContainer,
VideoCard,
VideoCardName,
VideoCardTitle,
FileContainer,
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 {
mode?: string
mode?: string;
}
export const FileListComponentLevel = ({ mode }: VideoListProps) => {
const { name: paramName } = useParams()
const theme = useTheme()
const [isLoading, setIsLoading] = useState<boolean>(true)
const { name: paramName } = useParams();
const theme = useTheme();
const [isLoading, setIsLoading] = useState<boolean>(true);
const firstFetch = useRef(false)
const afterFetch = useRef(false)
const firstFetch = useRef(false);
const afterFetch = 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, setVideos] = React.useState<Video[]>([])
const navigate = useNavigate()
const {
getVideo,
getNewFiles,
checkNewFiles,
checkAndUpdateVideo
} = useFetchFiles()
(state: RootState) => state.file.hashMapFiles
);
const [videos, setVideos] = React.useState<Video[]>([]);
const navigate = useNavigate();
const { getFile, getNewFiles, checkNewFiles, checkAndUpdateFile } =
useFetchFiles();
const getVideos = React.useCallback(async () => {
try {
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 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 response = await fetch(url, {
method: 'GET',
method: "GET",
headers: {
'Content-Type': 'application/json'
}
})
const responseData = await response.json()
"Content-Type": "application/json",
},
});
const responseData = await response.json();
const structureData = responseData.map((video: any): Video => {
return {
title: video?.metadata?.title,
@ -76,161 +86,144 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
created: video?.created,
updated: video?.updated,
user: video.name,
videoImage: '',
id: video.identifier
}
})
const copiedVideos: Video[] = [...videos]
videoImage: "",
id: video.identifier,
};
});
const copiedVideos: Video[] = [...videos];
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) {
copiedVideos[index] = video
copiedVideos[index] = video;
} else {
copiedVideos.push(video)
copiedVideos.push(video);
}
})
setVideos(copiedVideos)
});
setVideos(copiedVideos);
for (const content of structureData) {
if (content.user && content.id) {
const res = checkAndUpdateVideo(content)
const res = checkAndUpdateFile(content);
if (res) {
queue.push(() => getVideo(content.user, content.id, content));
queue.push(() => getFile(content.user, content.id, content));
}
}
}
} catch (error) {
} finally {
}
}, [videos, hashMapVideos])
}, [videos, hashMapVideos]);
const getVideosHandler = React.useCallback(async () => {
if(!firstFetch.current || !afterFetch.current) return
await getVideos()
}, [getVideos])
if (!firstFetch.current || !afterFetch.current) return;
await getVideos();
}, [getVideos]);
const getVideosHandlerMount = React.useCallback(async () => {
if(firstFetch.current) return
firstFetch.current = true
await getVideos()
afterFetch.current = true
setIsLoading(false)
}, [getVideos])
if (firstFetch.current) return;
firstFetch.current = true;
await getVideos();
afterFetch.current = true;
setIsLoading(false);
}, [getVideos]);
useEffect(()=> {
if(!firstFetch.current){
getVideosHandlerMount()
useEffect(() => {
if (!firstFetch.current) {
getVideosHandlerMount();
}
}, [getVideosHandlerMount]);
}, [getVideosHandlerMount ])
return (
<Box sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}>
<VideoContainer>
{videos.map((video: any, index: number) => {
const existingVideo = hashMapVideos[video?.id];
let hasHash = false;
let videoObj = video;
if (existingVideo) {
videoObj = existingVideo;
hasHash = true;
}
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<FileContainer>
{videos.map((file: any, index: number) => {
const existingFile = hashMapVideos[file?.id];
let hasHash = false;
let fileObj = file;
if (existingFile) {
fileObj = existingFile;
hasHash = true;
}
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;
const icon = getIconsFromObject(fileObj);
return (
<Box
sx={{
display: "flex",
alignItems: "center",
width: "100%",
height: "75px",
position:"relative"
}}
key={videoObj.id}
>
{hasHash ? (
<>
return (
<Box
sx={{
display: "flex",
alignItems: "center",
width: "100%",
height: "75px",
position: "relative",
}}
key={fileObj.id}
>
{hasHash ? (
<>
<VideoCard
onClick={() => {
navigate(`/share/${videoObj?.user}/${videoObj?.id}`);
navigate(`/share/${fileObj?.user}/${fileObj?.id}`);
}}
sx={{
height: '100%',
width: '100%',
display: 'flex',
gap: '25px',
flexDirection: 'row',
justifyContent: 'space-between'
height: "100%",
width: "100%",
display: "flex",
gap: "25px",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<Box sx={{
display: 'flex',
gap: '25px',
alignItems: 'center'
}}>
{icon ? <img src={icon} width="50px" style={{
borderRadius: '5px'
}}/> : (
<AttachFileIcon />
<Box
sx={{
display: "flex",
gap: "25px",
alignItems: "center",
}}
>
{icon ? (
<img
src={icon}
width="50px"
style={{
borderRadius: "5px",
}}
/>
) : (
<AttachFileIcon />
)}
<VideoCardTitle sx={{
width: '100px'
}}>
{formatBytes(videoObj?.files.reduce((acc, cur) => acc + (cur?.size || 0), 0))}
</VideoCardTitle>
<VideoCardTitle>{videoObj.title}</VideoCardTitle>
<VideoCardTitle
sx={{
width: "100px",
}}
>
{formatBytes(
fileObj?.files.reduce(
(acc, cur) => acc + (cur?.size || 0),
0
)
)}
</VideoCardTitle>
<VideoCardTitle>{fileObj.title}</VideoCardTitle>
</Box>
<BottomParent>
<NameContainer
onClick={(e) => {
onClick={e => {
e.stopPropagation();
navigate(`/channel/${videoObj?.user}`);
navigate(`/channel/${fileObj?.user}`);
}}
>
<Avatar
sx={{ height: 24, width: 24 }}
src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`}
alt={`${videoObj?.user}'s avatar`}
src={`/arbitrary/THUMBNAIL/${fileObj?.user}/qortal_avatar`}
alt={`${fileObj?.user}'s avatar`}
/>
<VideoCardName
sx={{
@ -239,39 +232,36 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
},
}}
>
{videoObj?.user}
{fileObj?.user}
</VideoCardName>
</NameContainer>
{videoObj?.created && (
{fileObj?.created && (
<VideoUploadDate>
{formatDate(videoObj.created)}
{formatDate(fileObj.created)}
</VideoUploadDate>
)}
</BottomParent>
</VideoCard>
</>
) : (
<Skeleton
variant="rectangular"
style={{
width: "100%",
height: "100%",
paddingBottom: "10px",
objectFit: "contain",
visibility: "visible",
borderRadius: "8px",
}}
/>
)}
</Box>
);
})}
</VideoContainer>
</>
) : (
<Skeleton
variant="rectangular"
style={{
width: "100%",
height: "100%",
paddingBottom: "10px",
objectFit: "contain",
visibility: "visible",
borderRadius: "8px",
}}
/>
)}
</Box>
);
})}
</FileContainer>
<LazyLoad onLoadMore={getVideosHandler} isLoading={isLoading}></LazyLoad>
</Box>
)
}
);
};

330
src/pages/Home/Home.tsx

@ -1,15 +1,323 @@
import React from 'react'
import { FileList } from './FileList.tsx'
import React, { useEffect, useRef, useState } from "react";
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'
import { RootState } from '../../state/store'
interface HomeProps {
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
);
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]);
export const Home = () => {
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 (
<>
<FileList />
</>
)
}
<Grid container sx={{ width: "100%" }}>
<FiltersCol item xs={12} md={2} sm={3}>
<FiltersContainer>
<StatsData />
<Input
id="standard-adornment-name"
onChange={e => {
setFilterSearch(e.target.value);
}}
onKeyDown={searchOnEnter}
value={filterSearch}
placeholder="Search"
sx={{
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>
);
};

71
src/pages/IndividualProfile/IndividualProfile.tsx

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

81
src/pages/VideoContent/VideoContent-styles.tsx

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

188
src/state/features/fileSlice.ts

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

69
src/state/features/globalSlice.ts

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

216
src/state/features/videoSlice.ts

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

24
src/state/store.ts

@ -1,27 +1,27 @@
import { configureStore } from '@reduxjs/toolkit'
import notificationsReducer from './features/notificationsSlice'
import authReducer from './features/authSlice'
import globalReducer from './features/globalSlice'
import videoReducer from './features/videoSlice'
import { configureStore } from "@reduxjs/toolkit";
import notificationsReducer from "./features/notificationsSlice";
import authReducer from "./features/authSlice";
import globalReducer from "./features/globalSlice";
import fileReducer from "./features/fileSlice.ts";
export const store = configureStore({
reducer: {
notifications: notificationsReducer,
auth: authReducer,
global: globalReducer,
video: videoReducer,
file: fileReducer,
},
middleware: (getDefaultMiddleware) =>
middleware: 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.
// 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.
// 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;

20
src/utils/utilFunctions.ts

@ -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);
};
Loading…
Cancel
Save