mirror of
https://github.com/Qortal/q-support.git
synced 2025-02-11 17:55:50 +00:00
commit
6bbe68a3fa
1
.gitignore
vendored
1
.gitignore
vendored
@ -13,6 +13,7 @@ dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
src/assets/icons/*
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import { CssBaseline } from "@mui/material";
|
||||
@ -11,12 +11,16 @@ import { Home } from "./pages/Home/Home";
|
||||
import { IssueContent } from "./pages/IssueContent/IssueContent.tsx";
|
||||
import DownloadWrapper from "./wrappers/DownloadWrapper";
|
||||
import { IndividualProfile } from "./pages/IndividualProfile/IndividualProfile";
|
||||
import { fetchFeesRedux } from "./constants/PublishFees/FeePricePublish/FeePricePublish.ts";
|
||||
|
||||
function App() {
|
||||
// const themeColor = window._qdnTheme
|
||||
|
||||
const [theme, setTheme] = useState("dark");
|
||||
|
||||
useEffect(() => {
|
||||
fetchFeesRedux();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<ThemeProvider theme={theme === "light" ? lightTheme : darkTheme}>
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
Before Width: | Height: | Size: 132 KiB |
Binary file not shown.
Before Width: | Height: | Size: 7.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
BIN
src/assets/img/Q-SupportIcon(AlphaX).webp
Normal file
BIN
src/assets/img/Q-SupportIcon(AlphaX).webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 73 KiB |
Binary file not shown.
Before Width: | Height: | Size: 39 KiB |
@ -25,8 +25,8 @@ import { QSUPPORT_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 { allCategoryData } from "../../constants/Categories/Categories.ts";
|
||||
import { log, titleFormatter } from "../../constants/Misc.ts";
|
||||
import {
|
||||
CategoryList,
|
||||
CategoryListRef,
|
||||
@ -37,6 +37,13 @@ import {
|
||||
ImagePublisherRef,
|
||||
} from "../common/ImagePublisher/ImagePublisher.tsx";
|
||||
import { ThemeButtonBright } from "../../pages/Home/Home-styles.tsx";
|
||||
import {
|
||||
AutocompleteQappNames,
|
||||
QappNamesRef,
|
||||
} from "../common/AutocompleteQappNames.tsx";
|
||||
import { payPublishFeeQORT } from "../../constants/PublishFees/SendFeeFunctions.ts";
|
||||
import { feeAmountBase } from "../../constants/PublishFees/FeeData.tsx";
|
||||
import { verifyPayment } from "../../constants/PublishFees/VerifyPayment.ts";
|
||||
|
||||
const uid = new ShortUniqueId();
|
||||
const shortuid = new ShortUniqueId({ length: 5 });
|
||||
@ -58,6 +65,7 @@ interface VideoFile {
|
||||
identifier?: string;
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
export const EditIssue = () => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
@ -65,9 +73,13 @@ export const EditIssue = () => {
|
||||
const userAddress = useSelector(
|
||||
(state: RootState) => state.auth?.user?.address
|
||||
);
|
||||
const editFileProperties = useSelector(
|
||||
const editIssueProperties = useSelector(
|
||||
(state: RootState) => state.file.editFileProperties
|
||||
);
|
||||
const QappNames = useSelector(
|
||||
(state: RootState) => state.file.publishedQappNames
|
||||
);
|
||||
|
||||
const [publishes, setPublishes] = useState<any>(null);
|
||||
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
|
||||
const [videoPropertiesToSetToRedux, setVideoPropertiesToSetToRedux] =
|
||||
@ -78,9 +90,11 @@ export const EditIssue = () => {
|
||||
const [coverImage, setCoverImage] = useState<string>("");
|
||||
const [file, setFile] = useState(null);
|
||||
const [files, setFiles] = useState<VideoFile[]>([]);
|
||||
const [editCategories, setEditCategories] = useState<string[]>([]);
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||
const [isIssuePaid, setIsIssuePaid] = useState<boolean>(true);
|
||||
const categoryListRef = useRef<CategoryListRef>(null);
|
||||
const imagePublisherRef = useRef<ImagePublisherRef>(null);
|
||||
const autocompleteRef = useRef<QappNamesRef>(null);
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
maxFiles: 10,
|
||||
@ -118,21 +132,25 @@ export const EditIssue = () => {
|
||||
});
|
||||
|
||||
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>`;
|
||||
if (editIssueProperties) {
|
||||
setTitle(editIssueProperties?.title || "");
|
||||
setFiles(editIssueProperties?.files || []);
|
||||
if (editIssueProperties?.htmlDescription) {
|
||||
setDescription(editIssueProperties?.htmlDescription);
|
||||
} else if (editIssueProperties?.fullDescription) {
|
||||
const paragraph = `<p>${editIssueProperties?.fullDescription}</p>`;
|
||||
setDescription(paragraph);
|
||||
}
|
||||
|
||||
verifyPayment(editIssueProperties).then(isIssuePaid => {
|
||||
setIsIssuePaid(isIssuePaid);
|
||||
});
|
||||
const categoriesFromEditFile =
|
||||
getCategoriesFromObject(editFileProperties);
|
||||
setEditCategories(categoriesFromEditFile);
|
||||
getCategoriesFromObject(editIssueProperties);
|
||||
setSelectedCategories(categoriesFromEditFile);
|
||||
}
|
||||
}, [editFileProperties]);
|
||||
}, [editIssueProperties]);
|
||||
|
||||
const onClose = () => {
|
||||
dispatch(setEditFile(null));
|
||||
setVideoPropertiesToSetToRedux(null);
|
||||
@ -142,14 +160,22 @@ export const EditIssue = () => {
|
||||
setCoverImage("");
|
||||
};
|
||||
|
||||
async function publishQDNResource() {
|
||||
async function publishQDNResource(payFee: boolean) {
|
||||
try {
|
||||
const categoryList = categoryListRef.current?.getSelectedCategories();
|
||||
if (!description) throw new Error("Please enter a description");
|
||||
if (!categoryList[0]) throw new Error("Please select a category");
|
||||
if (!editFileProperties) return;
|
||||
if (!categoryListRef.current) throw new Error("No CategoryListRef found");
|
||||
if (!userAddress) throw new Error("Unable to locate user address");
|
||||
if (!description) throw new Error("Please enter a description");
|
||||
const allCategoriesSelected = !selectedCategories.includes("");
|
||||
if (!allCategoriesSelected)
|
||||
throw new Error("All Categories must be selected");
|
||||
|
||||
console.log("categories", selectedCategories);
|
||||
const QappsCategoryID = "3";
|
||||
if (
|
||||
selectedCategories[0] === QappsCategoryID &&
|
||||
!autocompleteRef?.current?.getSelectedValue()
|
||||
)
|
||||
throw new Error("Select a published Q-App");
|
||||
let errorMsg = "";
|
||||
let name = "";
|
||||
if (username) {
|
||||
@ -160,7 +186,7 @@ export const EditIssue = () => {
|
||||
"Cannot publish without access to your name. Please authenticate.";
|
||||
}
|
||||
|
||||
if (editFileProperties?.user !== username) {
|
||||
if (editIssueProperties?.user !== username) {
|
||||
errorMsg = "Cannot publish another user's resource";
|
||||
}
|
||||
|
||||
@ -223,9 +249,8 @@ export const EditIssue = () => {
|
||||
filename = alphanumericString;
|
||||
}
|
||||
|
||||
let metadescription =
|
||||
`**${categoryListRef.current?.getCategoriesFetchString()}**` +
|
||||
fullDescription.slice(0, 150);
|
||||
const categoryString = `**${categoryListRef.current?.getSelectedCategories()}**`;
|
||||
let metadescription = categoryString + fullDescription.slice(0, 150);
|
||||
|
||||
const requestBodyVideo: any = {
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
@ -248,23 +273,50 @@ export const EditIssue = () => {
|
||||
size: file.size,
|
||||
});
|
||||
}
|
||||
const selectedQappName = autocompleteRef?.current?.getSelectedValue();
|
||||
|
||||
const fileObject: any = {
|
||||
const issueObject: any = {
|
||||
title,
|
||||
version: editFileProperties.version,
|
||||
version: editIssueProperties.version,
|
||||
fullDescription,
|
||||
htmlDescription: description,
|
||||
commentsId: editFileProperties.commentsId,
|
||||
commentsId: editIssueProperties.commentsId,
|
||||
...categoryListRef.current?.categoriesToObject(),
|
||||
files: fileReferences,
|
||||
images: imagePublisherRef?.current?.getImageArray(),
|
||||
QappName: selectedQappName,
|
||||
feeData: editIssueProperties?.feeData,
|
||||
};
|
||||
if (payFee) {
|
||||
const publishFeeResponse = await payPublishFeeQORT(feeAmountBase);
|
||||
if (!publishFeeResponse) {
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: "Fee publish rejected by user.",
|
||||
alertType: "error",
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (log) console.log("feeResponse: ", publishFeeResponse);
|
||||
|
||||
let metadescription =
|
||||
`**${categoryListRef.current?.getCategoriesFetchString()}**` +
|
||||
fullDescription.slice(0, 150);
|
||||
issueObject.feeData = { signature: publishFeeResponse };
|
||||
dispatch(updateInHashMap(issueObject)); // shows issue as paid right away?
|
||||
}
|
||||
|
||||
const fileObjectToBase64 = await objectToBase64(fileObject);
|
||||
const QappNameString = autocompleteRef?.current?.getQappNameFetchString();
|
||||
const categoryString =
|
||||
categoryListRef.current?.getCategoriesFetchString(selectedCategories);
|
||||
const metaDataString = `**${categoryString + QappNameString}**`;
|
||||
|
||||
let metadescription = metaDataString + fullDescription.slice(0, 150);
|
||||
if (log) console.log("description is: ", metadescription);
|
||||
if (log) console.log("description length is: ", metadescription.length);
|
||||
if (log) console.log("characters left:", 240 - metadescription.length);
|
||||
if (log)
|
||||
console.log("% of characters used:", metadescription.length / 240);
|
||||
|
||||
const fileObjectToBase64 = await objectToBase64(issueObject);
|
||||
// Description is obtained from raw data
|
||||
|
||||
const requestBodyJson: any = {
|
||||
@ -274,7 +326,7 @@ export const EditIssue = () => {
|
||||
data64: fileObjectToBase64,
|
||||
title: title.slice(0, 50),
|
||||
description: metadescription,
|
||||
identifier: editFileProperties.id,
|
||||
identifier: editIssueProperties.id,
|
||||
tag1: QSUPPORT_FILE_BASE,
|
||||
filename: `video_metadata.json`,
|
||||
};
|
||||
@ -287,10 +339,13 @@ export const EditIssue = () => {
|
||||
setPublishes(multiplePublish);
|
||||
setIsOpenMultiplePublish(true);
|
||||
setVideoPropertiesToSetToRedux({
|
||||
...editFileProperties,
|
||||
...fileObject,
|
||||
...editIssueProperties,
|
||||
...issueObject,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.log("error is: ", error);
|
||||
if (error === "User declined request") return;
|
||||
|
||||
let notificationObj: any = null;
|
||||
if (typeof error === "string") {
|
||||
notificationObj = {
|
||||
@ -315,26 +370,15 @@ export const EditIssue = () => {
|
||||
}
|
||||
}
|
||||
|
||||
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 isShowQappNameTextField = () => {
|
||||
const QappID = "3";
|
||||
return selectedCategories[0] === QappID;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={!!editFileProperties}
|
||||
open={!!editIssueProperties}
|
||||
aria-labelledby="modal-title"
|
||||
aria-describedby="modal-description"
|
||||
>
|
||||
@ -410,15 +454,26 @@ export const EditIssue = () => {
|
||||
>
|
||||
<CategoryList
|
||||
categoryData={allCategoryData}
|
||||
initialCategories={editCategories}
|
||||
initialCategories={selectedCategories}
|
||||
columns={3}
|
||||
ref={categoryListRef}
|
||||
showEmptyItem={false}
|
||||
afterChange={newSelectedCategories => {
|
||||
setSelectedCategories(newSelectedCategories);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
{isShowQappNameTextField() && (
|
||||
<AutocompleteQappNames
|
||||
ref={autocompleteRef}
|
||||
namesList={QappNames}
|
||||
initialSelection={editIssueProperties?.QappName}
|
||||
/>
|
||||
)}
|
||||
<ImagePublisher
|
||||
ref={imagePublisherRef}
|
||||
initialImages={editFileProperties?.images}
|
||||
initialImages={editIssueProperties?.images}
|
||||
/>
|
||||
<CustomInputField
|
||||
name="title"
|
||||
@ -466,10 +521,11 @@ export const EditIssue = () => {
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{isIssuePaid === false && (
|
||||
<ThemeButtonBright
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
publishQDNResource();
|
||||
publishQDNResource(true);
|
||||
}}
|
||||
sx={{
|
||||
fontFamily: "Montserrat",
|
||||
@ -478,7 +534,23 @@ export const EditIssue = () => {
|
||||
letterSpacing: "0.2px",
|
||||
}}
|
||||
>
|
||||
Publish
|
||||
Publish Edit with Fee
|
||||
</ThemeButtonBright>
|
||||
)}
|
||||
|
||||
<ThemeButtonBright
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
publishQDNResource(false);
|
||||
}}
|
||||
sx={{
|
||||
fontFamily: "Montserrat",
|
||||
fontSize: "16px",
|
||||
fontWeight: 400,
|
||||
letterSpacing: "0.2px",
|
||||
}}
|
||||
>
|
||||
Publish Edit
|
||||
</ThemeButtonBright>
|
||||
</Box>
|
||||
</CrowdfundActionButtonRow>
|
||||
@ -506,7 +578,7 @@ export const EditIssue = () => {
|
||||
dispatch(updateInHashMap(clonedCopy));
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: "File updated",
|
||||
msg: "Issue updated",
|
||||
alertType: "success",
|
||||
})
|
||||
);
|
||||
|
@ -43,9 +43,9 @@ import { PlaylistListEdit } from "../PlaylistListEdit/PlaylistListEdit";
|
||||
import { TextEditor } from "../common/TextEditor/TextEditor";
|
||||
import { extractTextFromHTML } from "../common/TextEditor/utils";
|
||||
import {
|
||||
firstCategories,
|
||||
secondCategories,
|
||||
} from "../../constants/Categories/1stCategories.ts";
|
||||
issueLocation,
|
||||
thirdCategories,
|
||||
} from "../../constants/Categories/Categories.ts";
|
||||
|
||||
const uid = new ShortUniqueId();
|
||||
const shortuid = new ShortUniqueId({ length: 5 });
|
||||
@ -183,7 +183,7 @@ export const EditPlaylist = () => {
|
||||
setVideos(editVideoProperties?.videos || []);
|
||||
|
||||
if (editVideoProperties?.category) {
|
||||
const selectedOption = firstCategories.find(
|
||||
const selectedOption = issueLocation.find(
|
||||
option => option.id === +editVideoProperties.category
|
||||
);
|
||||
setSelectedCategoryVideos(selectedOption || null);
|
||||
@ -192,9 +192,9 @@ export const EditPlaylist = () => {
|
||||
if (
|
||||
editVideoProperties?.category &&
|
||||
editVideoProperties?.subcategory &&
|
||||
secondCategories[+editVideoProperties?.category]
|
||||
thirdCategories[+editVideoProperties?.category]
|
||||
) {
|
||||
const selectedOption = secondCategories[
|
||||
const selectedOption = thirdCategories[
|
||||
+editVideoProperties?.category
|
||||
]?.find(option => option.id === +editVideoProperties.subcategory);
|
||||
setSelectedSubCategoryVideos(selectedOption || null);
|
||||
@ -405,7 +405,7 @@ export const EditPlaylist = () => {
|
||||
event: SelectChangeEvent<string>
|
||||
) => {
|
||||
const optionId = event.target.value;
|
||||
const selectedOption = firstCategories.find(
|
||||
const selectedOption = issueLocation.find(
|
||||
option => option.id === +optionId
|
||||
);
|
||||
setSelectedCategoryVideos(selectedOption || null);
|
||||
@ -479,7 +479,7 @@ export const EditPlaylist = () => {
|
||||
value={selectedCategoryVideos?.id || ""}
|
||||
onChange={handleOptionCategoryChangeVideos}
|
||||
>
|
||||
{firstCategories.map(option => (
|
||||
{issueLocation.map(option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
@ -487,7 +487,7 @@ export const EditPlaylist = () => {
|
||||
</Select>
|
||||
</FormControl>
|
||||
{selectedCategoryVideos &&
|
||||
secondCategories[selectedCategoryVideos?.id] && (
|
||||
thirdCategories[selectedCategoryVideos?.id] && (
|
||||
<FormControl fullWidth sx={{ marginBottom: 2 }}>
|
||||
<InputLabel id="Category">Select a Sub-Category</InputLabel>
|
||||
<Select
|
||||
@ -497,11 +497,11 @@ export const EditPlaylist = () => {
|
||||
onChange={e =>
|
||||
handleOptionSubCategoryChangeVideos(
|
||||
e,
|
||||
secondCategories[selectedCategoryVideos?.id]
|
||||
thirdCategories[selectedCategoryVideos?.id]
|
||||
)
|
||||
}
|
||||
>
|
||||
{secondCategories[selectedCategoryVideos.id].map(
|
||||
{thirdCategories[selectedCategoryVideos.id].map(
|
||||
option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Autocomplete,
|
||||
Box,
|
||||
Button,
|
||||
Grid,
|
||||
@ -11,8 +12,10 @@ import {
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
|
||||
import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate";
|
||||
import { TimesSVG } from "../../assets/svgs/TimesSVG";
|
||||
import { fontSizeMedium } from "../../constants/Misc.ts";
|
||||
|
||||
export const DoubleLine = styled(Typography)`
|
||||
display: -webkit-box;
|
||||
@ -59,7 +62,7 @@ export const ModalBody = styled(Box)(({ theme }) => ({
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: "75%",
|
||||
maxWidth: "900px",
|
||||
maxWidth: "1000px",
|
||||
padding: "15px 35px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
@ -113,9 +116,9 @@ export const NewCrowdfundTimeDescription = styled(Typography)(({ theme }) => ({
|
||||
textDecoration: "underline",
|
||||
}));
|
||||
|
||||
export const CustomInputField = styled(TextField)(({ theme }) => ({
|
||||
const getInputFieldStyles = (theme: any) => {
|
||||
return {
|
||||
fontFamily: "Mulish",
|
||||
fontSize: "19px",
|
||||
letterSpacing: "0px",
|
||||
fontWeight: 400,
|
||||
color: theme.palette.text.primary,
|
||||
@ -124,7 +127,7 @@ export const CustomInputField = styled(TextField)(({ theme }) => ({
|
||||
"& label": {
|
||||
color: theme.palette.mode === "light" ? "#808183" : "#edeef0",
|
||||
fontFamily: "Mulish",
|
||||
fontSize: "19px",
|
||||
fontSize: fontSizeMedium,
|
||||
letterSpacing: "0px",
|
||||
fontWeight: 400,
|
||||
},
|
||||
@ -147,7 +150,7 @@ export const CustomInputField = styled(TextField)(({ theme }) => ({
|
||||
},
|
||||
"& .MuiInputBase-root": {
|
||||
fontFamily: "Mulish",
|
||||
fontSize: "19px",
|
||||
fontSize: "25px",
|
||||
letterSpacing: "0px",
|
||||
fontWeight: 400,
|
||||
},
|
||||
@ -157,6 +160,15 @@ export const CustomInputField = styled(TextField)(({ theme }) => ({
|
||||
"& .MuiFilledInput-root:after": {
|
||||
borderBottomColor: theme.palette.secondary.main,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const CustomInputField = styled(TextField)(({ theme }) => ({
|
||||
...getInputFieldStyles(theme),
|
||||
}));
|
||||
|
||||
export const CustomAutoCompleteField = styled(Autocomplete)(({ theme }) => ({
|
||||
...getInputFieldStyles(theme),
|
||||
}));
|
||||
|
||||
export const CrowdfundTitle = styled(Typography)(({ theme }) => ({
|
||||
|
@ -21,19 +21,34 @@ import { QSUPPORT_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 { allCategoryData } from "../../constants/Categories/Categories.ts";
|
||||
import {
|
||||
fontSizeLarge,
|
||||
fontSizeSmall,
|
||||
log,
|
||||
titleFormatter,
|
||||
} from "../../constants/Misc.ts";
|
||||
import {
|
||||
appendCategoryToList,
|
||||
CategoryList,
|
||||
CategoryListRef,
|
||||
} from "../common/CategoryList/CategoryList.tsx";
|
||||
import { SupportState } from "../../constants/Categories/2ndCategories.ts";
|
||||
import {
|
||||
ImagePublisher,
|
||||
ImagePublisherRef,
|
||||
} from "../common/ImagePublisher/ImagePublisher.tsx";
|
||||
import { ThemeButtonBright } from "../../pages/Home/Home-styles.tsx";
|
||||
import {
|
||||
AutocompleteQappNames,
|
||||
QappNamesRef,
|
||||
} from "../common/AutocompleteQappNames.tsx";
|
||||
import {
|
||||
feeAmountBase,
|
||||
feeDisclaimer,
|
||||
} from "../../constants/PublishFees/FeeData.tsx";
|
||||
import {
|
||||
payPublishFeeQORT,
|
||||
PublishFeeData,
|
||||
} from "../../constants/PublishFees/SendFeeFunctions.ts";
|
||||
|
||||
const uid = new ShortUniqueId();
|
||||
const shortuid = new ShortUniqueId({ length: 5 });
|
||||
@ -53,14 +68,21 @@ interface VideoFile {
|
||||
description: string;
|
||||
coverImage?: string;
|
||||
}
|
||||
|
||||
export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
|
||||
const [QappName, setQappName] = useState<string>("");
|
||||
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||
const username = useSelector((state: RootState) => state.auth?.user?.name);
|
||||
const userAddress = useSelector(
|
||||
(state: RootState) => state.auth?.user?.address
|
||||
);
|
||||
const QappNames = useSelector(
|
||||
(state: RootState) => state.file.publishedQappNames
|
||||
);
|
||||
const [files, setFiles] = useState<VideoFile[]>([]);
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
@ -77,8 +99,11 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
|
||||
const [playlistSetting, setPlaylistSetting] = useState<null | string>(null);
|
||||
const [publishes, setPublishes] = useState<any>(null);
|
||||
|
||||
const categoryListRef = useRef<CategoryListRef>(null);
|
||||
const imagePublisherRef = useRef<ImagePublisherRef>(null);
|
||||
const autocompleteRef = useRef<QappNamesRef>(null);
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
maxFiles: 10,
|
||||
maxSize: 419430400, // 400 MB in bytes
|
||||
@ -128,8 +153,18 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
if (!categoryListRef.current) throw new Error("No CategoryListRef found");
|
||||
if (!userAddress) throw new Error("Unable to locate user address");
|
||||
if (!description) throw new Error("Please enter a description");
|
||||
if (!categoryListRef.current?.getSelectedCategories()[0])
|
||||
throw new Error("Please select a category");
|
||||
|
||||
const allCategoriesSelected =
|
||||
selectedCategories && selectedCategories[0] && selectedCategories[1];
|
||||
if (!allCategoriesSelected)
|
||||
throw new Error("All Categories must be selected");
|
||||
|
||||
const QappsCategoryID = "3";
|
||||
if (
|
||||
selectedCategories[0] === QappsCategoryID &&
|
||||
!autocompleteRef?.current?.getSelectedValue()
|
||||
)
|
||||
throw new Error("Select a published Q-App");
|
||||
let errorMsg = "";
|
||||
let name = "";
|
||||
if (username) {
|
||||
@ -200,14 +235,10 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
filename = alphanumericString;
|
||||
}
|
||||
|
||||
const categoryList = appendCategoryToList(
|
||||
categoryListRef.current?.getSelectedCategories(),
|
||||
"101"
|
||||
);
|
||||
const categoryString = `**${categoryListRef.current?.getCategoriesFetchString(categoryList)}**`;
|
||||
const categoryString = `**${categoryListRef.current?.getSelectedCategories()}**`;
|
||||
let metadescription = categoryString + fullDescription.slice(0, 150);
|
||||
|
||||
const requestBodyVideo: any = {
|
||||
const requestBodyFile: any = {
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: name,
|
||||
service: "FILE",
|
||||
@ -218,7 +249,7 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
filename,
|
||||
tag1: QSUPPORT_FILE_BASE,
|
||||
};
|
||||
listOfPublishes.push(requestBodyVideo);
|
||||
listOfPublishes.push(requestBodyFile);
|
||||
fileReferences.push({
|
||||
filename: file.name,
|
||||
identifier,
|
||||
@ -232,12 +263,19 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
const idMeta = uid();
|
||||
const identifier = `${QSUPPORT_FILE_BASE}${sanitizeTitle.slice(0, 30)}_${idMeta}`;
|
||||
|
||||
const categoryList = appendCategoryToList(
|
||||
categoryListRef.current?.getSelectedCategories(),
|
||||
"101"
|
||||
);
|
||||
const categoryList = categoryListRef.current?.getSelectedCategories();
|
||||
|
||||
const fileObject: any = {
|
||||
const selectedQappName = autocompleteRef?.current?.getSelectedValue();
|
||||
|
||||
const publishFeeResponse = await payPublishFeeQORT(feeAmountBase);
|
||||
if (log) console.log("feeResponse: ", publishFeeResponse);
|
||||
|
||||
const feeData: PublishFeeData = {
|
||||
signature: publishFeeResponse,
|
||||
senderName: "",
|
||||
};
|
||||
|
||||
const issueObject: any = {
|
||||
title,
|
||||
version: 1,
|
||||
fullDescription,
|
||||
@ -246,12 +284,24 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
...categoryListRef.current?.categoriesToObject(categoryList),
|
||||
files: fileReferences,
|
||||
images: imagePublisherRef?.current?.getImageArray(),
|
||||
QappName: selectedQappName,
|
||||
feeData,
|
||||
};
|
||||
|
||||
const categoryString = `**${categoryListRef.current?.getCategoriesFetchString(categoryList)}**`;
|
||||
let metadescription = categoryString + fullDescription.slice(0, 150);
|
||||
const QappNameString = autocompleteRef?.current?.getQappNameFetchString();
|
||||
const categoryString =
|
||||
categoryListRef.current?.getCategoriesFetchString(categoryList);
|
||||
const metaDataString = `**${categoryString + QappNameString}**`;
|
||||
|
||||
const fileObjectToBase64 = await objectToBase64(fileObject);
|
||||
let metadescription = metaDataString + fullDescription.slice(0, 150);
|
||||
|
||||
if (log) console.log("description is: ", metadescription);
|
||||
if (log) console.log("description length is: ", metadescription.length);
|
||||
if (log) console.log("characters left:", 240 - metadescription.length);
|
||||
if (log)
|
||||
console.log("% of characters used:", metadescription.length / 240);
|
||||
|
||||
const fileObjectToBase64 = await objectToBase64(issueObject);
|
||||
// Description is obtained from raw data
|
||||
const requestBodyJson: any = {
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
@ -295,6 +345,11 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
const isShowQappNameTextField = () => {
|
||||
const QappID = "3";
|
||||
return selectedCategories[0] === QappID;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{username && (
|
||||
@ -386,13 +441,29 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
categoryData={allCategoryData}
|
||||
ref={categoryListRef}
|
||||
columns={3}
|
||||
excludeCategories={SupportState}
|
||||
afterChange={newSelectedCategories => {
|
||||
if (
|
||||
newSelectedCategories[0] &&
|
||||
newSelectedCategories[1] &&
|
||||
!newSelectedCategories[2]
|
||||
) {
|
||||
newSelectedCategories[2] = "101";
|
||||
}
|
||||
setSelectedCategories(newSelectedCategories);
|
||||
}}
|
||||
showEmptyItem={false}
|
||||
/>
|
||||
</Box>
|
||||
{isShowQappNameTextField() && (
|
||||
<AutocompleteQappNames
|
||||
ref={autocompleteRef}
|
||||
namesList={QappNames}
|
||||
/>
|
||||
)}
|
||||
<ImagePublisher ref={imagePublisherRef} />
|
||||
<CustomInputField
|
||||
name="title"
|
||||
label="Title of Issue"
|
||||
label="Title"
|
||||
variant="filled"
|
||||
value={title}
|
||||
onChange={e => {
|
||||
@ -400,15 +471,15 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
const formattedValue = value.replace(titleFormatter, "");
|
||||
setTitle(formattedValue);
|
||||
}}
|
||||
inputProps={{ maxLength: 180 }}
|
||||
inputProps={{ maxLength: 60 }}
|
||||
required
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "18px",
|
||||
fontSize: fontSizeLarge,
|
||||
}}
|
||||
>
|
||||
Description of Issue
|
||||
Description
|
||||
</Typography>
|
||||
<TextEditor
|
||||
inlineContent={description}
|
||||
@ -426,7 +497,10 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
}}
|
||||
variant="contained"
|
||||
color="error"
|
||||
sx={{ color: theme.palette.text.primary }}
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: fontSizeSmall,
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
@ -439,13 +513,10 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
>
|
||||
<ThemeButtonBright
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
publishQDNResource();
|
||||
}}
|
||||
onClick={publishQDNResource}
|
||||
sx={{
|
||||
fontFamily: "Montserrat",
|
||||
fontSize: "16px",
|
||||
fontWeight: 400,
|
||||
fontWeight: "400",
|
||||
letterSpacing: "0.2px",
|
||||
}}
|
||||
>
|
||||
@ -453,6 +524,7 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
</ThemeButtonBright>
|
||||
</Box>
|
||||
</ActionButtonRow>
|
||||
{feeDisclaimer}
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
|
||||
|
@ -15,14 +15,14 @@ export const StatsData = () => {
|
||||
}));
|
||||
|
||||
const {
|
||||
getFiles,
|
||||
checkAndUpdateFile,
|
||||
getFile,
|
||||
getIssues,
|
||||
checkAndUpdateIssue,
|
||||
getIssue,
|
||||
hashMapFiles,
|
||||
getNewFiles,
|
||||
checkNewFiles,
|
||||
getFilesFiltered,
|
||||
getFilesCount,
|
||||
getNewIssues,
|
||||
checkNewIssues,
|
||||
getIssuesFiltered,
|
||||
getIssuesCount,
|
||||
} = useFetchIssues();
|
||||
|
||||
const totalIssuesPublished = useSelector(
|
||||
@ -36,8 +36,8 @@ export const StatsData = () => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
getFilesCount();
|
||||
}, [getFilesCount]);
|
||||
getIssuesCount();
|
||||
}, [getIssuesCount]);
|
||||
|
||||
return (
|
||||
totalIssuesPublished > 0 && (
|
||||
|
135
src/components/common/AutocompleteQappNames.tsx
Normal file
135
src/components/common/AutocompleteQappNames.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import React, { useEffect, useImperativeHandle, useState } from "react";
|
||||
import { Autocomplete, SxProps, Theme } from "@mui/material";
|
||||
import { CustomInputField } from "../PublishIssue/PublishIssue-styles.tsx";
|
||||
import { log } from "../../constants/Misc.ts";
|
||||
|
||||
interface AutoCompleteQappNamesProps {
|
||||
namesList?: string[];
|
||||
afterChange?: (selectedName: string) => void;
|
||||
sx?: SxProps<Theme>;
|
||||
required?: boolean;
|
||||
initialSelection?: string;
|
||||
}
|
||||
|
||||
export type QappNamesRef = {
|
||||
getSelectedValue: () => string;
|
||||
setSelectedValue: (selectedValue: string) => void;
|
||||
getQappNameFetchString: () => string;
|
||||
};
|
||||
|
||||
export const AutocompleteQappNames = React.forwardRef<
|
||||
QappNamesRef,
|
||||
AutoCompleteQappNamesProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
namesList,
|
||||
afterChange,
|
||||
sx,
|
||||
required = true,
|
||||
initialSelection = null,
|
||||
}: AutoCompleteQappNamesProps,
|
||||
ref
|
||||
) => {
|
||||
const [QappNamesList, setQappNamesList] = useState<string[]>([]);
|
||||
|
||||
const [selectedQappName, setSelectedQappName] = useState<string>(
|
||||
initialSelection || null
|
||||
);
|
||||
|
||||
if (log) console.log("initial selection: ", initialSelection);
|
||||
useEffect(() => {
|
||||
if (namesList) {
|
||||
if (log) console.log("prop namesList: ", namesList);
|
||||
setQappNamesList(namesList);
|
||||
return;
|
||||
}
|
||||
|
||||
getPublishedQappNames().then((names: string[]) => {
|
||||
setQappNamesList(names);
|
||||
if (log) console.log("QappNames set manually");
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedQappName(initialSelection || null);
|
||||
}, [initialSelection]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getSelectedValue: () => {
|
||||
return selectedQappName;
|
||||
},
|
||||
setSelectedValue: (selectedValue: string) => {
|
||||
setSelectedQappName(selectedValue);
|
||||
},
|
||||
getQappNameFetchString: () => {
|
||||
return getQappNameFetchString(selectedQappName);
|
||||
},
|
||||
}));
|
||||
return (
|
||||
<Autocomplete
|
||||
options={QappNamesList}
|
||||
value={selectedQappName}
|
||||
onChange={(e, newValue) => {
|
||||
setSelectedQappName(newValue);
|
||||
if (afterChange) afterChange(newValue || null);
|
||||
}}
|
||||
sx={{ height: "100px", ...sx }}
|
||||
renderInput={params => (
|
||||
<CustomInputField
|
||||
{...params}
|
||||
label={"Q-App/Website Name"}
|
||||
value={selectedQappName}
|
||||
variant={"filled"}
|
||||
required={required}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export interface MetaData {
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
export interface SearchResourcesResponse {
|
||||
name: string;
|
||||
service: string;
|
||||
identifier: string;
|
||||
metadata?: MetaData;
|
||||
size: number;
|
||||
created: number;
|
||||
updated: number;
|
||||
}
|
||||
|
||||
const searchService = (service: string) => {
|
||||
return qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: service,
|
||||
limit: 0,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPublishedQappNames = async () => {
|
||||
const QappList: Promise<SearchResourcesResponse[]> = searchService("APP");
|
||||
const siteList: Promise<SearchResourcesResponse[]> = searchService("WEBSITE");
|
||||
|
||||
const responses = await Promise.all([QappList, siteList]);
|
||||
const processedQappList = responses[0].map(value => value.name);
|
||||
const processedWebsiteList = responses[1].map(value => value.name);
|
||||
|
||||
const removedDuplicates = Array.from(
|
||||
new Set<string>([...processedQappList, ...processedWebsiteList])
|
||||
);
|
||||
return removedDuplicates.sort((a, b) => {
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
};
|
||||
|
||||
export const getQappNameFetchString = (selectedQappName: string) => {
|
||||
return selectedQappName ? `Qapp:${selectedQappName};` : "";
|
||||
};
|
@ -12,13 +12,15 @@ import {
|
||||
|
||||
import React, { useEffect, useImperativeHandle, useState } from "react";
|
||||
import { CategoryContainer } from "./CategoryList-styles.tsx";
|
||||
import { allCategoryData } from "../../../constants/Categories/1stCategories.ts";
|
||||
import { allCategoryData } from "../../../constants/Categories/Categories.ts";
|
||||
import { log } from "../../../constants/Misc.ts";
|
||||
import { findCategoryData } from "../../../constants/Categories/CategoryFunctions.ts";
|
||||
|
||||
export interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface Categories {
|
||||
@ -29,8 +31,6 @@ export interface CategoryData {
|
||||
subCategories: Categories[];
|
||||
}
|
||||
|
||||
type ListDirection = "column" | "row";
|
||||
|
||||
interface CategoryListProps {
|
||||
sx?: SxProps<Theme>;
|
||||
categoryData: CategoryData;
|
||||
@ -38,6 +38,7 @@ interface CategoryListProps {
|
||||
columns?: number;
|
||||
afterChange?: (categories: string[]) => void;
|
||||
excludeCategories?: Category[];
|
||||
showEmptyItem?: boolean;
|
||||
}
|
||||
|
||||
export type CategoryListRef = {
|
||||
@ -60,6 +61,7 @@ export const CategoryList = React.forwardRef<
|
||||
columns = 1,
|
||||
afterChange,
|
||||
excludeCategories,
|
||||
showEmptyItem = true,
|
||||
}: CategoryListProps,
|
||||
ref
|
||||
) => {
|
||||
@ -127,7 +129,8 @@ export const CategoryList = React.forwardRef<
|
||||
const newSelectedCategories: string[] = selectedCategories.map(
|
||||
(category, categoryIndex) => {
|
||||
if (index > categoryIndex) return category;
|
||||
else if (index === categoryIndex) return selectedOption.id.toString();
|
||||
else if (index === categoryIndex)
|
||||
return selectedOption?.id?.toString();
|
||||
else return "";
|
||||
}
|
||||
);
|
||||
@ -140,23 +143,31 @@ export const CategoryList = React.forwardRef<
|
||||
};
|
||||
|
||||
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
|
||||
},
|
||||
},
|
||||
// // Target the input field
|
||||
// ".MuiSelect-select": {
|
||||
// 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 emptyMenuItem = (
|
||||
<MenuItem
|
||||
key={""}
|
||||
value={""}
|
||||
sx={{
|
||||
"@media (min-width: 600px)": { minHeight: "46.5px" },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const fillMenu = (category: Categories, index: number) => {
|
||||
const subCategoryIndex = selectedCategories[index];
|
||||
if (log) console.log("selected categories: ", selectedCategories);
|
||||
@ -171,12 +182,23 @@ export const CategoryList = React.forwardRef<
|
||||
if (log) console.log("categoryData: ", categoryData);
|
||||
|
||||
const menuToFill = category[subCategoryIndex];
|
||||
if (menuToFill)
|
||||
return menuToFill.map(option => (
|
||||
if (menuToFill) {
|
||||
const menuItems = [];
|
||||
|
||||
if (showEmptyItem) menuItems.push(emptyMenuItem);
|
||||
|
||||
menuToFill.map(option =>
|
||||
menuItems.push(
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
));
|
||||
)
|
||||
);
|
||||
if (log) console.log(" returning menuItems: ", menuItems);
|
||||
|
||||
return menuItems;
|
||||
}
|
||||
if (log) console.log("not returning menuItems");
|
||||
};
|
||||
|
||||
const hasSubCategory = (category: Categories, index: number) => {
|
||||
@ -202,11 +224,11 @@ export const CategoryList = React.forwardRef<
|
||||
<FormControl fullWidth sx={{ marginBottom: 1 }}>
|
||||
<InputLabel
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
fontSize: "24px",
|
||||
}}
|
||||
id="Category-1"
|
||||
>
|
||||
Category
|
||||
{categoryData.category[0]?.label || "Category"}
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="Category 1"
|
||||
@ -217,6 +239,7 @@ export const CategoryList = React.forwardRef<
|
||||
}}
|
||||
sx={categorySelectSX}
|
||||
>
|
||||
{showEmptyItem && emptyMenuItem}
|
||||
{categoryData.category.map(option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
@ -237,11 +260,14 @@ export const CategoryList = React.forwardRef<
|
||||
>
|
||||
<InputLabel
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
fontSize: "24px",
|
||||
}}
|
||||
id={`Category-${index + 2}`}
|
||||
>
|
||||
{`Category-${index + 2}`}
|
||||
{findCategoryData(+selectedCategories[index + 1])
|
||||
?.label ||
|
||||
category[selectedCategories[index]][0]?.label ||
|
||||
`Category-${index + 2}`}
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId={`Category ${index + 2}`}
|
||||
@ -250,24 +276,6 @@ export const CategoryList = React.forwardRef<
|
||||
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>
|
||||
@ -285,9 +293,9 @@ 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}`;
|
||||
if (index === 0 && category) fetchString += `cat:${category};`;
|
||||
else if (index === 1 && category) fetchString += `sub:${category};`;
|
||||
else if (category) fetchString += `sub${index}:${category};`;
|
||||
}
|
||||
});
|
||||
if (log) console.log("categoriesAsDescription: ", fetchString);
|
||||
@ -318,3 +326,16 @@ export const getCategoriesFromObject = (editFileProperties: any) => {
|
||||
}
|
||||
return categoryList;
|
||||
};
|
||||
|
||||
export const getCategoriesLength = categoryList => {
|
||||
return categoryList.filter(category => category !== "").length;
|
||||
};
|
||||
|
||||
export const hasCategories = (categories: string[]) => {
|
||||
return categories.findIndex(category => category !== "") >= 0;
|
||||
};
|
||||
|
||||
export const appendCategory = (categoryList: string[], category: string) => {
|
||||
const nextIndex = categoryList.findIndex(category => category === "");
|
||||
categoryList[nextIndex] = category;
|
||||
};
|
||||
|
@ -0,0 +1,178 @@
|
||||
import {
|
||||
FormControl,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
OutlinedInput,
|
||||
Select,
|
||||
SxProps,
|
||||
Theme,
|
||||
} from "@mui/material";
|
||||
|
||||
import React, { useEffect, useImperativeHandle, useState } from "react";
|
||||
import { CategoryContainer } from "./CategoryList-styles.tsx";
|
||||
import { log } from "../../../constants/Misc.ts";
|
||||
|
||||
export interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface Categories {
|
||||
[key: number]: Category[];
|
||||
}
|
||||
export interface CategoryData {
|
||||
category: Category[];
|
||||
subCategories: Categories[];
|
||||
}
|
||||
|
||||
interface CategoryListProps {
|
||||
sx?: SxProps<Theme>;
|
||||
categoryData: Category[];
|
||||
initialCategory?: string;
|
||||
afterChange?: (category: string) => void;
|
||||
showEmptyItem?: boolean;
|
||||
}
|
||||
|
||||
export type CategorySelectRef = {
|
||||
getSelectedCategory: () => string;
|
||||
setSelectedCategory: (arr: string) => void;
|
||||
clearCategory: () => void;
|
||||
getCategoryFetchString: (categories?: string) => string;
|
||||
};
|
||||
|
||||
export const CategorySelect = React.forwardRef<
|
||||
CategorySelectRef,
|
||||
CategoryListProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
sx,
|
||||
categoryData,
|
||||
initialCategory,
|
||||
afterChange,
|
||||
showEmptyItem = true,
|
||||
}: CategoryListProps,
|
||||
ref
|
||||
) => {
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>(
|
||||
initialCategory || ""
|
||||
);
|
||||
useEffect(() => {
|
||||
if (initialCategory) setSelectedCategory(initialCategory);
|
||||
}, [initialCategory]);
|
||||
|
||||
const updateCategory = (category: string) => {
|
||||
if (log) console.log("updateCategory ID: ", category);
|
||||
setSelectedCategory(category);
|
||||
if (afterChange) afterChange(category);
|
||||
};
|
||||
const categoryToObject = (category: string) => {
|
||||
let categoryObject = {};
|
||||
categoryObject["category"] = category;
|
||||
if (log) console.log("categoryObject is: ", categoryObject);
|
||||
return categoryObject;
|
||||
};
|
||||
|
||||
const clearCategory = () => {
|
||||
updateCategory("");
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getSelectedCategory: () => {
|
||||
return selectedCategory;
|
||||
},
|
||||
setSelectedCategory: category => {
|
||||
if (log) console.log("setSelectedCategory: ", category);
|
||||
updateCategory(category);
|
||||
},
|
||||
clearCategory,
|
||||
getCategoryFetchString: (category?: string) =>
|
||||
getCategoryFetchString(category || selectedCategory),
|
||||
categoriesToObject: (category?: string) =>
|
||||
categoryToObject(category || selectedCategory),
|
||||
}));
|
||||
|
||||
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 emptyMenuItem = (
|
||||
<MenuItem
|
||||
key={""}
|
||||
value={""}
|
||||
// sx={{
|
||||
// "& .MuiButtonBase-root-MuiMenuItem-root": {
|
||||
// minHeight: "50px",
|
||||
// },
|
||||
sx={{
|
||||
"@media (min-width: 600px)": { minHeight: "46.5px" },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const fillMenu = () => {
|
||||
const menuItems = [];
|
||||
if (showEmptyItem) menuItems.push(emptyMenuItem);
|
||||
|
||||
categoryData.map(option =>
|
||||
menuItems.push(
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
)
|
||||
);
|
||||
return menuItems;
|
||||
};
|
||||
return (
|
||||
<CategoryContainer sx={{ width: "100%", ...sx }}>
|
||||
<FormControl fullWidth sx={{ marginBottom: 1 }}>
|
||||
<InputLabel
|
||||
sx={{
|
||||
fontSize: "24px",
|
||||
}}
|
||||
id="Category-1"
|
||||
>
|
||||
{categoryData[0]?.label || "Category"}
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="Category 1"
|
||||
input={<OutlinedInput label="Category 1" />}
|
||||
value={selectedCategory || ""}
|
||||
onChange={e => {
|
||||
updateCategory(e.target.value);
|
||||
}}
|
||||
>
|
||||
{fillMenu()}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</CategoryContainer>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const getCategoryFetchString = (category: string) => {
|
||||
return `cat:${category}`;
|
||||
};
|
||||
|
||||
export const getCategoryFromObject = (editFileProperties: any) => {
|
||||
const categoryList: string[] = [];
|
||||
if (editFileProperties.category)
|
||||
categoryList.push(editFileProperties.category);
|
||||
return categoryList;
|
||||
};
|
@ -1,7 +1,6 @@
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
@ -9,26 +8,26 @@ import {
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import React, { useCallback, useState, useEffect } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { CommentEditor } from "./CommentEditor";
|
||||
import {
|
||||
AuthorTextComment,
|
||||
CardContentContainerComment,
|
||||
CommentActionButtonRow,
|
||||
CommentDateText,
|
||||
EditReplyButton,
|
||||
StyledCardComment,
|
||||
} from "./Comments-styles";
|
||||
import { StyledCardHeaderComment } from "./Comments-styles";
|
||||
import { StyledCardColComment } from "./Comments-styles";
|
||||
import { AuthorTextComment } from "./Comments-styles";
|
||||
import {
|
||||
StyledCardContentComment,
|
||||
LoadMoreCommentsButton as CommentActionButton,
|
||||
StyledCardColComment,
|
||||
StyledCardComment,
|
||||
StyledCardContentComment,
|
||||
StyledCardHeaderComment,
|
||||
} from "./Comments-styles";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../state/store";
|
||||
import Portal from "../Portal";
|
||||
import { formatDate } from "../../../utils/time";
|
||||
import { ThemeButton } from "../../../pages/Home/Home-styles.tsx";
|
||||
|
||||
interface CommentProps {
|
||||
comment: any;
|
||||
postId: string;
|
||||
@ -69,12 +68,15 @@ export const Comment = ({
|
||||
onClose={() => setCurrentEdit(null)}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
maxWidth={false}
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title"></DialogTitle>
|
||||
<DialogTitle id="alert-dialog-title" sx={{ fontSize: "30px" }}>
|
||||
Edit Comment
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box
|
||||
sx={{
|
||||
width: "300px",
|
||||
width: "1000px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
@ -90,9 +92,12 @@ export const Comment = ({
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={() => setCurrentEdit(null)}>
|
||||
<ThemeButton
|
||||
variant="contained"
|
||||
onClick={() => setCurrentEdit(null)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</ThemeButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
@ -125,6 +130,7 @@ export const Comment = ({
|
||||
</Typography>
|
||||
)}
|
||||
<CommentActionButtonRow>
|
||||
{user?.name !== comment?.name && (
|
||||
<CommentActionButton
|
||||
size="small"
|
||||
variant="contained"
|
||||
@ -132,6 +138,7 @@ export const Comment = ({
|
||||
>
|
||||
reply
|
||||
</CommentActionButton>
|
||||
)}
|
||||
{user?.name === comment?.name && (
|
||||
<CommentActionButton
|
||||
size="small"
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { Box, Button, TextField } from "@mui/material";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "../../../state/store";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import { setNotification } from "../../../state/features/notificationsSlice";
|
||||
import { toBase64 } from "../../../utils/toBase64";
|
||||
import localforage from "localforage";
|
||||
import {
|
||||
CommentInput,
|
||||
@ -12,6 +10,9 @@ import {
|
||||
SubmitCommentButton,
|
||||
} from "./Comments-styles";
|
||||
import { QSUPPORT_COMMENT_BASE } from "../../../constants/Identifiers.ts";
|
||||
import { sendQchatDM } from "../../../utils/qortalRequests.ts";
|
||||
import { maxCommentLength } from "../../../constants/Misc.ts";
|
||||
|
||||
const uid = new ShortUniqueId();
|
||||
|
||||
const notification = localforage.createInstance({
|
||||
@ -123,7 +124,6 @@ export const CommentEditor = ({
|
||||
let address;
|
||||
let name;
|
||||
let errorMsg = "";
|
||||
|
||||
address = user?.address;
|
||||
name = user?.name || "";
|
||||
|
||||
@ -134,8 +134,8 @@ export const CommentEditor = ({
|
||||
errorMsg = "Cannot post without a name";
|
||||
}
|
||||
|
||||
if (value.length > 200) {
|
||||
errorMsg = "Comment needs to be under 200 characters";
|
||||
if (value.length > maxCommentLength) {
|
||||
errorMsg = `Comment needs to be under ${maxCommentLength} characters`;
|
||||
}
|
||||
|
||||
if (errorMsg) {
|
||||
@ -157,6 +157,7 @@ export const CommentEditor = ({
|
||||
data64: base64,
|
||||
identifier: identifier,
|
||||
});
|
||||
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: "Comment successfully published",
|
||||
@ -171,7 +172,19 @@ export const CommentEditor = ({
|
||||
postName: postName,
|
||||
});
|
||||
}
|
||||
if (!isReply && !isEdit) {
|
||||
// const notificationMessage = `This is an automated Q-Support notification indicating that someone has commented on your issue here:
|
||||
// qortal://APP/Q-Support/issue/${postName}/${postId}
|
||||
//
|
||||
// Here are the first ${maxNotificationLength} characters of the comment:
|
||||
//
|
||||
// ${value.substring(0, maxNotificationLength)}`;
|
||||
|
||||
const notificationMessage = `This is an automated Q-Support notification indicating that someone has commented on your issue here:
|
||||
qortal://APP/Q-Support/issue/${postName}/${postId}`;
|
||||
|
||||
await sendQchatDM(postName, notificationMessage);
|
||||
}
|
||||
return resourceResponse;
|
||||
} catch (error: any) {
|
||||
let notificationObj: any = null;
|
||||
@ -236,11 +249,11 @@ export const CommentEditor = ({
|
||||
id="standard-multiline-flexible"
|
||||
label="Your comment"
|
||||
multiline
|
||||
maxRows={4}
|
||||
maxRows={10}
|
||||
variant="filled"
|
||||
value={value}
|
||||
inputProps={{
|
||||
maxLength: 200,
|
||||
maxLength: maxCommentLength,
|
||||
}}
|
||||
InputLabelProps={{ style: { fontSize: "18px" } }}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
@ -252,3 +265,5 @@ export const CommentEditor = ({
|
||||
</CommentInputContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const sendDMwithComment = () => {};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { styled } from "@mui/system";
|
||||
import { Card, Box, Typography, Button, TextField } from "@mui/material";
|
||||
import { Box, Button, Card, TextField, Typography } from "@mui/material";
|
||||
import { ThemeButton } from "../../../pages/Home/Home-styles.tsx";
|
||||
|
||||
export const StyledCard = styled(Card)(({ theme }) => ({
|
||||
backgroundColor:
|
||||
@ -93,7 +94,7 @@ export const StyledCardComment = styled(Typography)(({ theme }) => ({
|
||||
fontWeight: 400,
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: "19px",
|
||||
wordBreak: "break-word"
|
||||
wordBreak: "break-word",
|
||||
}));
|
||||
|
||||
export const TitleText = styled(Typography)({
|
||||
@ -200,13 +201,10 @@ export const EditReplyButton = styled(Button)(({ theme }) => ({
|
||||
color: "#ffffff",
|
||||
}));
|
||||
|
||||
export const LoadMoreCommentsButton = styled(Button)(({ theme }) => ({
|
||||
export const LoadMoreCommentsButton = styled(ThemeButton)(({ theme }) => ({
|
||||
fontFamily: "Montserrat",
|
||||
fontWeight: 400,
|
||||
letterSpacing: "0.2px",
|
||||
fontSize: "15px",
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: "#ffffff",
|
||||
}));
|
||||
|
||||
export const CommentActionButtonRow = styled(Box)({
|
||||
@ -234,8 +232,7 @@ export const CommentInputContainer = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
marginTop: "15px",
|
||||
width: "90%",
|
||||
maxWidth: "1000px",
|
||||
width: "100%",
|
||||
borderRadius: "8px",
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
@ -270,12 +267,9 @@ export const CommentInput = styled(TextField)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
export const SubmitCommentButton = styled(Button)(({ theme }) => ({
|
||||
export const SubmitCommentButton = styled(ThemeButton)(({ theme }) => ({
|
||||
fontFamily: "Montserrat",
|
||||
fontWeight: 400,
|
||||
letterSpacing: "0.2px",
|
||||
fontSize: "15px",
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: "#ffffff",
|
||||
width: "75%",
|
||||
}));
|
||||
|
@ -11,6 +11,7 @@ export const AddCoverImageButton = styled(Button)(({ theme }) => ({
|
||||
fontWeight: 400,
|
||||
letterSpacing: "0.2px",
|
||||
color: theme.palette.text.primary,
|
||||
width: "170px",
|
||||
backgroundColor: "#44c4ff",
|
||||
"&:hover": { backgroundColor: "#01a9e9" },
|
||||
gap: "5px",
|
||||
|
@ -98,6 +98,7 @@ export const ImageUploader: React.FC<ImageUploaderProps> = ({
|
||||
{...getRootProps()}
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "170px",
|
||||
}}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
|
61
src/components/common/IssueIcon.tsx
Normal file
61
src/components/common/IssueIcon.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import AttachFileIcon from "@mui/icons-material/AttachFile";
|
||||
import React, { CSSProperties } from "react";
|
||||
|
||||
interface IssueIconProps {
|
||||
iconSrc: string;
|
||||
showBackupIcon?: boolean;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
export const IssueIcon = ({
|
||||
iconSrc,
|
||||
showBackupIcon = true,
|
||||
style,
|
||||
}: IssueIconProps) => {
|
||||
const displayFileIcon = !iconSrc && showBackupIcon;
|
||||
|
||||
return (
|
||||
<>
|
||||
{iconSrc && (
|
||||
<img
|
||||
src={iconSrc}
|
||||
width="50px"
|
||||
height="50px"
|
||||
style={{
|
||||
borderRadius: "5px",
|
||||
...style,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{displayFileIcon && (
|
||||
<AttachFileIcon
|
||||
sx={{
|
||||
...style,
|
||||
width: "40px",
|
||||
height: "40px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IssueIconsProps {
|
||||
iconSources: string[];
|
||||
showBackupIcon?: boolean;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const IssueIcons = ({
|
||||
iconSources,
|
||||
showBackupIcon = true,
|
||||
style,
|
||||
}: IssueIconsProps) => {
|
||||
return iconSources.map((icon, index) => (
|
||||
<IssueIcon
|
||||
key={icon + index}
|
||||
iconSrc={icon}
|
||||
style={{ ...style }}
|
||||
showBackupIcon={showBackupIcon}
|
||||
/>
|
||||
));
|
||||
};
|
@ -22,7 +22,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
|
||||
import { DownloadTaskManager } from "../../common/DownloadTaskManager";
|
||||
import QSupportLogo from "../../../assets/img/Q-SupportIcon.webp";
|
||||
import QSupportIcon from "../../../assets/img/Q-SupportIcon(AlphaX).webp";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
addFilteredFiles,
|
||||
@ -32,6 +32,7 @@ import {
|
||||
import { RootState } from "../../../state/store";
|
||||
import { useWindowSize } from "../../../hooks/useWindowSize";
|
||||
import { PublishIssue } from "../../PublishIssue/PublishIssue.tsx";
|
||||
import { FeeHistoryModal } from "../../../constants/PublishFees/FeePricePublish/FeeHistoryModal.tsx";
|
||||
|
||||
interface Props {
|
||||
isAuthenticated: boolean;
|
||||
@ -114,7 +115,7 @@ const NavBar: React.FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={QSupportLogo}
|
||||
src={QSupportIcon}
|
||||
style={{
|
||||
width: "auto",
|
||||
height: "100px",
|
||||
@ -238,6 +239,7 @@ const NavBar: React.FC<Props> = ({
|
||||
</Popover>
|
||||
|
||||
<DownloadTaskManager />
|
||||
<FeeHistoryModal />
|
||||
{theme.palette.mode === "dark" ? (
|
||||
<LightModeIcon
|
||||
onClickFunc={() => setTheme("light")}
|
||||
|
@ -1,48 +0,0 @@
|
||||
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 {
|
||||
Categories,
|
||||
Category,
|
||||
CategoryData,
|
||||
} from "../../components/common/CategoryList/CategoryList.tsx";
|
||||
import {
|
||||
getAllCategoriesWithIcons,
|
||||
sortCategory,
|
||||
} from "./CategoryFunctions.ts";
|
||||
import { QappCategories, SupportState } from "./2ndCategories.ts";
|
||||
|
||||
export const firstCategories: Category[] = [
|
||||
{ id: 1, name: "Core" },
|
||||
{ id: 2, name: "UI" },
|
||||
{ id: 3, name: "Q-Apps" },
|
||||
{ id: 4, name: "Website" },
|
||||
{ id: 5, name: "Marketing" },
|
||||
{ id: 99, name: "Other" },
|
||||
];
|
||||
export const secondCategories: Categories = {
|
||||
1: SupportState,
|
||||
2: SupportState,
|
||||
3: QappCategories,
|
||||
4: SupportState,
|
||||
5: SupportState,
|
||||
99: SupportState,
|
||||
};
|
||||
|
||||
export let thirdCategories: Categories = {};
|
||||
QappCategories.map(
|
||||
supportStateCategory =>
|
||||
(thirdCategories[supportStateCategory.id] = SupportState)
|
||||
);
|
||||
export const allCategoryData: CategoryData = {
|
||||
category: firstCategories,
|
||||
subCategories: [secondCategories, thirdCategories],
|
||||
};
|
||||
|
||||
export const iconCategories = getAllCategoriesWithIcons();
|
@ -1,23 +0,0 @@
|
||||
import OpenIcon from "../../assets/icons/OpenIcon.png";
|
||||
import ClosedIcon from "../../assets/icons/ClosedIcon.png";
|
||||
import InProgressIcon from "../../assets/icons/InProgressIcon.png";
|
||||
import CompleteIcon from "../../assets/icons/CompleteIcon.png";
|
||||
|
||||
export const SupportState = [
|
||||
{ id: 101, name: "Open", icon: OpenIcon },
|
||||
{ id: 102, name: "Closed", icon: ClosedIcon },
|
||||
{ id: 103, name: "In Progress", icon: InProgressIcon },
|
||||
{ id: 104, name: "Complete", icon: CompleteIcon },
|
||||
];
|
||||
|
||||
export const QappCategories = [
|
||||
{ id: 301, name: "Q-Blog" },
|
||||
{ id: 302, name: "Q-Mail" },
|
||||
{ id: 303, name: "Q-Shop" },
|
||||
{ id: 304, name: "Q-Fund" },
|
||||
{ id: 305, name: "Ear-Bump" },
|
||||
{ id: 306, name: "Q-Tube" },
|
||||
{ id: 307, name: "Q-Share" },
|
||||
{ id: 308, name: "Q-Support" },
|
||||
{ id: 399, name: "Other" },
|
||||
];
|
67
src/constants/Categories/Categories.ts
Normal file
67
src/constants/Categories/Categories.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import {
|
||||
Categories,
|
||||
Category,
|
||||
CategoryData,
|
||||
} from "../../components/common/CategoryList/CategoryList.tsx";
|
||||
import { getAllCategoriesWithIcons } from "./CategoryFunctions.ts";
|
||||
import CoreIcon from "../../assets/icons/Qortal-Core-Icon.webp";
|
||||
import UIicon from "../../assets/icons/Qortal-UI-Icon.webp";
|
||||
import QappIcon from "../../assets/icons/Q-App-Icon.webp";
|
||||
import UnknownIcon from "../../assets/icons/unknown.webp";
|
||||
|
||||
import BugReportIcon from "../../assets/icons/Bug-Report-Icon.webp";
|
||||
import FeatureRequestIcon from "../../assets/icons/Feature-Request-Icon.webp";
|
||||
import TechSupportIcon from "../../assets/icons/Tech-Support-Icon.webp";
|
||||
|
||||
import OpenIcon from "../../assets/icons/Open-Icon.webp";
|
||||
import ClosedIcon from "../../assets/icons/Closed-Icon.webp";
|
||||
import InProgressIcon from "../../assets/icons/In-Progress-Icon.webp";
|
||||
import CompleteIcon from "../../assets/icons/Complete-Icon.webp";
|
||||
|
||||
const issueLocationLabel = "Issue Location";
|
||||
export const issueLocation: Category[] = [
|
||||
{ id: 1, name: "Core", icon: CoreIcon, label: issueLocationLabel },
|
||||
{ id: 2, name: "UI", icon: UIicon, label: issueLocationLabel },
|
||||
{ id: 3, name: "Q-Apps/Websites", icon: QappIcon, label: issueLocationLabel },
|
||||
{ id: 99, name: "Other", icon: UnknownIcon, label: issueLocationLabel },
|
||||
];
|
||||
|
||||
const issueTypeLabel = "Issue Type";
|
||||
export const issueType = [
|
||||
{ id: 11, name: "Bug Report", icon: BugReportIcon, label: issueTypeLabel },
|
||||
{
|
||||
id: 12,
|
||||
name: "Feature Request",
|
||||
icon: FeatureRequestIcon,
|
||||
label: issueTypeLabel,
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Tech Support",
|
||||
icon: TechSupportIcon,
|
||||
label: issueTypeLabel,
|
||||
},
|
||||
{ id: 19, name: "Other", icon: UnknownIcon, label: issueTypeLabel },
|
||||
];
|
||||
|
||||
export const secondCategories: Categories = {};
|
||||
issueLocation.map(c => (secondCategories[c.id] = issueType));
|
||||
|
||||
const issueLabel = "Issue State";
|
||||
export const IssueState = [
|
||||
{ id: 101, name: "Open", icon: OpenIcon, label: issueLabel },
|
||||
{ id: 102, name: "Closed", icon: ClosedIcon, label: issueLabel },
|
||||
{ id: 103, name: "In Progress", icon: InProgressIcon, label: issueLabel },
|
||||
{ id: 104, name: "Complete", icon: CompleteIcon, label: issueLabel },
|
||||
];
|
||||
|
||||
export const thirdCategories: Categories = {};
|
||||
|
||||
issueType.map(issueType => (thirdCategories[issueType.id] = IssueState));
|
||||
|
||||
export const allCategoryData: CategoryData = {
|
||||
category: issueLocation,
|
||||
subCategories: [secondCategories, thirdCategories],
|
||||
};
|
||||
|
||||
export const iconCategories = getAllCategoriesWithIcons();
|
@ -2,7 +2,7 @@ import {
|
||||
Category,
|
||||
getCategoriesFromObject,
|
||||
} from "../../components/common/CategoryList/CategoryList.tsx";
|
||||
import { allCategoryData, iconCategories } from "./1stCategories.ts";
|
||||
import { allCategoryData, iconCategories } from "./Categories.ts";
|
||||
|
||||
export const sortCategory = (a: Category, b: Category) => {
|
||||
if (a.name === "Other") return 1;
|
||||
@ -81,11 +81,9 @@ export const getAllCategoriesWithIcons = () => {
|
||||
|
||||
export const getIconsFromObject = (fileObj: any) => {
|
||||
const categories = getCategoriesFromObject(fileObj);
|
||||
const icons = categories
|
||||
.map(categoryID => {
|
||||
const icons = categories.map(categoryID => {
|
||||
return iconCategories.find(category => category.id === +categoryID)?.icon;
|
||||
})
|
||||
.reverse();
|
||||
});
|
||||
|
||||
return icons.find(icon => icon !== undefined);
|
||||
return icons.filter(icon => icon !== undefined);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
const useTestIdentifiers = true;
|
||||
export const useTestIdentifiers = false;
|
||||
|
||||
export const QSUPPORT_FILE_BASE = useTestIdentifiers
|
||||
? "MYTEST_support_issue_"
|
||||
|
@ -3,3 +3,10 @@ export const titleFormatter = /[^a-zA-Z0-9\s-_!?()&'",.;:|—~@#$%^*+=<>]/g;
|
||||
export const titleFormatterOnSave = /[^a-zA-Z0-9\s-_!()&',.;—~@#$%^+=]/g;
|
||||
|
||||
export const log = false;
|
||||
|
||||
export const fontSizeSmall = "80%";
|
||||
export const fontSizeMedium = "100%";
|
||||
export const fontSizeLarge = "120%";
|
||||
export const fontSizeExLarge = "150%";
|
||||
export const maxCommentLength = 10_000;
|
||||
export const maxNotificationLength = 2000;
|
||||
|
29
src/constants/PublishFees/FeeData.tsx
Normal file
29
src/constants/PublishFees/FeeData.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { Box } from "@mui/material";
|
||||
import React from "react";
|
||||
import { useTestIdentifiers } from "../Identifiers.ts";
|
||||
|
||||
export const appName = "Q-Support";
|
||||
export const feeDestinationName = "Q-Support";
|
||||
|
||||
export const feeAmountBase = useTestIdentifiers ? 0.000001 : 0.25;
|
||||
export const FEE_BASE = useTestIdentifiers
|
||||
? "MYTEST_support_fees"
|
||||
: "q_support_fees";
|
||||
|
||||
export const maxFeePublishTimeDiff = 10; // time in minutes before/after publish when fee is considered valid
|
||||
export type FeeType = "default" | "comment" | "like" | "dislike" | "superlike";
|
||||
|
||||
export const feeDisclaimerString = `When Publishing (but not editing) Issues ${feeAmountBase} \n
|
||||
QORT is requested to fund continued development of Q-Support.`;
|
||||
|
||||
export const feeDisclaimer = (
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: "28px",
|
||||
color: "#f44336",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{feeDisclaimerString}
|
||||
</Box>
|
||||
);
|
60
src/constants/PublishFees/FeePricePublish/DataTable.tsx
Normal file
60
src/constants/PublishFees/FeePricePublish/DataTable.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
import { SxProps } from "@mui/material/styles";
|
||||
|
||||
export interface DataTableProps {
|
||||
columnNames: string[];
|
||||
data: string[][];
|
||||
sx?: SxProps;
|
||||
}
|
||||
export const DataTable = ({ columnNames, data, sx }: DataTableProps) => {
|
||||
return (
|
||||
<TableContainer sx={{ ...sx }}>
|
||||
<Table align="center" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columnNames.map((columnName, index) => (
|
||||
<TableCell
|
||||
sx={{
|
||||
fontSize: "30px",
|
||||
textAlign: "center",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
key={columnName + index}
|
||||
>
|
||||
{columnName}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map((tableRow, index) => {
|
||||
return (
|
||||
<TableRow key={tableRow.toString() + index}>
|
||||
{tableRow.map((tableCell, index) => (
|
||||
<TableCell
|
||||
sx={{
|
||||
fontSize: index === 0 ? "30px" : "25px",
|
||||
fontWeight: index === 0 ? "bold" : "normal",
|
||||
textAlign: "center",
|
||||
}}
|
||||
key={tableCell + index}
|
||||
>
|
||||
{tableCell}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
@ -0,0 +1,48 @@
|
||||
import { Button, Modal, useTheme } from "@mui/material";
|
||||
import { ThemeButton } from "../../../pages/Home/Home-styles.tsx";
|
||||
import { appName } from "../FeeData.tsx";
|
||||
import { ModalBody } from "./FeePricePublish-styles.tsx";
|
||||
import { useEffect, useState } from "react";
|
||||
import { userHasName } from "../VerifyPayment-Functions.ts";
|
||||
import { FeeHistoryTable } from "./FeeHistoryTable.tsx";
|
||||
|
||||
export const FeeHistoryModal = () => {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [userOwnsApp, setUserOwnsApp] = useState<boolean>(false);
|
||||
const theme = useTheme();
|
||||
useEffect(() => {
|
||||
userHasName(appName).then(userHasName => setUserOwnsApp(userHasName));
|
||||
}, []);
|
||||
|
||||
const buttonSX = {
|
||||
fontSize: "20px",
|
||||
color: theme.palette.secondary.main,
|
||||
fontWeight: "bold",
|
||||
};
|
||||
if (theme.palette.mode === "light")
|
||||
buttonSX["&:hover"] = { backgroundColor: theme.palette.primary.dark };
|
||||
|
||||
return (
|
||||
<>
|
||||
<ThemeButton
|
||||
sx={{ height: "40px", marginRight: "5px" }}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{appName} Fees
|
||||
</ThemeButton>
|
||||
<Modal
|
||||
open={open}
|
||||
aria-labelledby="modal-title"
|
||||
aria-describedby="modal-description"
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<ModalBody sx={{ width: "75vw", maxWidth: "75vw" }}>
|
||||
<FeeHistoryTable />
|
||||
<Button sx={buttonSX} onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,49 @@
|
||||
import { DataTable } from "./DataTable.tsx";
|
||||
import { FeePrice, fetchFees } from "./FeePricePublish.ts";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
export interface FeeHistoryProps {
|
||||
showFeeType?: boolean;
|
||||
showCoinType?: boolean;
|
||||
filterData?: () => string[][];
|
||||
}
|
||||
export const FeeHistoryTable = ({
|
||||
showFeeType = true,
|
||||
showCoinType = true,
|
||||
filterData,
|
||||
}: FeeHistoryProps) => {
|
||||
const [feeData, setFeeData] = useState<FeePrice[]>([]);
|
||||
|
||||
const fetchFeesOnStartup = () => {
|
||||
fetchFees().then(feeResponse => {
|
||||
setFeeData(filterData ? feeData.filter(filterData) : feeResponse);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(fetchFeesOnStartup, []);
|
||||
|
||||
const columnNames = ["ID", "Date", "Fee Amount"];
|
||||
if (showFeeType) columnNames.push("Fee Type");
|
||||
if (showCoinType) columnNames.push("Coin Type");
|
||||
|
||||
const data: string[][] = [];
|
||||
|
||||
const getRowData = (row: FeePrice, index: number) => {
|
||||
const rowData: string[] = [];
|
||||
rowData.push(
|
||||
index.toString(),
|
||||
new Date(row.time).toDateString(),
|
||||
row.feeAmount.toString()
|
||||
);
|
||||
|
||||
if (showFeeType) rowData.push(row.feeType);
|
||||
if (showCoinType) rowData.push(row.coinType);
|
||||
|
||||
return rowData;
|
||||
};
|
||||
|
||||
feeData.map((row, index) => {
|
||||
data.push(getRowData(row, index + 1));
|
||||
});
|
||||
return <DataTable columnNames={columnNames} data={data} />;
|
||||
};
|
@ -0,0 +1,43 @@
|
||||
import { Box } from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
|
||||
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",
|
||||
},
|
||||
}));
|
90
src/constants/PublishFees/FeePricePublish/FeePricePublish.ts
Normal file
90
src/constants/PublishFees/FeePricePublish/FeePricePublish.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { appName, FEE_BASE, feeAmountBase, FeeType } from "../FeeData.tsx";
|
||||
import { objectToBase64 } from "../../../utils/toBase64.ts";
|
||||
import { store } from "../../../state/store.ts";
|
||||
import { setFeeData } from "../../../state/features/globalSlice.ts";
|
||||
import { useTestIdentifiers } from "../../Identifiers.ts";
|
||||
|
||||
export type CoinType = "QORT" | "BTC" | "LTC" | "DOGE" | "DGB" | "RVN" | "ARRR";
|
||||
|
||||
export interface FeePrice {
|
||||
time: number;
|
||||
feeAmount: number;
|
||||
feeType: FeeType; // used to differentiate different types of fees such as comments, likes, data, etc.
|
||||
coinType: CoinType;
|
||||
}
|
||||
|
||||
const feesPublishService = "DOCUMENT";
|
||||
|
||||
export const fetchFees = async () => {
|
||||
const feeData = store.getState().global.feeData;
|
||||
if (feeData.length > 0) {
|
||||
return feeData;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
identifier: FEE_BASE,
|
||||
name: "Q-Support",
|
||||
service: feesPublishService,
|
||||
});
|
||||
|
||||
return (await response) as FeePrice[];
|
||||
} catch (e) {
|
||||
console.log("fetch current fees error: ", e);
|
||||
return [] as FeePrice[];
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchFeesRedux = () => {
|
||||
const feeData = store.getState().global.feeData;
|
||||
if (feeData.length > 0) {
|
||||
return feeData;
|
||||
}
|
||||
|
||||
fetchFees().then(feeData => store.dispatch(setFeeData(feeData)));
|
||||
};
|
||||
|
||||
export const addFeePrice = async (
|
||||
feeAmount = feeAmountBase,
|
||||
feeType: FeeType = "default",
|
||||
coinType: CoinType = "QORT"
|
||||
) => {
|
||||
let fees = await fetchFees();
|
||||
|
||||
fees.push({
|
||||
time: Date.now(),
|
||||
feeAmount,
|
||||
feeType,
|
||||
coinType,
|
||||
});
|
||||
|
||||
const feesBase64 = await objectToBase64(fees);
|
||||
console.log("fees are: ", fees);
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: appName,
|
||||
identifier: FEE_BASE,
|
||||
service: feesPublishService,
|
||||
data64: feesBase64,
|
||||
});
|
||||
};
|
||||
|
||||
const feeFilter = (fee: FeePrice, feeToVerify: FeePrice) => {
|
||||
const nameCheck = fee.feeType === feeToVerify.feeType;
|
||||
const coinTypeCheck = fee.coinType === feeToVerify.coinType;
|
||||
const timeCheck = feeToVerify.time <= feeToVerify.time;
|
||||
|
||||
return nameCheck && coinTypeCheck && timeCheck;
|
||||
};
|
||||
|
||||
export const verifyFeeAmount = async (feeToVerify: FeePrice) => {
|
||||
if (useTestIdentifiers) return true;
|
||||
|
||||
const fees = await fetchFees();
|
||||
const filteredFees = fees.filter(fee => feeFilter(fee, feeToVerify));
|
||||
if (filteredFees.length === 0) return false;
|
||||
|
||||
const feeToCheck = filteredFees[filteredFees.length - 1]; // gets fee that applies at the time of feeToVerify
|
||||
return feeToVerify.feeAmount >= feeToCheck.feeAmount;
|
||||
};
|
79
src/constants/PublishFees/SendFeeFunctions.ts
Normal file
79
src/constants/PublishFees/SendFeeFunctions.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { feeDestinationName, FeeType } from "./FeeData.tsx";
|
||||
import { CoinType } from "./FeePricePublish/FeePricePublish.ts";
|
||||
|
||||
export interface NameData {
|
||||
name: string;
|
||||
reducedName: string;
|
||||
owner: string;
|
||||
data: string;
|
||||
registered: number;
|
||||
isForSale: boolean;
|
||||
}
|
||||
export const getNameData = async (name: string) => {
|
||||
return qortalRequest({
|
||||
action: "GET_NAME_DATA",
|
||||
name: name,
|
||||
}) as Promise<NameData>;
|
||||
};
|
||||
|
||||
export interface SendCoinResponse {
|
||||
amount: number;
|
||||
approvalStatus: string;
|
||||
fee: string;
|
||||
recipient: string;
|
||||
reference: string;
|
||||
senderPublicKey: string;
|
||||
signature: string;
|
||||
timestamp: number;
|
||||
txGroupId: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export const sendCoin = async (
|
||||
address: string,
|
||||
amount: number,
|
||||
coin: CoinType
|
||||
) => {
|
||||
try {
|
||||
return (await qortalRequest({
|
||||
action: "SEND_COIN",
|
||||
coin,
|
||||
destinationAddress: address,
|
||||
amount,
|
||||
})) as SendCoinResponse;
|
||||
} catch (e) {
|
||||
console.log("sendCoin refused", e);
|
||||
}
|
||||
};
|
||||
|
||||
export const sendQORT = async (address: string, amount: number) => {
|
||||
return await sendCoin(address, amount, "QORT");
|
||||
};
|
||||
|
||||
export const sendQORTtoName = async (name: string, amount: number) => {
|
||||
const address = await getNameData(name);
|
||||
if (address) return await sendQORT(address.owner, amount);
|
||||
else throw Error("Name Not Found");
|
||||
};
|
||||
|
||||
export interface PublishFeeData {
|
||||
signature: string;
|
||||
senderName: string;
|
||||
createdTimestamp?: number; //timestamp of the metadata publish, NOT the send feeAmount publish, added after publish is fetched
|
||||
updatedTimestamp?: number;
|
||||
feeType?: FeeType;
|
||||
coinType?: CoinType;
|
||||
isPaid?: boolean;
|
||||
}
|
||||
|
||||
export type CommentType = "reply" | "edit" | "comment";
|
||||
|
||||
export interface CommentObject {
|
||||
text: string;
|
||||
feeData: PublishFeeData;
|
||||
}
|
||||
|
||||
export const payPublishFeeQORT = async (feeAmount: number) => {
|
||||
const publish = await sendQORTtoName(feeDestinationName, feeAmount);
|
||||
return publish?.signature;
|
||||
};
|
85
src/constants/PublishFees/VerifyPayment-Functions.ts
Normal file
85
src/constants/PublishFees/VerifyPayment-Functions.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { Issue } from "../../state/features/fileSlice.ts";
|
||||
import { PublishFeeData } from "./SendFeeFunctions.ts";
|
||||
|
||||
export type AccountName = { name: string; owner: string };
|
||||
|
||||
export interface GetRequestData {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
reverse?: boolean;
|
||||
}
|
||||
|
||||
export interface getTransactionBySignatureResponse {
|
||||
type: "string";
|
||||
timestamp: number;
|
||||
reference: string;
|
||||
fee: number;
|
||||
signature: string;
|
||||
txGroupId: number;
|
||||
recipient: string;
|
||||
blockHeight: number;
|
||||
approvalStatus: string;
|
||||
creatorAddress: string;
|
||||
senderPublicKey: string;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
export const stringIsEmpty = (value: string) => {
|
||||
return value === "";
|
||||
};
|
||||
|
||||
export const getAccountNames = async (
|
||||
address: string,
|
||||
params?: GetRequestData
|
||||
) => {
|
||||
const names = (await qortalRequest({
|
||||
action: "GET_ACCOUNT_NAMES",
|
||||
address: address,
|
||||
...params,
|
||||
})) as AccountName[];
|
||||
|
||||
const namelessAddress = { name: "", owner: address };
|
||||
const emptyNamesFilled = names.map(({ name, owner }) => {
|
||||
return stringIsEmpty(name) ? namelessAddress : { name, owner };
|
||||
});
|
||||
|
||||
const returnValue =
|
||||
emptyNamesFilled.length > 0 ? emptyNamesFilled : [namelessAddress];
|
||||
return returnValue as AccountName[];
|
||||
};
|
||||
|
||||
export const getUserAccountNames = async () => {
|
||||
const account = await getUserAccount();
|
||||
return await getAccountNames(account.address);
|
||||
};
|
||||
|
||||
export const userHasName = async (name: string) => {
|
||||
const userAccountNames = await getUserAccountNames();
|
||||
const userNames = userAccountNames.map(userName => userName.name);
|
||||
return userNames.includes(name);
|
||||
};
|
||||
|
||||
export const objectToPublishFeeData = (object: Issue) => {
|
||||
const createdTimestamp = +object?.created || 0;
|
||||
const updatedTimestamp = +object?.updated || 0;
|
||||
return {
|
||||
signature: object?.feeData?.signature,
|
||||
createdTimestamp,
|
||||
updatedTimestamp,
|
||||
feeType: object?.feeData?.feeType || "default",
|
||||
coinType: object?.feeData?.coinType || "QORT",
|
||||
senderName: object?.user,
|
||||
isPaid: object?.feeData?.isPaid || false,
|
||||
} as PublishFeeData;
|
||||
};
|
||||
export const objectHasNullValues = (object: object) => {
|
||||
const objectAsArray = Object.values(object);
|
||||
return objectAsArray.some(value => value == null);
|
||||
};
|
||||
|
||||
export type AccountInfo = { address: string; publicKey: string };
|
||||
export const getUserAccount = async () => {
|
||||
return (await qortalRequest({
|
||||
action: "GET_USER_ACCOUNT",
|
||||
})) as AccountInfo;
|
||||
};
|
120
src/constants/PublishFees/VerifyPayment.ts
Normal file
120
src/constants/PublishFees/VerifyPayment.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { feeDestinationName, maxFeePublishTimeDiff } from "./FeeData.tsx";
|
||||
import {
|
||||
getAccountNames,
|
||||
getTransactionBySignatureResponse,
|
||||
objectHasNullValues,
|
||||
objectToPublishFeeData,
|
||||
} from "./VerifyPayment-Functions.ts";
|
||||
import { verifyFeeAmount } from "./FeePricePublish/FeePricePublish.ts";
|
||||
import { getNameData, PublishFeeData } from "./SendFeeFunctions.ts";
|
||||
import { Issue } from "../../state/features/fileSlice.ts";
|
||||
|
||||
const getSignature = async (signature: string) => {
|
||||
const url = "/transactions/signature/" + signature;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
return (await response.json()) as getTransactionBySignatureResponse;
|
||||
};
|
||||
|
||||
const verifySignature = async (feeData: PublishFeeData) => {
|
||||
const {
|
||||
signature,
|
||||
createdTimestamp,
|
||||
updatedTimestamp,
|
||||
feeType,
|
||||
coinType,
|
||||
senderName,
|
||||
} = feeData;
|
||||
|
||||
const [signatureData, accountData] = await Promise.all([
|
||||
getSignature(signature),
|
||||
getNameData(senderName),
|
||||
]);
|
||||
|
||||
const namesofFeeRecipient = await getAccountNames(signatureData.recipient);
|
||||
const doesFeeAmountMatch = await verifyFeeAmount({
|
||||
time: signatureData.timestamp,
|
||||
feeAmount: +signatureData.amount,
|
||||
feeType,
|
||||
coinType,
|
||||
});
|
||||
|
||||
const signatureTime = signatureData.timestamp;
|
||||
let doesTimeMatch: boolean = false;
|
||||
if (!updatedTimestamp) {
|
||||
const timeDiff = createdTimestamp - signatureTime;
|
||||
const timeDiffMinutes = Math.abs(timeDiff) / 1000 / 60;
|
||||
|
||||
doesTimeMatch = timeDiffMinutes <= maxFeePublishTimeDiff;
|
||||
} else {
|
||||
const minutesPublishDiff = 1000 * 60 * maxFeePublishTimeDiff;
|
||||
const startTime = createdTimestamp - minutesPublishDiff;
|
||||
const endTime = updatedTimestamp;
|
||||
|
||||
const sigTimeAfterStartTime = signatureTime > startTime;
|
||||
const sigTimeBeforeEndTime = signatureTime < endTime;
|
||||
|
||||
doesTimeMatch = sigTimeAfterStartTime && sigTimeBeforeEndTime;
|
||||
}
|
||||
|
||||
const doesSignatureMatch = signature === signatureData?.signature;
|
||||
|
||||
const doesSenderMatch = signatureData.creatorAddress === accountData.owner;
|
||||
|
||||
const doesFeeRecipientNameMatch =
|
||||
namesofFeeRecipient.findIndex(
|
||||
nameData => nameData?.name === feeDestinationName
|
||||
) >= 0;
|
||||
|
||||
if (!doesTimeMatch) console.log("Time does not match");
|
||||
if (!doesSignatureMatch) console.log("Signature does not match");
|
||||
if (!doesSenderMatch) console.log("Sender does not match");
|
||||
if (!doesFeeRecipientNameMatch) console.log("Recipient does not match");
|
||||
if (!doesFeeAmountMatch) console.log("FeeAmount does not match");
|
||||
return (
|
||||
doesTimeMatch &&
|
||||
doesSignatureMatch &&
|
||||
doesSenderMatch &&
|
||||
doesFeeRecipientNameMatch &&
|
||||
doesFeeAmountMatch
|
||||
);
|
||||
};
|
||||
|
||||
export const verifyPayment = async (publishToVerify: Issue) => {
|
||||
if (!publishToVerify) return false;
|
||||
|
||||
const publishFeeData = objectToPublishFeeData(publishToVerify);
|
||||
|
||||
if (objectHasNullValues(publishFeeData)) return false;
|
||||
|
||||
const verifyFunctionsList: Promise<boolean>[] = [];
|
||||
|
||||
verifyFunctionsList.push(verifySignature(publishFeeData));
|
||||
|
||||
const paymentChecks = await Promise.all(verifyFunctionsList);
|
||||
return paymentChecks.every(check => check === true);
|
||||
};
|
||||
|
||||
export const appendIsPaidToFeeData = (issue: Issue, isPaid: boolean): Issue => {
|
||||
return {
|
||||
...issue,
|
||||
feeData: {
|
||||
...(issue?.feeData || { signature: undefined, senderName: "" }),
|
||||
isPaid,
|
||||
},
|
||||
};
|
||||
};
|
||||
export const verifyAllPayments = async (issues: Issue[]) => {
|
||||
const verifiedPayments = await Promise.all(
|
||||
issues.map(issue => verifyPayment(issue))
|
||||
);
|
||||
|
||||
return issues.map((issue, index) => {
|
||||
return appendIsPaidToFeeData(issue, verifiedPayments[index]);
|
||||
});
|
||||
};
|
87
src/global.d.ts
vendored
87
src/global.d.ts
vendored
@ -1,55 +1,56 @@
|
||||
// src/global.d.ts
|
||||
interface QortalRequestOptions {
|
||||
action: string
|
||||
name?: string
|
||||
service?: string
|
||||
data64?: string
|
||||
title?: string
|
||||
description?: string
|
||||
category?: string
|
||||
tags?: string[]
|
||||
identifier?: string
|
||||
address?: string
|
||||
metaData?: string
|
||||
encoding?: string
|
||||
includeMetadata?: boolean
|
||||
limit?: numebr
|
||||
offset?: number
|
||||
reverse?: boolean
|
||||
resources?: any[]
|
||||
filename?: string
|
||||
list_name?: string
|
||||
item?: string
|
||||
items?: strings[]
|
||||
tag1?: string
|
||||
tag2?: string
|
||||
tag3?: string
|
||||
tag4?: string
|
||||
tag5?: string
|
||||
coin?: string
|
||||
destinationAddress?: string
|
||||
amount?: number
|
||||
blob?: Blob
|
||||
mimeType?: string
|
||||
file?: File
|
||||
encryptedData?: string
|
||||
name?: string
|
||||
mode?: string
|
||||
query?: string
|
||||
excludeBlocked?: boolean
|
||||
exactMatchNames?: boolean
|
||||
action: string;
|
||||
name?: string;
|
||||
service?: string;
|
||||
data64?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
tags?: string[];
|
||||
identifier?: string;
|
||||
address?: string;
|
||||
metaData?: string;
|
||||
encoding?: string;
|
||||
includeMetadata?: boolean;
|
||||
limit?: numebr;
|
||||
offset?: number;
|
||||
reverse?: boolean;
|
||||
resources?: any[];
|
||||
filename?: string;
|
||||
list_name?: string;
|
||||
item?: string;
|
||||
items?: strings[];
|
||||
tag1?: string;
|
||||
tag2?: string;
|
||||
tag3?: string;
|
||||
tag4?: string;
|
||||
tag5?: string;
|
||||
coin?: string;
|
||||
destinationAddress?: string;
|
||||
amount?: number;
|
||||
blob?: Blob;
|
||||
mimeType?: string;
|
||||
file?: File;
|
||||
encryptedData?: string;
|
||||
name?: string;
|
||||
mode?: string;
|
||||
query?: string;
|
||||
excludeBlocked?: boolean;
|
||||
exactMatchNames?: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
declare function qortalRequest(options: QortalRequestOptions): Promise<any>
|
||||
declare function qortalRequest(options: QortalRequestOptions): Promise<any>;
|
||||
declare function qortalRequestWithTimeout(
|
||||
options: QortalRequestOptions,
|
||||
time: number
|
||||
): Promise<any>
|
||||
): Promise<any>;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
_qdnBase: any // Replace 'any' with the appropriate type if you know it
|
||||
_qdnTheme: string
|
||||
_qdnBase: any; // Replace 'any' with the appropriate type if you know it
|
||||
_qdnTheme: string;
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,6 +58,6 @@ declare global {
|
||||
interface Window {
|
||||
showSaveFilePicker: (
|
||||
options?: SaveFilePickerOptions
|
||||
) => Promise<FileSystemFileHandle>
|
||||
) => Promise<FileSystemFileHandle>;
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,12 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
addFiles,
|
||||
addToHashMap,
|
||||
Issue,
|
||||
removeFromHashMap,
|
||||
setCountNewFiles,
|
||||
upsertFiles,
|
||||
upsertFilesBeginning,
|
||||
upsertFilteredFiles,
|
||||
Video,
|
||||
} from "../state/features/fileSlice.ts";
|
||||
import {
|
||||
setFilesPerNamePublished,
|
||||
@ -24,7 +24,8 @@ import {
|
||||
QSUPPORT_PLAYLIST_BASE,
|
||||
} from "../constants/Identifiers.ts";
|
||||
import { queue } from "../wrappers/GlobalWrapper";
|
||||
import { getCategoriesFetchString } from "../components/common/CategoryList/CategoryList.tsx";
|
||||
import { log } from "../constants/Misc.ts";
|
||||
import { verifyAllPayments } from "../constants/PublishFees/VerifyPayment.ts";
|
||||
|
||||
export const useFetchIssues = () => {
|
||||
const dispatch = useDispatch();
|
||||
@ -50,7 +51,7 @@ export const useFetchIssues = () => {
|
||||
);
|
||||
|
||||
const checkAndUpdateIssue = React.useCallback(
|
||||
(video: Video) => {
|
||||
(video: Issue) => {
|
||||
const existingVideo = hashMapFiles[video.id];
|
||||
if (!existingVideo) {
|
||||
return true;
|
||||
@ -97,10 +98,10 @@ export const useFetchIssues = () => {
|
||||
videoId: issueID,
|
||||
content,
|
||||
});
|
||||
console.log("response is: ", res);
|
||||
res?.isValid
|
||||
? dispatch(addToHashMap(res))
|
||||
: dispatch(removeFromHashMap(issueID));
|
||||
return res;
|
||||
} catch (error) {
|
||||
retries = retries + 1;
|
||||
if (retries < 2) {
|
||||
@ -112,7 +113,7 @@ export const useFetchIssues = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getNewFiles = React.useCallback(async () => {
|
||||
const getNewIssues = React.useCallback(async () => {
|
||||
try {
|
||||
dispatch(setIsLoadingGlobal(true));
|
||||
|
||||
@ -149,7 +150,7 @@ export const useFetchIssues = () => {
|
||||
fetchAll = responseData.slice(0, findVideo);
|
||||
}
|
||||
|
||||
const structureData = fetchAll.map((video: any): Video => {
|
||||
const structureData = fetchAll.map((video: any): Issue => {
|
||||
return {
|
||||
title: video?.metadata?.title,
|
||||
category: video?.metadata?.category,
|
||||
@ -186,20 +187,21 @@ export const useFetchIssues = () => {
|
||||
}
|
||||
}, [videos, hashMapFiles]);
|
||||
|
||||
const getFiles = React.useCallback(
|
||||
const getIssues = React.useCallback(
|
||||
async (
|
||||
filters = {},
|
||||
reset?: boolean,
|
||||
resetFilers?: boolean,
|
||||
resetFilters?: boolean,
|
||||
limit?: number
|
||||
) => {
|
||||
try {
|
||||
const {
|
||||
name = "",
|
||||
categories = [],
|
||||
categories = "",
|
||||
QappName = "",
|
||||
keywords = "",
|
||||
type = "",
|
||||
}: any = resetFilers ? {} : filters;
|
||||
}: any = resetFilters ? {} : filters;
|
||||
let offset = videos.length;
|
||||
if (reset) {
|
||||
offset = 0;
|
||||
@ -211,10 +213,17 @@ export const useFetchIssues = () => {
|
||||
defaultUrl += `&name=${name}`;
|
||||
}
|
||||
|
||||
if (categories.length > 0) {
|
||||
defaultUrl += "&description=" + getCategoriesFetchString(categories);
|
||||
}
|
||||
if (categories) {
|
||||
defaultUrl += "&description=";
|
||||
if (log) console.log("categories: ", categories);
|
||||
if (categories) defaultUrl += categories;
|
||||
|
||||
if (log) console.log("description: ", defaultUrl);
|
||||
}
|
||||
if (QappName) {
|
||||
defaultUrl += `&query=${QappName}`;
|
||||
}
|
||||
if (log) console.log("defaultURL: ", defaultUrl);
|
||||
if (keywords) {
|
||||
defaultUrl = defaultUrl + `&query=${keywords}`;
|
||||
}
|
||||
@ -236,47 +245,48 @@ export const useFetchIssues = () => {
|
||||
});
|
||||
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 => {
|
||||
let structureData = responseData.map((issue: any): Issue => {
|
||||
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,
|
||||
title: issue?.metadata?.title,
|
||||
service: issue?.service,
|
||||
category: issue?.metadata?.category,
|
||||
categoryName: issue?.metadata?.categoryName,
|
||||
tags: issue?.metadata?.tags || [],
|
||||
description: issue?.metadata?.description,
|
||||
created: issue?.created,
|
||||
updated: issue?.updated,
|
||||
user: issue.name,
|
||||
videoImage: "",
|
||||
id: video.identifier,
|
||||
id: issue.identifier,
|
||||
};
|
||||
});
|
||||
if (reset) {
|
||||
dispatch(addFiles(structureData));
|
||||
} else {
|
||||
dispatch(upsertFiles(structureData));
|
||||
}
|
||||
const verifiedIssuePromises: Promise<Issue>[] = [];
|
||||
for (const content of structureData) {
|
||||
if (content.user && content.id) {
|
||||
const res = checkAndUpdateIssue(content);
|
||||
const issue: Promise<Issue> = getIssue(
|
||||
content.user,
|
||||
content.id,
|
||||
content
|
||||
);
|
||||
verifiedIssuePromises.push(issue);
|
||||
if (res) {
|
||||
queue.push(() => getIssue(content.user, content.id, content));
|
||||
queue.push(() => issue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const issues = await Promise.all(verifiedIssuePromises);
|
||||
const verifiedIssues = await verifyAllPayments(issues);
|
||||
structureData = structureData.map((issue, index) => {
|
||||
return {
|
||||
...issue,
|
||||
feeData: verifiedIssues[index]?.feeData,
|
||||
};
|
||||
});
|
||||
|
||||
if (reset) dispatch(addFiles(structureData));
|
||||
else dispatch(upsertFiles(structureData));
|
||||
} catch (error) {
|
||||
console.log({ error });
|
||||
} finally {
|
||||
@ -285,7 +295,7 @@ export const useFetchIssues = () => {
|
||||
[videos, hashMapFiles]
|
||||
);
|
||||
|
||||
const getFilesFiltered = React.useCallback(
|
||||
const getIssuesFiltered = React.useCallback(
|
||||
async (filterValue: string) => {
|
||||
try {
|
||||
const offset = filteredVideos.length;
|
||||
@ -314,7 +324,7 @@ export const useFetchIssues = () => {
|
||||
// exactMatchNames: true,
|
||||
// name: names
|
||||
// })
|
||||
const structureData = responseData.map((video: any): Video => {
|
||||
const structureData = responseData.map((video: any): Issue => {
|
||||
return {
|
||||
title: video?.metadata?.title,
|
||||
category: video?.metadata?.category,
|
||||
@ -345,7 +355,7 @@ export const useFetchIssues = () => {
|
||||
[filteredVideos, hashMapFiles]
|
||||
);
|
||||
|
||||
const checkNewFiles = React.useCallback(async () => {
|
||||
const checkNewIssues = React.useCallback(async () => {
|
||||
try {
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QSUPPORT_FILE_BASE}&limit=20&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true`;
|
||||
const response = await fetch(url, {
|
||||
@ -382,7 +392,7 @@ export const useFetchIssues = () => {
|
||||
} catch (error) {}
|
||||
}, [videos]);
|
||||
|
||||
const getFilesCount = React.useCallback(async () => {
|
||||
const getIssuesCount = React.useCallback(async () => {
|
||||
try {
|
||||
let url = `/arbitrary/resources/search?mode=ALL&includemetadata=false&limit=0&service=DOCUMENT&identifier=${QSUPPORT_FILE_BASE}`;
|
||||
|
||||
@ -411,13 +421,13 @@ export const useFetchIssues = () => {
|
||||
}, []);
|
||||
|
||||
return {
|
||||
getFiles,
|
||||
checkAndUpdateFile: checkAndUpdateIssue,
|
||||
getFile: getIssue,
|
||||
getIssues,
|
||||
checkAndUpdateIssue,
|
||||
getIssue,
|
||||
hashMapFiles,
|
||||
getNewFiles,
|
||||
checkNewFiles,
|
||||
getFilesFiltered,
|
||||
getFilesCount,
|
||||
getNewIssues,
|
||||
checkNewIssues,
|
||||
getIssuesFiltered,
|
||||
getIssuesCount,
|
||||
};
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../state/store";
|
||||
import { Box, useTheme } from "@mui/material";
|
||||
import { FileContainer } from "./IssueList-styles.tsx";
|
||||
import { IssueContainer } from "./IssueList-styles.tsx";
|
||||
import ResponsiveImage from "../../components/ResponsiveImage";
|
||||
import { ChannelCard, ChannelTitle } from "./Home-styles";
|
||||
|
||||
@ -30,7 +30,7 @@ export const Channels = ({ mode }: VideoListProps) => {
|
||||
minHeight: "50vh",
|
||||
}}
|
||||
>
|
||||
<FileContainer>
|
||||
<IssueContainer>
|
||||
{publishNames &&
|
||||
publishNames?.slice(0, 10).map(name => {
|
||||
let avatarUrl = "";
|
||||
@ -62,7 +62,7 @@ export const Channels = ({ mode }: VideoListProps) => {
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</FileContainer>
|
||||
</IssueContainer>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { styled } from "@mui/system";
|
||||
import { Box, Button, Grid, Typography } from "@mui/material";
|
||||
import { fontSizeSmall } from "../../constants/Misc.ts";
|
||||
|
||||
export const SubtitleContainer = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
@ -88,12 +89,12 @@ export const ThemeButton = styled(Button)(({ theme }) => ({
|
||||
color: theme.palette.text.primary,
|
||||
backgroundColor: "#01a9e9",
|
||||
fontSize: "18px",
|
||||
"&:hover": { backgroundColor: "#3e74c1" },
|
||||
"&:hover": { backgroundColor: "#008fcd" },
|
||||
}));
|
||||
|
||||
export const ThemeButtonBright = styled(Button)(({ theme }) => ({
|
||||
color: theme.palette.text.primary,
|
||||
backgroundColor: "#44c4ff",
|
||||
fontSize: "18px",
|
||||
fontSize: fontSizeSmall,
|
||||
"&:hover": { backgroundColor: "#01a9e9" },
|
||||
}));
|
||||
|
@ -12,40 +12,48 @@ import {
|
||||
changefilterName,
|
||||
changefilterSearch,
|
||||
changeFilterType,
|
||||
setQappNames,
|
||||
} from "../../state/features/fileSlice.ts";
|
||||
import { allCategoryData } from "../../constants/Categories/1stCategories.ts";
|
||||
import {
|
||||
allCategoryData,
|
||||
IssueState,
|
||||
} from "../../constants/Categories/Categories.ts";
|
||||
import {
|
||||
CategoryList,
|
||||
CategoryListRef,
|
||||
getCategoriesFetchString,
|
||||
} from "../../components/common/CategoryList/CategoryList.tsx";
|
||||
import { StatsData } from "../../components/StatsData.tsx";
|
||||
import {
|
||||
CategorySelect,
|
||||
CategorySelectRef,
|
||||
} from "../../components/common/CategoryList/CategorySelect.tsx";
|
||||
import {
|
||||
AutocompleteQappNames,
|
||||
getPublishedQappNames,
|
||||
QappNamesRef,
|
||||
} from "../../components/common/AutocompleteQappNames.tsx";
|
||||
|
||||
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 QappNames = useSelector(
|
||||
(state: RootState) => state.file.publishedQappNames
|
||||
);
|
||||
const autocompleteRef = useRef<QappNamesRef>(null);
|
||||
|
||||
const setFilterSearch = payload => {
|
||||
dispatch(changefilterSearch(payload));
|
||||
@ -59,60 +67,66 @@ export const Home = ({ mode }: HomeProps) => {
|
||||
const isFilterMode = useRef(false);
|
||||
const firstFetch = useRef(false);
|
||||
const afterFetch = useRef(false);
|
||||
const isFetchingFiltered = useRef(false);
|
||||
const isFetching = useRef(false);
|
||||
const prevVal = useRef("");
|
||||
const categoryListRef = useRef<CategoryListRef>(null);
|
||||
const categorySelectRef = useRef<CategorySelectRef>(null);
|
||||
|
||||
const countNewFiles = useSelector(
|
||||
(state: RootState) => state.file.countNewFiles
|
||||
);
|
||||
const userAvatarHash = useSelector(
|
||||
(state: RootState) => state.global.userAvatarHash
|
||||
);
|
||||
|
||||
const [showCategoryList, setShowCategoryList] = useState<boolean>(true);
|
||||
const [showCategorySelect, setShowCategorySelect] = useState<boolean>(true);
|
||||
const { files: globalVideos } = useSelector((state: RootState) => state.file);
|
||||
|
||||
const setSelectedCategoryFiles = payload => {};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const filteredFiles = useSelector(
|
||||
(state: RootState) => state.file.filteredFiles
|
||||
);
|
||||
|
||||
const [QappNamesParam, setQappNamesParam] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
getPublishedQappNames().then(QappNamesResult => {
|
||||
dispatch(setQappNames(QappNamesResult));
|
||||
setQappNamesParam(QappNamesResult);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const {
|
||||
getFiles,
|
||||
checkAndUpdateFile,
|
||||
getFile,
|
||||
getIssues,
|
||||
checkAndUpdateIssue,
|
||||
getIssue,
|
||||
hashMapFiles,
|
||||
getNewFiles,
|
||||
checkNewFiles,
|
||||
getFilesFiltered,
|
||||
getFilesCount,
|
||||
getNewIssues,
|
||||
checkNewIssues,
|
||||
getIssuesFiltered,
|
||||
getIssuesCount,
|
||||
} = useFetchIssues();
|
||||
|
||||
const getFilesHandler = React.useCallback(
|
||||
async (reset?: boolean, resetFilers?: boolean) => {
|
||||
const getIssuesHandler = React.useCallback(
|
||||
async (reset?: boolean, resetFilters?: boolean) => {
|
||||
if (!firstFetch.current || !afterFetch.current) return;
|
||||
if (isFetching.current) return;
|
||||
isFetching.current = true;
|
||||
const selectedCategories =
|
||||
categoryListRef.current.getSelectedCategories() || [];
|
||||
|
||||
await getFiles(
|
||||
categoryListRef.current?.getSelectedCategories() || [];
|
||||
const issueType = categorySelectRef?.current?.getSelectedCategory();
|
||||
if (issueType) selectedCategories[2] = issueType;
|
||||
await getIssues(
|
||||
{
|
||||
name: filterName,
|
||||
categories: selectedCategories,
|
||||
categories: getCategoriesFetchString(selectedCategories),
|
||||
QappName: autocompleteRef?.current?.getQappNameFetchString(),
|
||||
keywords: filterSearch,
|
||||
type: filterType,
|
||||
},
|
||||
reset,
|
||||
resetFilers
|
||||
resetFilters
|
||||
);
|
||||
isFetching.current = false;
|
||||
},
|
||||
[
|
||||
getFiles,
|
||||
getIssues,
|
||||
filterValue,
|
||||
getFilesFiltered,
|
||||
getIssuesFiltered,
|
||||
isFiltering,
|
||||
filterName,
|
||||
filterSearch,
|
||||
@ -122,33 +136,33 @@ export const Home = ({ mode }: HomeProps) => {
|
||||
|
||||
const searchOnEnter = e => {
|
||||
if (e.keyCode == 13) {
|
||||
getFilesHandler(true);
|
||||
getIssuesHandler(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isFiltering && filterValue !== prevVal?.current) {
|
||||
prevVal.current = filterValue;
|
||||
getFilesHandler();
|
||||
getIssuesHandler();
|
||||
}
|
||||
}, [filterValue, isFiltering, filteredFiles, getFilesCount]);
|
||||
}, [filterValue, isFiltering, filteredFiles, getIssuesCount]);
|
||||
|
||||
const getFilesHandlerMount = React.useCallback(async () => {
|
||||
if (firstFetch.current) return;
|
||||
firstFetch.current = true;
|
||||
setIsLoading(true);
|
||||
|
||||
await getFiles();
|
||||
await getIssues();
|
||||
afterFetch.current = true;
|
||||
isFetching.current = false;
|
||||
|
||||
setIsLoading(false);
|
||||
}, [getFiles]);
|
||||
}, [getIssues]);
|
||||
|
||||
let videos = globalVideos;
|
||||
let issues = globalVideos;
|
||||
|
||||
if (isFiltering) {
|
||||
videos = filteredFiles;
|
||||
issues = filteredFiles;
|
||||
isFilterMode.current = true;
|
||||
} else {
|
||||
isFilterMode.current = false;
|
||||
@ -199,9 +213,10 @@ export const Home = ({ mode }: HomeProps) => {
|
||||
setFilterSearch("");
|
||||
setFilterName("");
|
||||
categoryListRef.current?.clearCategories();
|
||||
|
||||
categorySelectRef.current?.clearCategory();
|
||||
autocompleteRef.current?.setSelectedValue(null);
|
||||
ReactDOM.flushSync(() => {
|
||||
getFilesHandler(true, true);
|
||||
getIssuesHandler(true, true);
|
||||
});
|
||||
};
|
||||
|
||||
@ -268,7 +283,43 @@ export const Home = ({ mode }: HomeProps) => {
|
||||
fontSize: "20px",
|
||||
}}
|
||||
/>
|
||||
<CategoryList categoryData={allCategoryData} ref={categoryListRef} />
|
||||
{showCategoryList && (
|
||||
<CategoryList
|
||||
categoryData={allCategoryData}
|
||||
ref={categoryListRef}
|
||||
afterChange={value => {
|
||||
setShowCategorySelect(!value[0]);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showCategorySelect && (
|
||||
<CategorySelect
|
||||
categoryData={IssueState}
|
||||
ref={categorySelectRef}
|
||||
sx={{ marginTop: "20px" }}
|
||||
afterChange={value => {
|
||||
setShowCategoryList(!value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{QappNamesParam.length > 0 && (
|
||||
<AutocompleteQappNames
|
||||
ref={autocompleteRef}
|
||||
namesList={QappNamesParam}
|
||||
sx={{ marginTop: "20px" }}
|
||||
required={false}
|
||||
afterChange={() => {
|
||||
const currentSelectedCategories =
|
||||
categoryListRef?.current?.getSelectedCategories();
|
||||
categoryListRef?.current?.setSelectedCategories([
|
||||
"3",
|
||||
currentSelectedCategories[1],
|
||||
currentSelectedCategories[2],
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ThemeButton
|
||||
onClick={() => {
|
||||
@ -284,7 +335,7 @@ export const Home = ({ mode }: HomeProps) => {
|
||||
</ThemeButton>
|
||||
<ThemeButton
|
||||
onClick={() => {
|
||||
getFilesHandler(true);
|
||||
getIssuesHandler(true);
|
||||
}}
|
||||
sx={{
|
||||
marginTop: "20px",
|
||||
@ -314,9 +365,9 @@ export const Home = ({ mode }: HomeProps) => {
|
||||
maxWidth: "1400px",
|
||||
}}
|
||||
></SubtitleContainer>
|
||||
<IssueList issues={videos} />
|
||||
<IssueList issues={issues} />
|
||||
<LazyLoad
|
||||
onLoadMore={getFilesHandler}
|
||||
onLoadMore={getIssuesHandler}
|
||||
isLoading={isLoading}
|
||||
></LazyLoad>
|
||||
</Box>
|
||||
|
@ -8,8 +8,9 @@ import {
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { fontSizeMedium, fontSizeSmall } from "../../constants/Misc.ts";
|
||||
|
||||
export const FileContainer = styled(Box)(({ theme }) => ({
|
||||
export const IssueContainer = styled(Box)(({ theme }) => ({
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
padding: "15px",
|
||||
@ -33,7 +34,7 @@ export const StoresRow = styled(Grid)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
export const VideoCard = styled(Grid)(({ theme }) => ({
|
||||
export const IssueCard = styled(Grid)(({ theme }) => ({
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
@ -89,14 +90,14 @@ const DoubleLine = styled(Typography)`
|
||||
|
||||
export const VideoCardTitle = styled(DoubleLine)(({ theme }) => ({
|
||||
fontFamily: "Cairo",
|
||||
fontSize: "16px",
|
||||
fontSize: fontSizeMedium,
|
||||
letterSpacing: "0.4px",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
}));
|
||||
export const VideoCardName = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Cairo",
|
||||
fontSize: "14px",
|
||||
fontSize: fontSizeSmall,
|
||||
letterSpacing: "0.4px",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
@ -107,14 +108,16 @@ export const VideoCardName = styled(Typography)(({ theme }) => ({
|
||||
}));
|
||||
export const VideoUploadDate = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Cairo",
|
||||
fontSize: "12px",
|
||||
display: "span",
|
||||
fontSize: fontSizeSmall,
|
||||
letterSpacing: "0.4px",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
}));
|
||||
export const BottomParent = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
justifyItems: "flex-end",
|
||||
alignItems: "flex-end",
|
||||
flexDirection: "column",
|
||||
}));
|
||||
export const VideoCardDescription = styled(Typography)(({ theme }) => ({
|
||||
@ -155,11 +158,11 @@ export const MyStoresRow = styled(Grid)(({ theme }) => ({
|
||||
width: "100%",
|
||||
}));
|
||||
|
||||
export const NameContainer = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
export const NameAndDateContainer = styled(Box)(({ theme }) => ({
|
||||
display: "grid",
|
||||
gridTemplateRows: "1fr 1fr",
|
||||
justifyContent: "end",
|
||||
alignContent: "center",
|
||||
gap: "10px",
|
||||
marginBottom: "10px",
|
||||
}));
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { Avatar, Box, Skeleton } from "@mui/material";
|
||||
import {
|
||||
BlockIconContainer,
|
||||
BottomParent,
|
||||
FileContainer,
|
||||
IconsBox,
|
||||
NameContainer,
|
||||
VideoCard,
|
||||
IssueCard,
|
||||
IssueContainer,
|
||||
NameAndDateContainer,
|
||||
VideoCardName,
|
||||
VideoCardTitle,
|
||||
VideoUploadDate,
|
||||
@ -13,11 +12,10 @@ import {
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import {
|
||||
blockUser,
|
||||
Issue,
|
||||
setEditFile,
|
||||
Video,
|
||||
} from "../../state/features/fileSlice.ts";
|
||||
import BlockIcon from "@mui/icons-material/Block";
|
||||
import AttachFileIcon from "@mui/icons-material/AttachFile";
|
||||
import { formatBytes } from "../IssueContent/IssueContent.tsx";
|
||||
import { formatDate } from "../../utils/time.ts";
|
||||
import React, { useMemo, useState } from "react";
|
||||
@ -26,8 +24,12 @@ import { RootState } from "../../state/store.ts";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts";
|
||||
|
||||
import { IssueIcon, IssueIcons } from "../../components/common/IssueIcon.tsx";
|
||||
import QORTicon from "../../assets/icons/qort.png";
|
||||
import { fontSizeMedium } from "../../constants/Misc.ts";
|
||||
|
||||
interface FileListProps {
|
||||
issues: Video[];
|
||||
issues: Issue[];
|
||||
}
|
||||
export const IssueList = ({ issues }: FileListProps) => {
|
||||
const hashMapIssues = useSelector(
|
||||
@ -60,16 +62,21 @@ export const IssueList = ({ issues }: FileListProps) => {
|
||||
}, [issues, hashMapIssues]);
|
||||
|
||||
return (
|
||||
<FileContainer>
|
||||
{filteredIssues.map((file: any, index: number) => {
|
||||
const existingFile = hashMapIssues[file?.id];
|
||||
<IssueContainer>
|
||||
{filteredIssues.map((issue: any, index: number) => {
|
||||
const existingFile = hashMapIssues[issue?.id];
|
||||
let hasHash = false;
|
||||
let fileObj = file;
|
||||
let issueObj = issue;
|
||||
if (existingFile) {
|
||||
fileObj = existingFile;
|
||||
issueObj = existingFile;
|
||||
hasHash = true;
|
||||
}
|
||||
const icon = getIconsFromObject(fileObj);
|
||||
|
||||
const issueIcons = getIconsFromObject(issueObj);
|
||||
const fileBytes = issueObj?.files.reduce(
|
||||
(acc, cur) => acc + (cur?.size || 0),
|
||||
0
|
||||
);
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -79,22 +86,22 @@ export const IssueList = ({ issues }: FileListProps) => {
|
||||
height: "75px",
|
||||
position: "relative",
|
||||
}}
|
||||
key={fileObj.id}
|
||||
onMouseEnter={() => setShowIcons(fileObj.id)}
|
||||
key={issueObj.id}
|
||||
onMouseEnter={() => setShowIcons(issueObj.id)}
|
||||
onMouseLeave={() => setShowIcons(null)}
|
||||
>
|
||||
{hasHash ? (
|
||||
<>
|
||||
<IconsBox
|
||||
sx={{
|
||||
opacity: showIcons === fileObj.id ? 1 : 0,
|
||||
opacity: showIcons === issueObj.id ? 1 : 0,
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{fileObj?.user === username && (
|
||||
{issueObj?.user === username && (
|
||||
<BlockIconContainer
|
||||
onClick={() => {
|
||||
dispatch(setEditFile(fileObj));
|
||||
dispatch(setEditFile(issueObj));
|
||||
}}
|
||||
>
|
||||
<EditIcon />
|
||||
@ -102,10 +109,10 @@ export const IssueList = ({ issues }: FileListProps) => {
|
||||
</BlockIconContainer>
|
||||
)}
|
||||
|
||||
{fileObj?.user !== username && (
|
||||
{issueObj?.user !== username && (
|
||||
<BlockIconContainer
|
||||
onClick={() => {
|
||||
blockUserFunc(fileObj?.user);
|
||||
blockUserFunc(issueObj?.user);
|
||||
}}
|
||||
>
|
||||
<BlockIcon />
|
||||
@ -113,15 +120,14 @@ export const IssueList = ({ issues }: FileListProps) => {
|
||||
</BlockIconContainer>
|
||||
)}
|
||||
</IconsBox>
|
||||
<VideoCard
|
||||
<IssueCard
|
||||
onClick={() => {
|
||||
navigate(`/issue/${fileObj?.user}/${fileObj?.id}`);
|
||||
navigate(`/issue/${issueObj?.user}/${issueObj?.id}`);
|
||||
}}
|
||||
sx={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
gap: "25px",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
@ -129,47 +135,59 @@ export const IssueList = ({ issues }: FileListProps) => {
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "25px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{icon ? (
|
||||
<img
|
||||
src={icon}
|
||||
width="50px"
|
||||
<div
|
||||
style={{
|
||||
borderRadius: "5px",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<AttachFileIcon />
|
||||
)}
|
||||
|
||||
<VideoCardTitle
|
||||
sx={{
|
||||
width: "100px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "200px",
|
||||
}}
|
||||
>
|
||||
{formatBytes(
|
||||
fileObj?.files.reduce(
|
||||
(acc, cur) => acc + (cur?.size || 0),
|
||||
0
|
||||
)
|
||||
)}
|
||||
<IssueIcons
|
||||
iconSources={issueIcons}
|
||||
style={{ marginRight: "20px" }}
|
||||
showBackupIcon={true}
|
||||
/>
|
||||
</div>
|
||||
<VideoCardTitle
|
||||
sx={{
|
||||
width: "150px",
|
||||
fontSize: fontSizeMedium,
|
||||
}}
|
||||
>
|
||||
{fileBytes > 0 && formatBytes(fileBytes)}
|
||||
</VideoCardTitle>
|
||||
<VideoCardTitle sx={{ fontWeight: "bold", width: "500px" }}>
|
||||
{issueObj.title}
|
||||
</VideoCardTitle>
|
||||
<VideoCardTitle>{fileObj.title}</VideoCardTitle>
|
||||
</Box>
|
||||
<BottomParent>
|
||||
<NameContainer
|
||||
|
||||
{issue?.feeData?.isPaid && (
|
||||
<IssueIcon
|
||||
iconSrc={QORTicon}
|
||||
style={{ marginRight: "20px" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<NameAndDateContainer
|
||||
sx={{ width: "200px", height: "100%" }}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
navigate(`/channel/${fileObj?.user}`);
|
||||
navigate(`/channel/${issueObj?.user}`);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
width: "200px",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{ height: 24, width: 24 }}
|
||||
src={`/arbitrary/THUMBNAIL/${fileObj?.user}/qortal_avatar`}
|
||||
alt={`${fileObj?.user}'s avatar`}
|
||||
sx={{ height: 24, width: 24, marginRight: "10px" }}
|
||||
src={`/arbitrary/THUMBNAIL/${issueObj?.user}/qortal_avatar`}
|
||||
alt={`${issueObj?.user}'s avatar`}
|
||||
/>
|
||||
<VideoCardName
|
||||
sx={{
|
||||
@ -178,17 +196,17 @@ export const IssueList = ({ issues }: FileListProps) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{fileObj?.user}
|
||||
{issueObj?.user}
|
||||
</VideoCardName>
|
||||
</NameContainer>
|
||||
</div>
|
||||
|
||||
{fileObj?.created && (
|
||||
{issueObj?.created && (
|
||||
<VideoUploadDate>
|
||||
{formatDate(fileObj.created)}
|
||||
{formatDate(issueObj.created)}
|
||||
</VideoUploadDate>
|
||||
)}
|
||||
</BottomParent>
|
||||
</VideoCard>
|
||||
</NameAndDateContainer>
|
||||
</IssueCard>
|
||||
</>
|
||||
) : (
|
||||
<Skeleton
|
||||
@ -206,6 +224,6 @@ export const IssueList = ({ issues }: FileListProps) => {
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</FileContainer>
|
||||
</IssueContainer>
|
||||
);
|
||||
};
|
||||
|
@ -2,31 +2,33 @@ 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 { useFetchIssues } from "../../hooks/useFetchIssues.tsx";
|
||||
import LazyLoad from "../../components/common/LazyLoad";
|
||||
import {
|
||||
BottomParent,
|
||||
FileContainer,
|
||||
NameContainer,
|
||||
VideoCard,
|
||||
IssueCard,
|
||||
IssueContainer,
|
||||
NameAndDateContainer,
|
||||
VideoCardName,
|
||||
VideoCardTitle,
|
||||
VideoUploadDate,
|
||||
} from "./IssueList-styles.tsx";
|
||||
import { formatDate } from "../../utils/time";
|
||||
import { Video } from "../../state/features/fileSlice.ts";
|
||||
import { Issue } from "../../state/features/fileSlice.ts";
|
||||
import { queue } from "../../wrappers/GlobalWrapper";
|
||||
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
||||
import { formatBytes } from "../IssueContent/IssueContent.tsx";
|
||||
import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts";
|
||||
import { IssueIcon, IssueIcons } from "../../components/common/IssueIcon.tsx";
|
||||
import QORTicon from "../../assets/icons/qort.png";
|
||||
import { verifyAllPayments } from "../../constants/PublishFees/VerifyPayment.ts";
|
||||
|
||||
interface VideoListProps {
|
||||
mode?: string;
|
||||
}
|
||||
export const FileListComponentLevel = ({ mode }: VideoListProps) => {
|
||||
export const IssueListComponentLevel = ({ mode }: VideoListProps) => {
|
||||
const { name: paramName } = useParams();
|
||||
const theme = useTheme();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
@ -37,16 +39,17 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
|
||||
(state: RootState) => state.file.hashMapFiles
|
||||
);
|
||||
|
||||
const [videos, setVideos] = React.useState<Video[]>([]);
|
||||
const [issues, setIssues] = React.useState<Issue[]>([]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { getFile, getNewFiles, checkNewFiles, checkAndUpdateFile } =
|
||||
const { getIssue, getNewIssues, checkNewIssues, checkAndUpdateIssue } =
|
||||
useFetchIssues();
|
||||
|
||||
const getVideos = React.useCallback(async () => {
|
||||
const getIssues = React.useCallback(async () => {
|
||||
try {
|
||||
const offset = videos.length;
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QSUPPORT_FILE_BASE}_&limit=50&includemetadata=false&reverse=true&excludeblocked=true&name=${paramName}&exactmatchnames=true&offset=${offset}`;
|
||||
const offset = issues.length;
|
||||
// `/arbitrary/resources/search?mode=ALL&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}&limit=${videoLimit}`;
|
||||
const url = `/arbitrary/resources/search?mode=ALL&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}&limit=50&service=DOCUMENT&query=${QSUPPORT_FILE_BASE}_&name=${paramName}`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
@ -55,63 +58,69 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
|
||||
});
|
||||
const responseData = await response.json();
|
||||
|
||||
const structureData = responseData.map((video: any): Video => {
|
||||
const structureData = responseData.map((issue: any): Issue => {
|
||||
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,
|
||||
title: issue?.metadata?.title,
|
||||
category: issue?.metadata?.category,
|
||||
categoryName: issue?.metadata?.categoryName,
|
||||
tags: issue?.metadata?.tags || [],
|
||||
description: issue?.metadata?.description,
|
||||
created: issue?.created,
|
||||
updated: issue?.updated,
|
||||
user: issue.name,
|
||||
videoImage: "",
|
||||
id: video.identifier,
|
||||
id: issue.identifier,
|
||||
};
|
||||
});
|
||||
|
||||
const copiedVideos: Video[] = [...videos];
|
||||
structureData.forEach((video: Video) => {
|
||||
const index = videos.findIndex(p => p.id === video.id);
|
||||
const copiedIssues: Issue[] = [...issues];
|
||||
structureData.forEach((issue: Issue) => {
|
||||
const index = issues.findIndex(p => p.id === issue.id);
|
||||
if (index !== -1) {
|
||||
copiedVideos[index] = video;
|
||||
copiedIssues[index] = issue;
|
||||
} else {
|
||||
copiedVideos.push(video);
|
||||
copiedIssues.push(issue);
|
||||
}
|
||||
});
|
||||
setVideos(copiedVideos);
|
||||
|
||||
for (const content of structureData) {
|
||||
const verifiedIssuePromises: Promise<Issue>[] = [];
|
||||
|
||||
for (const content of copiedIssues) {
|
||||
if (content.user && content.id) {
|
||||
const res = checkAndUpdateFile(content);
|
||||
if (res) {
|
||||
queue.push(() => getFile(content.user, content.id, content));
|
||||
}
|
||||
const res = checkAndUpdateIssue(content);
|
||||
const getIssueData = getIssue(content.user, content.id, content);
|
||||
if (res) queue.push(() => getIssueData);
|
||||
|
||||
verifiedIssuePromises.push(getIssueData);
|
||||
}
|
||||
}
|
||||
|
||||
const issueData = await Promise.all(verifiedIssuePromises);
|
||||
const verifiedIssues = await verifyAllPayments(issueData);
|
||||
setIssues(verifiedIssues);
|
||||
} catch (error) {
|
||||
} finally {
|
||||
}
|
||||
}, [videos, hashMapVideos]);
|
||||
}, [issues, hashMapVideos]);
|
||||
|
||||
const getVideosHandler = React.useCallback(async () => {
|
||||
const getIssuesHandler = React.useCallback(async () => {
|
||||
if (!firstFetch.current || !afterFetch.current) return;
|
||||
await getVideos();
|
||||
}, [getVideos]);
|
||||
await getIssues();
|
||||
}, [getIssues]);
|
||||
|
||||
const getVideosHandlerMount = React.useCallback(async () => {
|
||||
const getIssuesHandlerMount = React.useCallback(async () => {
|
||||
if (firstFetch.current) return;
|
||||
firstFetch.current = true;
|
||||
await getVideos();
|
||||
await getIssues();
|
||||
afterFetch.current = true;
|
||||
setIsLoading(false);
|
||||
}, [getVideos]);
|
||||
}, [getIssues]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!firstFetch.current) {
|
||||
getVideosHandlerMount();
|
||||
getIssuesHandlerMount();
|
||||
}
|
||||
}, [getVideosHandlerMount]);
|
||||
}, [getIssuesHandlerMount]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@ -122,18 +131,21 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<FileContainer>
|
||||
{videos.map((file: any, index: number) => {
|
||||
const existingFile = hashMapVideos[file?.id];
|
||||
<IssueContainer>
|
||||
{issues.map((issue: any, index: number) => {
|
||||
const existingFile = hashMapVideos[issue?.id];
|
||||
let hasHash = false;
|
||||
let fileObj = file;
|
||||
let issueObj = issue;
|
||||
if (existingFile) {
|
||||
fileObj = existingFile;
|
||||
issueObj = existingFile;
|
||||
hasHash = true;
|
||||
}
|
||||
|
||||
const icon = getIconsFromObject(fileObj);
|
||||
|
||||
const issueIcons = getIconsFromObject(issueObj);
|
||||
const fileBytes = issueObj?.files.reduce(
|
||||
(acc, cur) => acc + (cur?.size || 0),
|
||||
0
|
||||
);
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -143,13 +155,13 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
|
||||
height: "75px",
|
||||
position: "relative",
|
||||
}}
|
||||
key={fileObj.id}
|
||||
key={issueObj.id}
|
||||
>
|
||||
{hasHash ? (
|
||||
<>
|
||||
<VideoCard
|
||||
<IssueCard
|
||||
onClick={() => {
|
||||
navigate(`/issue/${fileObj?.user}/${fileObj?.id}`);
|
||||
navigate(`/issue/${issueObj?.user}/${issueObj?.id}`);
|
||||
}}
|
||||
sx={{
|
||||
height: "100%",
|
||||
@ -167,42 +179,49 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{icon ? (
|
||||
<img
|
||||
src={icon}
|
||||
width="50px"
|
||||
<div
|
||||
style={{
|
||||
borderRadius: "5px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "200px",
|
||||
}}
|
||||
>
|
||||
<IssueIcons
|
||||
iconSources={issueIcons}
|
||||
style={{ marginRight: "20px" }}
|
||||
showBackupIcon={true}
|
||||
/>
|
||||
) : (
|
||||
<AttachFileIcon />
|
||||
)}
|
||||
</div>
|
||||
<VideoCardTitle
|
||||
sx={{
|
||||
width: "100px",
|
||||
}}
|
||||
>
|
||||
{formatBytes(
|
||||
fileObj?.files.reduce(
|
||||
(acc, cur) => acc + (cur?.size || 0),
|
||||
0
|
||||
)
|
||||
)}
|
||||
{fileBytes > 0 && formatBytes(fileBytes)}
|
||||
</VideoCardTitle>
|
||||
<VideoCardTitle
|
||||
sx={{ fontWeight: "bold", width: "500px" }}
|
||||
>
|
||||
{issueObj.title}
|
||||
</VideoCardTitle>
|
||||
<VideoCardTitle>{fileObj.title}</VideoCardTitle>
|
||||
</Box>
|
||||
{issue?.feeData?.isPaid && (
|
||||
<IssueIcon
|
||||
iconSrc={QORTicon}
|
||||
style={{ marginRight: "20px" }}
|
||||
/>
|
||||
)}
|
||||
<BottomParent>
|
||||
<NameContainer
|
||||
<NameAndDateContainer
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
navigate(`/channel/${fileObj?.user}`);
|
||||
navigate(`/channel/${issueObj?.user}`);
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{ height: 24, width: 24 }}
|
||||
src={`/arbitrary/THUMBNAIL/${fileObj?.user}/qortal_avatar`}
|
||||
alt={`${fileObj?.user}'s avatar`}
|
||||
src={`/arbitrary/THUMBNAIL/${issueObj?.user}/qortal_avatar`}
|
||||
alt={`${issueObj?.user}'s avatar`}
|
||||
/>
|
||||
<VideoCardName
|
||||
sx={{
|
||||
@ -211,17 +230,17 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{fileObj?.user}
|
||||
{issueObj?.user}
|
||||
</VideoCardName>
|
||||
</NameContainer>
|
||||
</NameAndDateContainer>
|
||||
|
||||
{fileObj?.created && (
|
||||
{issueObj?.created && (
|
||||
<VideoUploadDate>
|
||||
{formatDate(fileObj.created)}
|
||||
{formatDate(issueObj.created)}
|
||||
</VideoUploadDate>
|
||||
)}
|
||||
</BottomParent>
|
||||
</VideoCard>
|
||||
</IssueCard>
|
||||
</>
|
||||
) : (
|
||||
<Skeleton
|
||||
@ -239,8 +258,8 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</FileContainer>
|
||||
<LazyLoad onLoadMore={getVideosHandler} isLoading={isLoading}></LazyLoad>
|
||||
</IssueContainer>
|
||||
<LazyLoad onLoadMore={getIssuesHandler} isLoading={isLoading}></LazyLoad>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { FileListComponentLevel } from "../Home/FileListComponentLevel.tsx";
|
||||
import { IssueListComponentLevel } from "../Home/IssueListComponentLevel.tsx";
|
||||
import { HeaderContainer, ProfileContainer } from "./Profile-styles";
|
||||
import {
|
||||
AuthorTextComment,
|
||||
@ -62,7 +62,7 @@ export const IndividualProfile = () => {
|
||||
</StyledCardHeaderComment>
|
||||
</Box>
|
||||
</HeaderContainer>
|
||||
<FileListComponentLevel />
|
||||
<IssueListComponentLevel />
|
||||
</ProfileContainer>
|
||||
);
|
||||
};
|
||||
|
@ -5,7 +5,6 @@ import { setIsLoadingGlobal } from "../../state/features/globalSlice";
|
||||
import { Avatar, Box, Typography, useTheme } from "@mui/material";
|
||||
import { RootState } from "../../state/store";
|
||||
import { addToHashMap } from "../../state/features/fileSlice.ts";
|
||||
import AttachFileIcon from "@mui/icons-material/AttachFile";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import {
|
||||
AuthorTextComment,
|
||||
@ -24,14 +23,20 @@ import { CommentSection } from "../../components/common/Comments/CommentSection"
|
||||
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
||||
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
|
||||
import FileElement from "../../components/common/FileElement";
|
||||
import { allCategoryData } from "../../constants/Categories/1stCategories.ts";
|
||||
import { allCategoryData } from "../../constants/Categories/Categories.ts";
|
||||
import {
|
||||
Category,
|
||||
getCategoriesFromObject,
|
||||
} from "../../components/common/CategoryList/CategoryList.tsx";
|
||||
import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts";
|
||||
import { IssueIcon, IssueIcons } from "../../components/common/IssueIcon.tsx";
|
||||
import QORTicon from "../../assets/icons/qort.png";
|
||||
import {
|
||||
appendIsPaidToFeeData,
|
||||
verifyPayment,
|
||||
} from "../../constants/PublishFees/VerifyPayment.ts";
|
||||
|
||||
export function formatBytes(bytes, decimals = 2) {
|
||||
export function formatBytes(bytes: number, decimals = 2) {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
|
||||
const k = 1024;
|
||||
@ -50,7 +55,7 @@ export const IssueContent = () => {
|
||||
const [descriptionHeight, setDescriptionHeight] = useState<null | number>(
|
||||
null
|
||||
);
|
||||
const [icon, setIcon] = useState<string>("");
|
||||
const [issueIcons, setIssueIcons] = useState<string[]>([]);
|
||||
const userAvatarHash = useSelector(
|
||||
(state: RootState) => state.global.userAvatarHash
|
||||
);
|
||||
@ -67,15 +72,15 @@ export const IssueContent = () => {
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const [fileData, setFileData] = useState<any>(null);
|
||||
const [issueData, setIssueData] = useState<any>(null);
|
||||
const [playlistData, setPlaylistData] = useState<any>(null);
|
||||
|
||||
const hashMapVideos = useSelector(
|
||||
(state: RootState) => state.file.hashMapFiles
|
||||
);
|
||||
const videoReference = useMemo(() => {
|
||||
if (!fileData) return null;
|
||||
const { videoReference } = fileData;
|
||||
if (!issueData) return null;
|
||||
const { videoReference } = issueData;
|
||||
if (
|
||||
videoReference?.identifier &&
|
||||
videoReference?.name &&
|
||||
@ -85,13 +90,13 @@ export const IssueContent = () => {
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [fileData]);
|
||||
}, [issueData]);
|
||||
|
||||
const videoCover = useMemo(() => {
|
||||
if (!fileData) return null;
|
||||
const { videoImage } = fileData;
|
||||
if (!issueData) return null;
|
||||
const { videoImage } = issueData;
|
||||
return videoImage || null;
|
||||
}, [fileData]);
|
||||
}, [issueData]);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const getVideoData = React.useCallback(async (name: string, id: string) => {
|
||||
@ -135,9 +140,16 @@ export const IssueContent = () => {
|
||||
...resourceData,
|
||||
...responseData,
|
||||
};
|
||||
setFileData(combinedData);
|
||||
|
||||
verifyPayment(combinedData).then(feeData => {
|
||||
console.log(
|
||||
"async data: ",
|
||||
appendIsPaidToFeeData(combinedData, feeData)
|
||||
);
|
||||
setIssueData(appendIsPaidToFeeData(combinedData, feeData));
|
||||
dispatch(addToHashMap(combinedData));
|
||||
checkforPlaylist(name, id, combinedData?.code);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@ -217,8 +229,10 @@ export const IssueContent = () => {
|
||||
const existingVideo = hashMapVideos[id];
|
||||
|
||||
if (existingVideo) {
|
||||
setFileData(existingVideo);
|
||||
verifyPayment(existingVideo).then(feeData => {
|
||||
setIssueData(appendIsPaidToFeeData(existingVideo, feeData));
|
||||
checkforPlaylist(name, id, existingVideo?.code);
|
||||
});
|
||||
} else {
|
||||
getVideoData(name, id);
|
||||
}
|
||||
@ -259,21 +273,21 @@ export const IssueContent = () => {
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
const height = contentRef.current.offsetHeight;
|
||||
if (height > 100) {
|
||||
const maxDescriptionHeight = 200;
|
||||
if (height > maxDescriptionHeight) {
|
||||
// Assuming 100px is your threshold
|
||||
setDescriptionHeight(100);
|
||||
setDescriptionHeight(maxDescriptionHeight);
|
||||
}
|
||||
}
|
||||
if (fileData) {
|
||||
const icon = getIconsFromObject(fileData);
|
||||
setIcon(icon);
|
||||
if (issueData) {
|
||||
const icons = getIconsFromObject(issueData);
|
||||
setIssueIcons(icons);
|
||||
}
|
||||
}, [fileData]);
|
||||
}, [issueData]);
|
||||
|
||||
const categoriesDisplay = useMemo(() => {
|
||||
if (fileData) {
|
||||
const categoryList = getCategoriesFromObject(fileData);
|
||||
|
||||
if (issueData) {
|
||||
const categoryList = getCategoriesFromObject(issueData);
|
||||
const categoryNames = categoryList.map((categoryID, index) => {
|
||||
let categoryName: Category;
|
||||
if (index === 0) {
|
||||
@ -294,14 +308,21 @@ export const IssueContent = () => {
|
||||
const filteredCategoryNames = categoryNames.filter(name => name);
|
||||
let categoryDisplay = "";
|
||||
const separator = " > ";
|
||||
const QappName = issueData?.QappName || "";
|
||||
|
||||
filteredCategoryNames.map((name, index) => {
|
||||
categoryDisplay +=
|
||||
index !== filteredCategoryNames.length - 1 ? name + separator : name;
|
||||
if (QappName && index === 1) {
|
||||
categoryDisplay += QappName + separator;
|
||||
}
|
||||
categoryDisplay += name;
|
||||
|
||||
if (index !== filteredCategoryNames.length - 1)
|
||||
categoryDisplay += separator;
|
||||
});
|
||||
return categoryDisplay;
|
||||
}
|
||||
return "no videodata";
|
||||
}, [fileData]);
|
||||
}, [issueData]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@ -325,18 +346,12 @@ export const IssueContent = () => {
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{icon ? (
|
||||
<img
|
||||
src={icon}
|
||||
width="50px"
|
||||
style={{
|
||||
borderRadius: "5px",
|
||||
marginRight: "10px",
|
||||
}}
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<IssueIcons
|
||||
iconSources={issueIcons}
|
||||
style={{ marginRight: "20px" }}
|
||||
/>
|
||||
) : (
|
||||
<AttachFileIcon />
|
||||
)}
|
||||
</div>
|
||||
<FileTitle
|
||||
variant="h1"
|
||||
color="textPrimary"
|
||||
@ -344,10 +359,13 @@ export const IssueContent = () => {
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{fileData?.title}
|
||||
{issueData?.title}
|
||||
</FileTitle>
|
||||
{issueData?.feeData?.isPaid && (
|
||||
<IssueIcon iconSrc={QORTicon} style={{ marginLeft: "10px" }} />
|
||||
)}
|
||||
</div>
|
||||
{fileData?.created && (
|
||||
{issueData?.created && (
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
@ -355,7 +373,7 @@ export const IssueContent = () => {
|
||||
}}
|
||||
color={theme.palette.text.primary}
|
||||
>
|
||||
{formatDate(fileData.created)}
|
||||
{formatDate(issueData.created)}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@ -407,12 +425,13 @@ export const IssueContent = () => {
|
||||
</Typography>
|
||||
</Box>
|
||||
<ImageContainer>
|
||||
{fileData?.images &&
|
||||
fileData.images.map(image => {
|
||||
{issueData?.images &&
|
||||
issueData.images.map(image => {
|
||||
return (
|
||||
<img
|
||||
key={image}
|
||||
src={image}
|
||||
width={`${1080 / fileData.images.length}px`}
|
||||
width={`${1080 / issueData.images.length}px`}
|
||||
style={{
|
||||
marginRight: "10px",
|
||||
marginBottom: "10px",
|
||||
@ -464,12 +483,12 @@ export const IssueContent = () => {
|
||||
? "auto"
|
||||
: isExpandedDescription
|
||||
? "auto"
|
||||
: "100px",
|
||||
: "30vh",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{fileData?.htmlDescription ? (
|
||||
<DisplayHtml html={fileData?.htmlDescription} />
|
||||
{issueData?.htmlDescription ? (
|
||||
<DisplayHtml html={issueData?.htmlDescription} />
|
||||
) : (
|
||||
<FileDescription
|
||||
variant="body1"
|
||||
@ -478,7 +497,7 @@ export const IssueContent = () => {
|
||||
cursor: "default",
|
||||
}}
|
||||
>
|
||||
{fileData?.fullDescription}
|
||||
{issueData?.fullDescription}
|
||||
</FileDescription>
|
||||
)}
|
||||
</Box>
|
||||
@ -509,7 +528,7 @@ export const IssueContent = () => {
|
||||
marginTop: "25px",
|
||||
}}
|
||||
>
|
||||
{fileData?.files?.map((file, index) => {
|
||||
{issueData?.files?.map((file, index) => {
|
||||
return (
|
||||
<FileAttachmentContainer
|
||||
sx={{
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { RootState } from "../store";
|
||||
import { PublishFeeData } from "../../constants/PublishFees/SendFeeFunctions.ts";
|
||||
|
||||
interface GlobalState {
|
||||
files: Video[];
|
||||
filteredFiles: Video[];
|
||||
hashMapFiles: Record<string, Video>;
|
||||
files: Issue[];
|
||||
filteredFiles: Issue[];
|
||||
hashMapFiles: Record<string, Issue>;
|
||||
countNewFiles: number;
|
||||
isFiltering: boolean;
|
||||
filterValue: string;
|
||||
@ -14,6 +14,7 @@ interface GlobalState {
|
||||
selectedCategoryFiles: any[];
|
||||
editFileProperties: any;
|
||||
editPlaylistProperties: any;
|
||||
publishedQappNames: string[];
|
||||
}
|
||||
const initialState: GlobalState = {
|
||||
files: [],
|
||||
@ -28,9 +29,10 @@ const initialState: GlobalState = {
|
||||
selectedCategoryFiles: [null, null, null, null],
|
||||
editFileProperties: null,
|
||||
editPlaylistProperties: null,
|
||||
publishedQappNames: [],
|
||||
};
|
||||
|
||||
export interface Video {
|
||||
export interface Issue {
|
||||
title: string;
|
||||
description: string;
|
||||
created: number | string;
|
||||
@ -44,6 +46,8 @@ export interface Video {
|
||||
updated?: number | string;
|
||||
isValid?: boolean;
|
||||
code?: string;
|
||||
feeData?: PublishFeeData;
|
||||
paymentVerified?: boolean;
|
||||
}
|
||||
|
||||
export const fileSlice = createSlice({
|
||||
@ -113,12 +117,12 @@ export const fileSlice = createSlice({
|
||||
},
|
||||
addArrayToHashMap: (state, action) => {
|
||||
const videos = action.payload;
|
||||
videos.forEach((video: Video) => {
|
||||
videos.forEach((video: Issue) => {
|
||||
state.hashMapFiles[video.id] = video;
|
||||
});
|
||||
},
|
||||
upsertFiles: (state, action) => {
|
||||
action.payload.forEach((video: Video) => {
|
||||
action.payload.forEach((video: Issue) => {
|
||||
const index = state.files.findIndex(p => p.id === video.id);
|
||||
if (index !== -1) {
|
||||
state.files[index] = video;
|
||||
@ -128,7 +132,7 @@ export const fileSlice = createSlice({
|
||||
});
|
||||
},
|
||||
upsertFilteredFiles: (state, action) => {
|
||||
action.payload.forEach((video: Video) => {
|
||||
action.payload.forEach((video: Issue) => {
|
||||
const index = state.filteredFiles.findIndex(p => p.id === video.id);
|
||||
if (index !== -1) {
|
||||
state.filteredFiles[index] = video;
|
||||
@ -138,7 +142,7 @@ export const fileSlice = createSlice({
|
||||
});
|
||||
},
|
||||
upsertFilesBeginning: (state, action) => {
|
||||
action.payload.reverse().forEach((video: Video) => {
|
||||
action.payload.reverse().forEach((video: Issue) => {
|
||||
const index = state.files.findIndex(p => p.id === video.id);
|
||||
if (index !== -1) {
|
||||
state.files[index] = video;
|
||||
@ -157,6 +161,9 @@ export const fileSlice = createSlice({
|
||||
const username = action.payload;
|
||||
state.files = state.files.filter(item => item.user !== username);
|
||||
},
|
||||
setQappNames: (state, action) => {
|
||||
state.publishedQappNames = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -183,6 +190,7 @@ export const {
|
||||
blockUser,
|
||||
setEditFile,
|
||||
setEditPlaylist,
|
||||
setQappNames,
|
||||
} = fileSlice.actions;
|
||||
|
||||
export default fileSlice.reducer;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { FeePrice } from "../../constants/PublishFees/FeePricePublish/FeePricePublish.ts";
|
||||
|
||||
interface GlobalState {
|
||||
isLoadingGlobal: boolean;
|
||||
@ -9,6 +10,7 @@ interface GlobalState {
|
||||
totalFilesPublished: number;
|
||||
totalNamesPublished: number;
|
||||
filesPerNamePublished: number;
|
||||
feeData: FeePrice[];
|
||||
}
|
||||
const initialState: GlobalState = {
|
||||
isLoadingGlobal: false,
|
||||
@ -19,6 +21,7 @@ const initialState: GlobalState = {
|
||||
totalFilesPublished: null,
|
||||
totalNamesPublished: null,
|
||||
filesPerNamePublished: null,
|
||||
feeData: [],
|
||||
};
|
||||
|
||||
export const globalSlice = createSlice({
|
||||
@ -61,6 +64,9 @@ export const globalSlice = createSlice({
|
||||
setFilesPerNamePublished: (state, action) => {
|
||||
state.filesPerNamePublished = action.payload;
|
||||
},
|
||||
setFeeData: (state, action) => {
|
||||
state.feeData = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -74,6 +80,7 @@ export const {
|
||||
setTotalFilesPublished,
|
||||
setTotalNamesPublished,
|
||||
setFilesPerNamePublished,
|
||||
setFeeData,
|
||||
} = globalSlice.actions;
|
||||
|
||||
export default globalSlice.reducer;
|
||||
|
@ -91,7 +91,7 @@ const lightTheme = createTheme({
|
||||
mode: "light",
|
||||
primary: {
|
||||
main: "#FCFCFC",
|
||||
dark: "#F5F5F5",
|
||||
dark: "#E0E0E0",
|
||||
light: "#FFFFFF",
|
||||
},
|
||||
secondary: {
|
||||
@ -138,14 +138,14 @@ const darkTheme = createTheme({
|
||||
palette: {
|
||||
mode: "dark",
|
||||
primary: {
|
||||
main: "#01a9e9", //
|
||||
dark: "#008fcd", //
|
||||
light: "#44c4ff", //
|
||||
main: "#01a9e9", // Qortal Blue
|
||||
dark: "#008fcd",
|
||||
light: "#44c4ff",
|
||||
},
|
||||
secondary: {
|
||||
main: "#007FFF", // Electric blue
|
||||
dark: "#0059B2", // Darker shade of electric blue
|
||||
light: "#3399FF", // Lighter shade of electric blue
|
||||
dark: "#0059B2",
|
||||
light: "#3399FF",
|
||||
},
|
||||
background: {
|
||||
default: "#1C1C1C", // Deep space black
|
||||
|
@ -17,6 +17,7 @@ export const fetchAndEvaluateIssues = async (data: any) => {
|
||||
service: content?.service || "DOCUMENT",
|
||||
identifier: videoId,
|
||||
});
|
||||
|
||||
if (checkStructure(responseData)) {
|
||||
obj = {
|
||||
...content,
|
||||
|
33
src/utils/qortalRequests.ts
Normal file
33
src/utils/qortalRequests.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { NameData } from "../constants/PublishFees/SendFeeFunctions.ts";
|
||||
import { getUserAccountNames } from "../constants/PublishFees/VerifyPayment-Functions.ts";
|
||||
|
||||
export const getNameData = async (name: string) => {
|
||||
return (await qortalRequest({
|
||||
action: "GET_NAME_DATA",
|
||||
name: name,
|
||||
})) as NameData;
|
||||
};
|
||||
|
||||
export const sendQchatDM = async (
|
||||
recipientName: string,
|
||||
message: string,
|
||||
allowSelfAsRecipient = false
|
||||
) => {
|
||||
if (!allowSelfAsRecipient) {
|
||||
const userAccountNames = await getUserAccountNames();
|
||||
const userNames = userAccountNames.map(name => name.name);
|
||||
if (userNames.includes(recipientName)) return;
|
||||
}
|
||||
|
||||
const address = await getNameData(recipientName);
|
||||
try {
|
||||
return await qortalRequest({
|
||||
action: "SEND_CHAT_MESSAGE",
|
||||
destinationAddress: address.owner,
|
||||
message,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return false;
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user