Files
q-support/src/components/EditFile/EditFile.tsx
QortalSeth 350d2ff052 Index.ts refactored into Identifiers.ts and Categories.ts for more clarity
Some references to Q-Tube in code replaced with Q-Share

Characters allowed in Publish Titles added to constants/Misc.ts, more characters are allowed than before

User Search Label now says "User's Exact Name" to communicate that users must be precise about what names they search for

Search and Name Search now can perform searches by hitting enter instead of clicking on Search Button

Phil's MultiplePublishAll.tsx component used

Added "Other" main Category

Categories are sorted by name, "Other" is always last
2024-01-15 14:53:32 -07:00

764 lines
25 KiB
TypeScript

import React, {useEffect, useState} from "react";
import {
CrowdfundActionButton,
CrowdfundActionButtonRow,
CustomInputField,
ModalBody,
NewCrowdfundTitle,
} from "./Upload-styles";
import {
Box,
FormControl,
InputLabel,
MenuItem,
Modal,
OutlinedInput,
Select,
SelectChangeEvent,
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";
const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 });
interface NewCrowdfundProps {
editId?: string;
editContent?: null | {
title: string;
user: string;
coverImage: string | null;
};
}
interface VideoFile {
file: File;
title: string;
description: string;
coverImage?: string;
identifier?:string;
filename?:string
}
export const EditFile = () => {
const theme = useTheme();
const dispatch = useDispatch();
const username = useSelector((state: RootState) => state.auth?.user?.name);
const userAddress = useSelector(
(state: RootState) => state.auth?.user?.address
);
const editVideoProperties = useSelector(
(state: RootState) => state.video.editVideoProperties
);
const [publishes, setPublishes] = useState<any>(null);
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
const [videoPropertiesToSetToRedux, setVideoPropertiesToSetToRedux] =
useState(null);
const [title, setTitle] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [coverImage, setCoverImage] = useState<string>("");
const [file, setFile] = useState(null);
const [files, setFiles] = useState<VideoFile[]>([]);
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]);
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);
}
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);
}
}
}, [editVideoProperties]);
const onClose = () => {
dispatch(setEditVideo(null));
setVideoPropertiesToSetToRedux(null);
setFile(null);
setTitle("");
setDescription("");
setCoverImage("");
};
async function publishQDNResource() {
try {
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 (!userAddress) throw new Error("Unable to locate user address");
if(files.length === 0) throw new Error("Add at least one file");
let errorMsg = "";
let name = "";
if (username) {
name = username;
}
if (!name) {
errorMsg =
"Cannot publish without access to your name. Please authenticate.";
}
if (editVideoProperties?.user !== username) {
errorMsg = "Cannot publish another user's resource";
}
if (errorMsg) {
dispatch(
setNotification({
msg: errorMsg,
alertType: "error",
})
);
return;
}
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();
for (const publish of files) {
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 filename = firstPartName.slice(0, 15);
// Step 1: Replace all white spaces with underscores
// Replace all forms of whitespace (including non-standard ones) with underscores
let stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, "_");
// Remove all non-alphanumeric characters (except underscores)
let alphanumericString = stringWithUnderscores.replace(
/[^a-zA-Z0-9_]/g,
""
);
if(fileExtension){
filename = `${alphanumericString.trim()}.${fileExtension}`
} else {
filename = alphanumericString
}
let metadescription =
`**cat:${category};sub:${subcategory};sub2:${subcategory2};sub3:${subcategory3}**` +
fullDescription.slice(0, 150);
const requestBodyVideo: any = {
action: "PUBLISH_QDN_RESOURCE",
name: name,
service: "FILE",
file,
title: title.slice(0, 50),
description: metadescription,
identifier,
filename,
tag1: QSHARE_FILE_BASE,
};
listOfPublishes.push(requestBodyVideo);
fileReferences.push({
filename: file.name,
identifier,
name,
service: 'FILE',
mimetype: file.type,
size: file.size
})
}
const fileObject: any = {
title,
version: editVideoProperties.version,
fullDescription,
htmlDescription: description,
commentsId: editVideoProperties.commentsId,
category,
subcategory,
subcategory2,
subcategory3,
files: fileReferences
};
let metadescription =
`**cat:${category};sub:${subcategory};sub2:${subcategory2}**` +
fullDescription.slice(0, 150);
const crowdfundObjectToBase64 = await objectToBase64(fileObject);
// Description is obtained from raw data
const requestBodyJson: any = {
action: "PUBLISH_QDN_RESOURCE",
name: name,
service: "DOCUMENT",
data64: crowdfundObjectToBase64,
title: title.slice(0, 50),
description: metadescription,
identifier: editVideoProperties.id,
tag1: QSHARE_FILE_BASE,
filename: `video_metadata.json`,
};
listOfPublishes.push(requestBodyJson);
const multiplePublish = {
action: "PUBLISH_MULTIPLE_QDN_RESOURCES",
resources: [...listOfPublishes],
};
setPublishes(multiplePublish);
setIsOpenMultiplePublish(true);
setVideoPropertiesToSetToRedux({
...editVideoProperties,
...fileObject,
});
} catch (error: any) {
let notificationObj: any = null;
if (typeof error === "string") {
notificationObj = {
msg: error || "Failed to publish update",
alertType: "error",
};
} else if (typeof error?.error === "string") {
notificationObj = {
msg: error?.error || "Failed to publish update",
alertType: "error",
};
} else {
notificationObj = {
msg: error?.message || "Failed to publish update",
alertType: "error",
};
}
if (!notificationObj) return;
dispatch(setNotification(notificationObj));
throw new Error("Failed to publish update");
}
}
const handleOnchange = (index: number, type: string, value: string) => {
// setFiles((prev) => {
// let formattedValue = value
// console.log({type})
// if(type === 'title'){
// formattedValue = value.replace(/[^a-zA-Z0-9\s]/g, "")
// }
// const copyFiles = [...prev];
// copyFiles[index] = {
// ...copyFiles[index],
// [type]: formattedValue,
// };
// return copyFiles;
// });
};
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}
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<ModalBody>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<NewCrowdfundTitle>Update share</NewCrowdfundTitle>
</Box>
<>
<Box
{...getRootProps()}
sx={{
border: "1px dashed gray",
padding: 2,
textAlign: "center",
marginBottom: 2,
cursor: "pointer",
}}
>
<input {...getInputProps()} />
<Typography>Click to add more files</Typography>
</Box>
{files.map((file, index) => {
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={{
cursor: "pointer",
}}
/>
</Box>
</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 && (
<>
<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>
<CrowdfundActionButton
onClick={() => {
onClose();
}}
variant="contained"
color="error"
>
Cancel
</CrowdfundActionButton>
<Box
sx={{
display: "flex",
gap: "20px",
alignItems: "center",
}}
>
<CrowdfundActionButton
variant="contained"
onClick={() => {
publishQDNResource();
}}
>
Publish
</CrowdfundActionButton>
</Box>
</CrowdfundActionButtonRow>
</ModalBody>
</Modal>
{isOpenMultiplePublish && (
<MultiplePublish
isOpen={isOpenMultiplePublish}
onError={(messageNotification)=> {
setIsOpenMultiplePublish(false);
setPublishes(null)
if(messageNotification){
dispatch(
setNotification({
msg: messageNotification,
alertType: 'error'
})
)
}
}}
onSubmit={() => {
setIsOpenMultiplePublish(false);
const clonedCopy = structuredClone(videoPropertiesToSetToRedux);
dispatch(updateVideo(clonedCopy));
dispatch(updateInHashMap(clonedCopy));
dispatch(
setNotification({
msg: "File updated",
alertType: "success",
})
);
onClose();
}}
publishes={publishes}
/>
)}
</>
);
};