Browse Source

Merge pull request #2 from QortalSeth/main

Massive Category Overhaul
main
Qortal Dev 6 months ago committed by GitHub
parent
commit
a3bce74ad8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  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. 377
      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. 369
      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>
);
};

377
src/components/PublishFile/PublishFile.tsx

@ -1,66 +1,32 @@
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,
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 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 { 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 +70,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 +87,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 +118,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 +152,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 +191,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 +217,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 +231,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 +254,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 +283,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 +314,7 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
justifyContent: "space-between",
}}
>
<NewCrowdfundTitle>Share</NewCrowdfundTitle>
<NewCrowdfundTitle>Share</NewCrowdfundTitle>
</Box>
{step === "videos" && (
@ -449,7 +347,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 +361,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 +399,12 @@ export const PublishFile = ({ editId, editContent }: NewCrowdfundProps) => {
</Typography>
<TextEditor
inlineContent={description}
setInlineContent={(value) => {
setInlineContent={value => {
setDescription(value);
}}
/>
</>
)}
</>
)}
<CrowdfundActionButtonRow>
@ -664,16 +440,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 +462,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>
);
};

369
src/pages/Home/FileListComponentLevel.tsx

@ -1,71 +1,60 @@
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, { 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, Box, Skeleton, useTheme } from "@mui/material";
import { useFetchFiles } from "../../hooks/useFetchFiles.tsx";
import LazyLoad from "../../components/common/LazyLoad";
import {
Avatar,
Box,
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";
BottomParent,
FileContainer,
NameContainer,
VideoCard,
VideoCardName,
VideoCardTitle,
VideoUploadDate,
} from "./FileList-styles.tsx";
import { formatDate } 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 { 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 +65,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])
useEffect(()=> {
if(!firstFetch.current){
getVideosHandlerMount()
if (firstFetch.current) return;
firstFetch.current = true;
await getVideos();
afterFetch.current = true;
setIsLoading(false);
}, [getVideos]);
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;
}
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;
<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;
}
return (
<Box
sx={{
display: "flex",
alignItems: "center",
width: "100%",
height: "75px",
position:"relative"
}}
key={videoObj.id}
>
{hasHash ? (
<>
const icon = getIconsFromObject(fileObj);
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 +211,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