mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-14 17:41:20 +00:00
980 lines
27 KiB
TypeScript
980 lines
27 KiB
TypeScript
import {
|
|
Avatar,
|
|
Box,
|
|
Breadcrumbs,
|
|
Button,
|
|
ButtonBase,
|
|
Card,
|
|
Dialog,
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogContentText,
|
|
DialogTitle,
|
|
Divider,
|
|
FormControlLabel,
|
|
IconButton,
|
|
Radio,
|
|
RadioGroup,
|
|
TextField,
|
|
Typography,
|
|
} from "@mui/material";
|
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { IndexCategory, useIndexStore } from "../../state/indexes";
|
|
import CloseIcon from "@mui/icons-material/Close";
|
|
import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos";
|
|
import { hashWordWithoutPublicSalt } from "../../utils/encryption";
|
|
import { usePublish } from "../../hooks/usePublish";
|
|
import {
|
|
dismissToast,
|
|
showError,
|
|
showLoading,
|
|
showSuccess,
|
|
} from "../../utils/toast";
|
|
import { objectToBase64 } from "../../utils/base64";
|
|
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
|
import { OptionsManager } from "../OptionsManager";
|
|
import ShortUniqueId from "short-unique-id";
|
|
import { Spacer } from "../../common/Spacer";
|
|
import { useModal } from "../useModal";
|
|
import { createAvatarLink } from "../../utils/qortal";
|
|
import { extractComponents } from "../../utils/text";
|
|
import NavigateNextIcon from "@mui/icons-material/NavigateNext";
|
|
|
|
const uid = new ShortUniqueId({ length: 10, dictionary: "alphanum" });
|
|
|
|
interface PropsMode {
|
|
link: string;
|
|
name: string;
|
|
mode: number;
|
|
setMode: (mode: number) => void;
|
|
username: string;
|
|
}
|
|
|
|
interface PropsIndexManager {
|
|
username: string | null;
|
|
}
|
|
|
|
const cleanString = (str: string) => str.replace(/\s{2,}/g, ' ').trim().toLocaleLowerCase();
|
|
|
|
export const IndexManager = ({ username }: PropsIndexManager) => {
|
|
const open = useIndexStore((state) => state.open);
|
|
const setOpen = useIndexStore((state) => state.setOpen);
|
|
const [title, setTitle] = useState("");
|
|
const [description, setDescription] = useState("");
|
|
const [hasMetadata, setHasMetadata] = useState(false);
|
|
const [mode, setMode] = useState(1);
|
|
const publish = usePublish();
|
|
const handleClose = () => {
|
|
setOpen(null);
|
|
setMode(1);
|
|
setTitle("");
|
|
setDescription("");
|
|
setHasMetadata(false);
|
|
};
|
|
|
|
const getMetadata = useCallback(async (name: string, link: string) => {
|
|
try {
|
|
const identifierWithoutHash = name + link;
|
|
const identifier = await hashWordWithoutPublicSalt(
|
|
identifierWithoutHash,
|
|
20
|
|
);
|
|
const rawData = await publish.fetchPublish(
|
|
{
|
|
name: name,
|
|
service: "METADATA",
|
|
identifier,
|
|
},
|
|
"JSON"
|
|
);
|
|
|
|
if (
|
|
rawData?.resource &&
|
|
rawData?.resource?.data?.title &&
|
|
rawData?.resource?.data?.description
|
|
) {
|
|
setHasMetadata(true);
|
|
setDescription(rawData?.resource?.data?.description);
|
|
setTitle(rawData?.resource?.data?.title);
|
|
}
|
|
} catch (error) {}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (open?.name && open?.link) {
|
|
setTimeout(() => {
|
|
getMetadata(open?.name, open?.link);
|
|
}, 100);
|
|
}
|
|
}, [open?.link, open?.name]);
|
|
|
|
if (!open || !username) return null;
|
|
|
|
return (
|
|
<Dialog
|
|
open={!!open}
|
|
fullWidth={true}
|
|
maxWidth={"md"}
|
|
sx={{
|
|
zIndex: 999990,
|
|
}}
|
|
>
|
|
<DialogTitle>Index manager</DialogTitle>
|
|
<IconButton
|
|
aria-label="close"
|
|
onClick={handleClose}
|
|
sx={(theme) => ({
|
|
position: "absolute",
|
|
right: 8,
|
|
top: 8,
|
|
})}
|
|
>
|
|
<CloseIcon />
|
|
</IconButton>
|
|
{mode === 1 && (
|
|
<EntryMode
|
|
link={open?.link}
|
|
name={open?.name}
|
|
mode={mode}
|
|
setMode={setMode}
|
|
username={username}
|
|
hasMetadata={hasMetadata}
|
|
/>
|
|
)}
|
|
{mode === 2 && (
|
|
<CreateIndex
|
|
link={open?.link}
|
|
name={open?.name}
|
|
mode={mode}
|
|
setMode={setMode}
|
|
username={username}
|
|
category={open?.category}
|
|
rootName={open?.rootName}
|
|
/>
|
|
)}
|
|
{/* {mode === 3 && (
|
|
<YourIndices
|
|
link={open?.link}
|
|
name={open?.name}
|
|
mode={mode}
|
|
setMode={setMode}
|
|
username={username}
|
|
category={open?.category}
|
|
rootName={open?.rootName}
|
|
/>
|
|
)} */}
|
|
{mode === 4 && (
|
|
<AddMetadata
|
|
link={open?.link}
|
|
name={open?.name}
|
|
mode={mode}
|
|
setMode={setMode}
|
|
username={username}
|
|
title={title}
|
|
description={description}
|
|
setDescription={setDescription}
|
|
setTitle={setTitle}
|
|
/>
|
|
)}
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
interface PropsEntryMode extends PropsMode {
|
|
hasMetadata: boolean;
|
|
}
|
|
|
|
const EntryMode = ({
|
|
link,
|
|
name,
|
|
mode,
|
|
setMode,
|
|
username,
|
|
hasMetadata,
|
|
}: PropsEntryMode) => {
|
|
return (
|
|
<>
|
|
<DialogContent>
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "20px",
|
|
width: "100%",
|
|
alignItems: "flex-start",
|
|
}}
|
|
>
|
|
<ButtonBase
|
|
sx={{
|
|
width: "100%",
|
|
}}
|
|
onClick={() => setMode(2)}
|
|
>
|
|
<Box
|
|
sx={{
|
|
p: 2,
|
|
border: "2px solid",
|
|
borderRadius: 2,
|
|
width: "100%",
|
|
}}
|
|
>
|
|
<Typography>Create new index</Typography>
|
|
</Box>
|
|
</ButtonBase>
|
|
{/* <ButtonBase
|
|
sx={{
|
|
width: "100%",
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
p: 2,
|
|
border: "2px solid",
|
|
borderRadius: 2,
|
|
width: "100%",
|
|
}}
|
|
>
|
|
<Typography>Your indices</Typography>
|
|
</Box>
|
|
</ButtonBase> */}
|
|
<ButtonBase
|
|
sx={{
|
|
width: "100%",
|
|
visibility: username === name ? 'visible' : 'hidden'
|
|
}}
|
|
onClick={() => setMode(4)}
|
|
>
|
|
<Box
|
|
sx={{
|
|
p: 2,
|
|
border: "2px solid",
|
|
borderRadius: 2,
|
|
width: "100%",
|
|
gap: "20px",
|
|
display: "flex",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<Typography>Add metadata</Typography>
|
|
{hasMetadata && <CheckCircleIcon color="success" />}
|
|
</Box>
|
|
</ButtonBase>
|
|
</Box>
|
|
</DialogContent>
|
|
</>
|
|
);
|
|
};
|
|
|
|
interface PropsAddMetadata extends PropsMode {
|
|
title: string;
|
|
description: string;
|
|
setTitle: (val: string) => void;
|
|
setDescription: (val: string) => void;
|
|
}
|
|
|
|
const AddMetadata = ({
|
|
link,
|
|
name,
|
|
mode,
|
|
setMode,
|
|
username,
|
|
title,
|
|
description,
|
|
setDescription,
|
|
setTitle,
|
|
}: PropsAddMetadata) => {
|
|
const publish = usePublish();
|
|
|
|
const disableButton = !title.trim() || !description.trim() || !name || !link;
|
|
|
|
const createMetadata = async () => {
|
|
const loadId = showLoading("Publishing metadata...");
|
|
try {
|
|
const identifierWithoutHash = name + link;
|
|
const identifier = await hashWordWithoutPublicSalt(
|
|
identifierWithoutHash,
|
|
20
|
|
);
|
|
const objectToPublish = {
|
|
title,
|
|
description,
|
|
};
|
|
const toBase64 = await objectToBase64(objectToPublish);
|
|
const res = await qortalRequest({
|
|
action: "PUBLISH_QDN_RESOURCE",
|
|
service: "METADATA",
|
|
identifier: identifier,
|
|
base64: toBase64,
|
|
});
|
|
|
|
if (res?.signature) {
|
|
showSuccess("Successfully published metadata");
|
|
publish.updatePublish(
|
|
{
|
|
identifier,
|
|
service: "METADATA",
|
|
name: username,
|
|
},
|
|
objectToPublish
|
|
);
|
|
}
|
|
} catch (error) {
|
|
const message =
|
|
error instanceof Error ? error.message : "Failed to publish metadata";
|
|
showError(message);
|
|
} finally {
|
|
dismissToast(loadId);
|
|
}
|
|
};
|
|
|
|
const res = extractComponents(link || "");
|
|
const appName = res?.name || "";
|
|
|
|
return (
|
|
<>
|
|
<DialogContent>
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "20px",
|
|
width: "100%",
|
|
alignItems: "flex-start",
|
|
}}
|
|
>
|
|
<IconButton disabled={mode === 1} onClick={() => setMode(1)}>
|
|
<ArrowBackIosIcon />
|
|
</IconButton>
|
|
<Typography>Example of how it could look like:</Typography>
|
|
<Card sx={{
|
|
width: '100%',
|
|
padding: '5px'
|
|
}}>
|
|
<Box sx={{ mb: 3, display: "flex", gap: 2, width: "100%" }}>
|
|
<Avatar
|
|
alt={appName}
|
|
src={createAvatarLink(appName)}
|
|
variant="square"
|
|
sx={{ width: 24, height: 24, mt: 0.5 }}
|
|
/>
|
|
<Box
|
|
sx={{
|
|
width: "calc(100% - 50px)",
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
width: "100%",
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
justifyContent: 'flex-start'
|
|
}}
|
|
>
|
|
<Breadcrumbs
|
|
separator={<NavigateNextIcon fontSize="small" />}
|
|
aria-label="breadcrumb"
|
|
>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{res?.service}
|
|
</Typography>
|
|
<Typography
|
|
sx={{
|
|
textAlign: "start",
|
|
}}
|
|
variant="body2"
|
|
color="text.secondary"
|
|
>
|
|
{appName}
|
|
</Typography>
|
|
<Typography
|
|
sx={{
|
|
textAlign: "start",
|
|
}}
|
|
variant="body2"
|
|
color="text.secondary"
|
|
>
|
|
{res?.path}
|
|
</Typography>
|
|
</Breadcrumbs>
|
|
</Box>
|
|
<Spacer height="10px" />
|
|
<Box
|
|
sx={{
|
|
width: "100%",
|
|
display: 'flex',
|
|
flexDirection: 'column'
|
|
}}
|
|
>
|
|
<Typography
|
|
variant="h6"
|
|
sx={{
|
|
display: "block",
|
|
textDecoration: "none",
|
|
width: "100%",
|
|
textAlign: "start",
|
|
"&:hover": { textDecoration: "underline" },
|
|
}}
|
|
>
|
|
{title}
|
|
</Typography>
|
|
</Box>
|
|
<Typography
|
|
sx={{
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
}}
|
|
variant="body2"
|
|
color="text.secondary"
|
|
>
|
|
{description}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
</Card>
|
|
<Box>
|
|
<Typography>Title</Typography>
|
|
<TextField
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
size="small"
|
|
placeholder="Add a title for the link"
|
|
slotProps={{
|
|
htmlInput: { maxLength: 50 },
|
|
}}
|
|
fullWidth
|
|
helperText={
|
|
<Typography
|
|
variant="caption"
|
|
color={title.length >= 50 ? "error" : "text.secondary"}
|
|
>
|
|
{title.length}/{50} characters
|
|
</Typography>
|
|
}
|
|
/>
|
|
</Box>
|
|
<Box>
|
|
<Typography>Description</Typography>
|
|
<TextField
|
|
fullWidth
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
size="small"
|
|
placeholder="Add a description for the link"
|
|
slotProps={{
|
|
htmlInput: { maxLength: 120 },
|
|
}}
|
|
helperText={
|
|
<Typography
|
|
variant="caption"
|
|
color={description.length >= 120 ? "error" : "text.secondary"}
|
|
>
|
|
{description.length}/{120} characters
|
|
</Typography>
|
|
}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button
|
|
onClick={createMetadata}
|
|
disabled={disableButton}
|
|
variant="contained"
|
|
>
|
|
Publish metadata
|
|
</Button>
|
|
</DialogActions>
|
|
</>
|
|
);
|
|
};
|
|
|
|
interface PropsCreateIndex extends PropsMode {
|
|
category: IndexCategory;
|
|
rootName: string;
|
|
}
|
|
|
|
const CreateIndex = ({
|
|
link,
|
|
name,
|
|
mode,
|
|
setMode,
|
|
username,
|
|
category,
|
|
rootName,
|
|
}: PropsCreateIndex) => {
|
|
const [terms, setTerms] = useState<string[]>([]);
|
|
const publish = usePublish();
|
|
const [size, setSize] = useState(0);
|
|
const [fullSize, setFullSize] = useState(0);
|
|
const { isShow, onCancel, onOk, show } = useModal();
|
|
const [recommendedIndices, setRecommendedIndices] = useState([])
|
|
const [recommendedSelection, setRecommendedSelection] = useState("")
|
|
|
|
|
|
const objectToPublish = useMemo(() => {
|
|
if(recommendedSelection){
|
|
return [
|
|
{
|
|
n: name,
|
|
t: cleanString(recommendedSelection),
|
|
c: category,
|
|
l: link,
|
|
}
|
|
]
|
|
}
|
|
return terms?.map((term) => {
|
|
return {
|
|
n: name,
|
|
t: cleanString(term),
|
|
c: category,
|
|
l: link,
|
|
};
|
|
});
|
|
}, [name, terms, category, link, recommendedSelection]);
|
|
const objectToCalculateSize = useMemo(() => {
|
|
return [
|
|
{
|
|
n: name,
|
|
t: "",
|
|
c: category,
|
|
l: link,
|
|
},
|
|
];
|
|
}, [name, category, link]);
|
|
|
|
const shouldRecommendMax = !recommendedSelection && terms?.length === 1 && 230 - size > 50;
|
|
const recommendedSize = 230 - size;
|
|
|
|
useEffect(() => {
|
|
const getSize = async (data: any, data2: any) => {
|
|
try {
|
|
const toBase64 = await objectToBase64(data);
|
|
const size = toBase64?.length;
|
|
setSize(size);
|
|
const toBase642 = await objectToBase64(data2);
|
|
const size2 = toBase642?.length;
|
|
setFullSize(size2);
|
|
} catch (error) {}
|
|
};
|
|
getSize(objectToCalculateSize, objectToPublish);
|
|
}, [objectToCalculateSize, objectToPublish]);
|
|
|
|
const getRecommendedIndices = useCallback(async (nameParam: string, linkParam: string, rootNameParam: string)=> {
|
|
try {
|
|
const hashedRootName = await hashWordWithoutPublicSalt(rootNameParam, 20);
|
|
const hashedLink = await hashWordWithoutPublicSalt(linkParam, 20);
|
|
const identifier = `idx-${hashedRootName}-${hashedLink}-`;
|
|
const res = await fetch(`/arbitrary/indices/${nameParam}/${identifier}`)
|
|
const data = await res.json()
|
|
setRecommendedIndices(data)
|
|
} catch (error) {
|
|
|
|
}
|
|
}, [])
|
|
useEffect(()=> {
|
|
if(name && link && rootName){
|
|
getRecommendedIndices(name, link, rootName)
|
|
}
|
|
}, [name, link, rootName, getRecommendedIndices])
|
|
const disableButton = (terms.length === 0 && !recommendedSelection) || !name || !link;
|
|
|
|
const createIndex = async () => {
|
|
const loadId = showLoading("Publishing index...");
|
|
try {
|
|
const hashedRootName = await hashWordWithoutPublicSalt(rootName, 20);
|
|
const hashedLink = await hashWordWithoutPublicSalt(link, 20);
|
|
const randomUid = uid.rnd();
|
|
const identifier = `idx-${hashedRootName}-${hashedLink}-${randomUid}`;
|
|
const toBase64 = await objectToBase64(objectToPublish);
|
|
const res = await qortalRequest({
|
|
action: "PUBLISH_QDN_RESOURCE",
|
|
service: "JSON",
|
|
identifier: identifier,
|
|
base64: toBase64,
|
|
});
|
|
|
|
if (res?.signature) {
|
|
showSuccess("Successfully published index");
|
|
publish.updatePublish(
|
|
{
|
|
identifier,
|
|
service: "JSON",
|
|
name: username,
|
|
},
|
|
objectToPublish
|
|
);
|
|
setTerms([]);
|
|
}
|
|
} catch (error) {
|
|
const message =
|
|
error instanceof Error ? error.message : "Failed to publish index";
|
|
showError(message);
|
|
} finally {
|
|
dismissToast(loadId);
|
|
}
|
|
};
|
|
|
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
setRecommendedSelection((event.target as HTMLInputElement).value);
|
|
};
|
|
|
|
|
|
return (
|
|
<>
|
|
<DialogContent>
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "20px",
|
|
width: "100%",
|
|
alignItems: "flex-start",
|
|
}}
|
|
>
|
|
<IconButton disabled={mode === 1} onClick={() => setMode(1)}>
|
|
<ArrowBackIosIcon />
|
|
</IconButton>
|
|
{recommendedIndices?.length > 0 && (
|
|
<>
|
|
<Typography>Recommended Indices</Typography>
|
|
<RadioGroup
|
|
aria-labelledby="demo-controlled-radio-buttons-group"
|
|
name="controlled-radio-buttons-group"
|
|
value={recommendedSelection}
|
|
onChange={handleChange}
|
|
>
|
|
{recommendedIndices?.map((ri: any, i)=> {
|
|
return <FormControlLabel key={i} value={ri?.term} control={<Radio />} label={ri?.term} />
|
|
})}
|
|
</RadioGroup>
|
|
<Divider sx={{
|
|
width: '100%'
|
|
}} />
|
|
</>
|
|
)}
|
|
|
|
|
|
|
|
<Box>
|
|
<RadioGroup
|
|
aria-labelledby="demo-controlled-radio-buttons-group"
|
|
name="controlled-radio-buttons-group"
|
|
value={recommendedSelection}
|
|
onChange={handleChange}
|
|
>
|
|
<FormControlLabel value="" control={<Radio />} label="Add search term" />
|
|
</RadioGroup>
|
|
<Spacer height="10px" />
|
|
{!recommendedSelection && (
|
|
<OptionsManager
|
|
maxLength={17}
|
|
items={terms}
|
|
onlyStrings
|
|
label="search terms"
|
|
setItems={async (termsNew: string[]) => {
|
|
try {
|
|
if (terms?.length === 1 && termsNew?.length === 2) {
|
|
await show({
|
|
message: "",
|
|
});
|
|
}
|
|
setTerms(termsNew);
|
|
} catch (error) {
|
|
//error
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
<Spacer height="10px" />
|
|
<Typography
|
|
sx={{
|
|
visibility:
|
|
shouldRecommendMax && fullSize > 230 ? "visible" : "hidden",
|
|
}}
|
|
>
|
|
It is recommended to keep your term character count below{" "}
|
|
{recommendedSize} characters
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button
|
|
onClick={createIndex}
|
|
disabled={disableButton}
|
|
variant="contained"
|
|
>
|
|
Publish index
|
|
</Button>
|
|
</DialogActions>
|
|
<Dialog
|
|
open={isShow}
|
|
aria-labelledby="alert-dialog-title"
|
|
aria-describedby="alert-dialog-description"
|
|
sx={{
|
|
zIndex: 999991,
|
|
}}
|
|
>
|
|
<DialogTitle id="alert-dialog-title">
|
|
Adding multiple indices
|
|
</DialogTitle>
|
|
<DialogContent>
|
|
<DialogContentText id="alert-dialog-description">
|
|
Subsequent indices will keep your publish fees lower, but they will
|
|
have less strength in future search results.
|
|
</DialogContentText>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={onCancel}>Cancel</Button>
|
|
<Button onClick={onOk}>
|
|
Continue
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
};
|
|
|
|
|
|
const YourIndices = ({
|
|
link,
|
|
name,
|
|
mode,
|
|
setMode,
|
|
username,
|
|
category,
|
|
rootName,
|
|
}: PropsCreateIndex) => {
|
|
const [terms, setTerms] = useState<string[]>([]);
|
|
const publish = usePublish();
|
|
const [size, setSize] = useState(0);
|
|
const [fullSize, setFullSize] = useState(0);
|
|
const { isShow, onCancel, onOk, show } = useModal();
|
|
const [recommendedIndices, setRecommendedIndices] = useState([])
|
|
const [recommendedSelection, setRecommendedSelection] = useState("")
|
|
const objectToPublish = useMemo(() => {
|
|
if(recommendedSelection){
|
|
return [
|
|
{
|
|
n: name,
|
|
t: recommendedSelection.toLocaleLowerCase(),
|
|
c: category,
|
|
l: link,
|
|
}
|
|
]
|
|
}
|
|
return terms?.map((term) => {
|
|
return {
|
|
n: name,
|
|
t: term.toLocaleLowerCase(),
|
|
c: category,
|
|
l: link,
|
|
};
|
|
});
|
|
}, [name, terms, category, link, recommendedSelection]);
|
|
const objectToCalculateSize = useMemo(() => {
|
|
return [
|
|
{
|
|
n: name,
|
|
t: "",
|
|
c: category,
|
|
l: link,
|
|
},
|
|
];
|
|
}, [name, category, link]);
|
|
|
|
const shouldRecommendMax = !recommendedSelection && terms?.length === 1 && 230 - size > 50;
|
|
const recommendedSize = 230 - size;
|
|
|
|
useEffect(() => {
|
|
const getSize = async (data: any, data2: any) => {
|
|
try {
|
|
const toBase64 = await objectToBase64(data);
|
|
const size = toBase64?.length;
|
|
setSize(size);
|
|
const toBase642 = await objectToBase64(data2);
|
|
const size2 = toBase642?.length;
|
|
setFullSize(size2);
|
|
} catch (error) {}
|
|
};
|
|
getSize(objectToCalculateSize, objectToPublish);
|
|
}, [objectToCalculateSize, objectToPublish]);
|
|
|
|
const getYourIndices = useCallback(async (nameParam: string, linkParam: string, rootNameParam: string)=> {
|
|
try {
|
|
const hashedRootName = await hashWordWithoutPublicSalt(rootNameParam, 20);
|
|
const hashedLink = await hashWordWithoutPublicSalt(linkParam, 20);
|
|
const identifier = `idx-${hashedRootName}-${hashedLink}-`;
|
|
const res = await fetch(`/arbitrary/indices/${nameParam}/${identifier}`)
|
|
const data = await res.json()
|
|
setRecommendedIndices(data)
|
|
} catch (error) {
|
|
|
|
}
|
|
}, [])
|
|
useEffect(()=> {
|
|
if(username && link && rootName){
|
|
getYourIndices(username, link, rootName)
|
|
}
|
|
}, [username, link, rootName, getYourIndices])
|
|
const disableButton = (terms.length === 0 && !recommendedSelection) || !name || !link;
|
|
|
|
const createIndex = async () => {
|
|
const loadId = showLoading("Publishing index...");
|
|
try {
|
|
const hashedRootName = await hashWordWithoutPublicSalt(rootName, 20);
|
|
const hashedLink = await hashWordWithoutPublicSalt(link, 20);
|
|
const randomUid = uid.rnd();
|
|
const identifier = `idx-${hashedRootName}-${hashedLink}-${randomUid}`;
|
|
const toBase64 = await objectToBase64(objectToPublish);
|
|
const res = await qortalRequest({
|
|
action: "PUBLISH_QDN_RESOURCE",
|
|
service: "JSON",
|
|
identifier: identifier,
|
|
base64: toBase64,
|
|
});
|
|
|
|
if (res?.signature) {
|
|
showSuccess("Successfully published index");
|
|
publish.updatePublish(
|
|
{
|
|
identifier,
|
|
service: "JSON",
|
|
name: username,
|
|
},
|
|
objectToPublish
|
|
);
|
|
setTerms([]);
|
|
}
|
|
} catch (error) {
|
|
const message =
|
|
error instanceof Error ? error.message : "Failed to publish index";
|
|
showError(message);
|
|
} finally {
|
|
dismissToast(loadId);
|
|
}
|
|
};
|
|
|
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
setRecommendedSelection((event.target as HTMLInputElement).value);
|
|
};
|
|
|
|
|
|
return (
|
|
<>
|
|
<DialogContent>
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "20px",
|
|
width: "100%",
|
|
alignItems: "flex-start",
|
|
}}
|
|
>
|
|
<IconButton disabled={mode === 1} onClick={() => setMode(1)}>
|
|
<ArrowBackIosIcon />
|
|
</IconButton>
|
|
{recommendedIndices?.length > 0 && (
|
|
<>
|
|
<Typography>Recommended Indices</Typography>
|
|
<RadioGroup
|
|
aria-labelledby="demo-controlled-radio-buttons-group"
|
|
name="controlled-radio-buttons-group"
|
|
value={recommendedSelection}
|
|
onChange={handleChange}
|
|
>
|
|
{recommendedIndices?.map((ri: any, i)=> {
|
|
return <FormControlLabel key={i} value={ri?.term} control={<Radio />} label={ri?.term} />
|
|
})}
|
|
</RadioGroup>
|
|
<Divider sx={{
|
|
width: '100%'
|
|
}} />
|
|
</>
|
|
)}
|
|
|
|
|
|
|
|
<Box>
|
|
<RadioGroup
|
|
aria-labelledby="demo-controlled-radio-buttons-group"
|
|
name="controlled-radio-buttons-group"
|
|
value={recommendedSelection}
|
|
onChange={handleChange}
|
|
>
|
|
<FormControlLabel value="" control={<Radio />} label="Add search term" />
|
|
</RadioGroup>
|
|
<Spacer height="10px" />
|
|
{!recommendedSelection && (
|
|
<OptionsManager
|
|
maxLength={17}
|
|
items={terms}
|
|
onlyStrings
|
|
label="search terms"
|
|
setItems={async (termsNew: string[]) => {
|
|
try {
|
|
if (terms?.length === 1 && termsNew?.length === 2) {
|
|
await show({
|
|
message: "",
|
|
});
|
|
}
|
|
setTerms(termsNew);
|
|
} catch (error) {
|
|
//error
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
<Spacer height="10px" />
|
|
<Typography
|
|
sx={{
|
|
visibility:
|
|
shouldRecommendMax && fullSize > 230 ? "visible" : "hidden",
|
|
}}
|
|
>
|
|
It is recommended to keep your term character count below{" "}
|
|
{recommendedSize} characters
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button
|
|
onClick={createIndex}
|
|
disabled={disableButton}
|
|
variant="contained"
|
|
>
|
|
Publish index
|
|
</Button>
|
|
</DialogActions>
|
|
<Dialog
|
|
open={isShow}
|
|
aria-labelledby="alert-dialog-title"
|
|
aria-describedby="alert-dialog-description"
|
|
sx={{
|
|
zIndex: 999991,
|
|
}}
|
|
>
|
|
<DialogTitle id="alert-dialog-title">
|
|
Adding multiple indices
|
|
</DialogTitle>
|
|
<DialogContent>
|
|
<DialogContentText id="alert-dialog-description">
|
|
Subsequent indices will keep your publish fees lower, but they will
|
|
have less strength in future search results.
|
|
</DialogContentText>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={onCancel}>Cancel</Button>
|
|
<Button onClick={onOk}>
|
|
Continue
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
}; |