mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-05-18 15:46:59 +00:00
571 lines
16 KiB
TypeScript
571 lines
16 KiB
TypeScript
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
|
import {
|
|
AppCircle,
|
|
AppCircleContainer,
|
|
AppCircleLabel,
|
|
AppDownloadButton,
|
|
AppDownloadButtonText,
|
|
AppInfoAppName,
|
|
AppInfoSnippetContainer,
|
|
AppInfoSnippetLeft,
|
|
AppInfoSnippetMiddle,
|
|
AppInfoSnippetRight,
|
|
AppInfoUserName,
|
|
AppLibrarySubTitle,
|
|
AppPublishTagsContainer,
|
|
AppsLibraryContainer,
|
|
AppsParent,
|
|
AppsWidthLimiter,
|
|
PublishQAppCTAButton,
|
|
PublishQAppChoseFile,
|
|
PublishQAppInfo,
|
|
} from './Apps-styles';
|
|
import {
|
|
InputBase,
|
|
InputLabel,
|
|
MenuItem,
|
|
Select,
|
|
useTheme,
|
|
} from '@mui/material';
|
|
import { styled } from '@mui/system';
|
|
import UnfoldMoreRoundedIcon from '@mui/icons-material/UnfoldMoreRounded';
|
|
import { Add } from '@mui/icons-material';
|
|
import { MyContext, getBaseApiReact } from '../../App';
|
|
import LogoSelected from '../../assets/svgs/LogoSelected.svg';
|
|
import { Spacer } from '../../common/Spacer';
|
|
import { executeEvent } from '../../utils/events';
|
|
import { useDropzone } from 'react-dropzone';
|
|
import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar';
|
|
import { CustomizedSnackbars } from '../Snackbar/Snackbar';
|
|
import { getFee } from '../../background';
|
|
import { fileToBase64 } from '../../utils/fileReading';
|
|
|
|
const CustomSelect = styled(Select)({
|
|
border: '0.5px solid var(--50-white, #FFFFFF80)',
|
|
padding: '0px 15px',
|
|
borderRadius: '5px',
|
|
height: '36px',
|
|
width: '100%',
|
|
maxWidth: '450px',
|
|
'& .MuiSelect-select': {
|
|
padding: '0px',
|
|
},
|
|
'&:hover': {
|
|
borderColor: 'none', // Border color on hover
|
|
},
|
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
|
borderColor: 'none', // Border color when focused
|
|
},
|
|
'&.Mui-disabled': {
|
|
opacity: 0.5, // Lower opacity when disabled
|
|
},
|
|
'& .MuiSvgIcon-root': {
|
|
color: 'var(--50-white, #FFFFFF80)',
|
|
},
|
|
});
|
|
|
|
const CustomMenuItem = styled(MenuItem)({
|
|
// backgroundColor: '#1f1f1f', // Background for dropdown items
|
|
// color: '#ccc',
|
|
// '&:hover': {
|
|
// backgroundColor: '#333', // Darker background on hover
|
|
// },
|
|
});
|
|
|
|
export const AppPublish = ({ categories, myAddress, myName }) => {
|
|
const [names, setNames] = useState([]);
|
|
const [name, setName] = useState('');
|
|
const [title, setTitle] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [category, setCategory] = useState('');
|
|
const [appType, setAppType] = useState('APP');
|
|
const [file, setFile] = useState(null);
|
|
const { show } = useContext(MyContext);
|
|
const theme = useTheme();
|
|
const [tag1, setTag1] = useState('');
|
|
const [tag2, setTag2] = useState('');
|
|
const [tag3, setTag3] = useState('');
|
|
const [tag4, setTag4] = useState('');
|
|
const [tag5, setTag5] = useState('');
|
|
const [openSnack, setOpenSnack] = useState(false);
|
|
const [infoSnack, setInfoSnack] = useState(null);
|
|
const [isLoading, setIsLoading] = useState('');
|
|
const maxFileSize = appType === 'APP' ? 50 * 1024 * 1024 : 400 * 1024 * 1024; // 50MB or 400MB
|
|
const { getRootProps, getInputProps } = useDropzone({
|
|
accept: {
|
|
'application/zip': ['.zip'], // Only accept zip files
|
|
},
|
|
maxSize: maxFileSize, // Set the max size based on appType
|
|
multiple: false, // Disable multiple file uploads
|
|
onDrop: (acceptedFiles) => {
|
|
if (acceptedFiles.length > 0) {
|
|
setFile(acceptedFiles[0]); // Set the file name
|
|
}
|
|
},
|
|
onDropRejected: (fileRejections) => {
|
|
fileRejections.forEach(({ file, errors }) => {
|
|
errors.forEach((error) => {
|
|
if (error.code === 'file-too-large') {
|
|
console.error(
|
|
`File ${file.name} is too large. Max size allowed is ${
|
|
maxFileSize / (1024 * 1024)
|
|
} MB.`
|
|
);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
});
|
|
|
|
const getQapp = React.useCallback(async (name, appType) => {
|
|
try {
|
|
setIsLoading('Loading app information');
|
|
const url = `${getBaseApiReact()}/arbitrary/resources/search?service=${appType}&mode=ALL&name=${name}&includemetadata=true`;
|
|
|
|
const response = await fetch(url, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
if (!response?.ok) return;
|
|
const responseData = await response.json();
|
|
|
|
if (responseData?.length > 0) {
|
|
const myApp = responseData[0];
|
|
setTitle(myApp?.metadata?.title || '');
|
|
setDescription(myApp?.metadata?.description || '');
|
|
setCategory(myApp?.metadata?.category || '');
|
|
setTag1(myApp?.metadata?.tags[0] || '');
|
|
setTag2(myApp?.metadata?.tags[1] || '');
|
|
setTag3(myApp?.metadata?.tags[2] || '');
|
|
setTag4(myApp?.metadata?.tags[3] || '');
|
|
setTag5(myApp?.metadata?.tags[4] || '');
|
|
}
|
|
} catch (error) {
|
|
} finally {
|
|
setIsLoading('');
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!name || !appType) return;
|
|
getQapp(name, appType);
|
|
}, [name, appType]);
|
|
|
|
const getNames = useCallback(async () => {
|
|
if (!myAddress) return;
|
|
try {
|
|
setIsLoading('Loading names');
|
|
const res = await fetch(
|
|
`${getBaseApiReact()}/names/address/${myAddress}`
|
|
);
|
|
const data = await res.json();
|
|
setNames(data?.map((item) => item.name));
|
|
} catch (error) {
|
|
console.error(error);
|
|
} finally {
|
|
setIsLoading('');
|
|
}
|
|
}, [myAddress]);
|
|
useEffect(() => {
|
|
getNames();
|
|
}, [getNames]);
|
|
|
|
const publishApp = async () => {
|
|
try {
|
|
const data = {
|
|
name,
|
|
title,
|
|
description,
|
|
category,
|
|
appType,
|
|
file,
|
|
};
|
|
const requiredFields = [
|
|
'name',
|
|
'title',
|
|
'description',
|
|
'category',
|
|
'appType',
|
|
'file',
|
|
];
|
|
|
|
const missingFields: string[] = [];
|
|
requiredFields.forEach((field) => {
|
|
if (!data[field]) {
|
|
missingFields.push(field);
|
|
}
|
|
});
|
|
if (missingFields.length > 0) {
|
|
const missingFieldsString = missingFields.join(', ');
|
|
const errorMsg = `Missing fields: ${missingFieldsString}`;
|
|
throw new Error(errorMsg);
|
|
}
|
|
const fee = await getFee('ARBITRARY');
|
|
|
|
await show({
|
|
message: 'Would you like to publish this app?',
|
|
publishFee: fee.fee + ' QORT',
|
|
});
|
|
setIsLoading('Publishing... Please wait.');
|
|
const fileBase64 = await fileToBase64(file);
|
|
await new Promise((res, rej) => {
|
|
window
|
|
.sendMessage('publishOnQDN', {
|
|
data: fileBase64,
|
|
service: appType,
|
|
title,
|
|
name,
|
|
description,
|
|
category,
|
|
tag1,
|
|
tag2,
|
|
tag3,
|
|
tag4,
|
|
tag5,
|
|
uploadType: 'zip',
|
|
})
|
|
.then((response) => {
|
|
if (!response?.error) {
|
|
res(response);
|
|
return;
|
|
}
|
|
rej(response.error);
|
|
})
|
|
.catch((error) => {
|
|
rej(error.message || 'An error occurred');
|
|
});
|
|
});
|
|
setInfoSnack({
|
|
type: 'success',
|
|
message:
|
|
'Successfully published. Please wait a couple minutes for the network to propogate the changes.',
|
|
});
|
|
setOpenSnack(true);
|
|
const dataObj = {
|
|
name: name,
|
|
service: appType,
|
|
metadata: {
|
|
title: title,
|
|
description: description,
|
|
category: category,
|
|
},
|
|
created: Date.now(),
|
|
};
|
|
executeEvent('addTab', {
|
|
data: dataObj,
|
|
});
|
|
} catch (error) {
|
|
setInfoSnack({
|
|
type: 'error',
|
|
message: error?.message || 'Unable to publish app',
|
|
});
|
|
setOpenSnack(true);
|
|
} finally {
|
|
setIsLoading('');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AppsLibraryContainer
|
|
sx={{
|
|
alignItems: 'center',
|
|
height: '100%',
|
|
paddingTop: '30px',
|
|
}}
|
|
>
|
|
<AppsWidthLimiter
|
|
sx={{
|
|
width: 'auto',
|
|
}}
|
|
>
|
|
<AppLibrarySubTitle>Create Apps!</AppLibrarySubTitle>
|
|
|
|
<Spacer height="18px" />
|
|
|
|
<PublishQAppInfo>
|
|
Note: Currently, only one App and Website is allowed per Name.
|
|
</PublishQAppInfo>
|
|
|
|
<Spacer height="18px" />
|
|
|
|
<InputLabel sx={{ fontSize: '14px', marginBottom: '2px' }}>
|
|
Name/App
|
|
</InputLabel>
|
|
|
|
<CustomSelect
|
|
placeholder="Select Name/App"
|
|
displayEmpty
|
|
value={name}
|
|
onChange={(event) => setName(event?.target.value)}
|
|
>
|
|
<CustomMenuItem value="">
|
|
<em
|
|
style={{
|
|
color: theme.palette.text.secondary,
|
|
}}
|
|
>
|
|
Select Name/App
|
|
</em>
|
|
{/* This is the placeholder item */}
|
|
</CustomMenuItem>
|
|
{names.map((name) => {
|
|
return <CustomMenuItem value={name}>{name}</CustomMenuItem>;
|
|
})}
|
|
</CustomSelect>
|
|
|
|
<Spacer height="15px" />
|
|
|
|
<InputLabel sx={{ fontSize: '14px', marginBottom: '2px' }}>
|
|
App service type
|
|
</InputLabel>
|
|
|
|
<CustomSelect
|
|
placeholder="SERVICE TYPE"
|
|
displayEmpty
|
|
value={appType}
|
|
onChange={(event) => setAppType(event?.target.value)}
|
|
>
|
|
<CustomMenuItem value="">
|
|
<em
|
|
style={{
|
|
color: theme.palette.text.secondary,
|
|
}}
|
|
>
|
|
Select App Type
|
|
</em>
|
|
</CustomMenuItem>
|
|
<CustomMenuItem value={'APP'}>App</CustomMenuItem>
|
|
<CustomMenuItem value={'WEBSITE'}>Website</CustomMenuItem>
|
|
</CustomSelect>
|
|
|
|
<Spacer height="15px" />
|
|
|
|
<InputLabel sx={{ fontSize: '14px', marginBottom: '2px' }}>
|
|
Title
|
|
</InputLabel>
|
|
|
|
<InputBase
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
sx={{
|
|
border: `0.5px solid ${theme.palette.action.disabled}`,
|
|
padding: '0px 15px',
|
|
borderRadius: '5px',
|
|
height: '36px',
|
|
width: '100%',
|
|
maxWidth: '450px',
|
|
}}
|
|
placeholder="Title"
|
|
inputProps={{
|
|
'aria-label': 'Title',
|
|
fontSize: '14px',
|
|
fontWeight: 400,
|
|
}}
|
|
/>
|
|
|
|
<Spacer height="15px" />
|
|
|
|
<InputLabel sx={{ fontSize: '14px', marginBottom: '2px' }}>
|
|
Description
|
|
</InputLabel>
|
|
|
|
<InputBase
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
sx={{
|
|
border: `0.5px solid ${theme.palette.action.disabled}`,
|
|
padding: '0px 15px',
|
|
borderRadius: '5px',
|
|
height: '36px',
|
|
width: '100%',
|
|
maxWidth: '450px',
|
|
}}
|
|
placeholder="Description"
|
|
inputProps={{
|
|
'aria-label': 'Description',
|
|
fontSize: '14px',
|
|
fontWeight: 400,
|
|
}}
|
|
/>
|
|
|
|
<Spacer height="15px" />
|
|
|
|
<InputLabel sx={{ fontSize: '14px', marginBottom: '2px' }}>
|
|
Category
|
|
</InputLabel>
|
|
|
|
<CustomSelect
|
|
displayEmpty
|
|
placeholder="Select Category"
|
|
value={category}
|
|
onChange={(event) => setCategory(event?.target.value)}
|
|
>
|
|
<CustomMenuItem value="">
|
|
<em
|
|
style={{
|
|
color: theme.palette.text.secondary,
|
|
}}
|
|
>
|
|
Select Category
|
|
</em>
|
|
</CustomMenuItem>
|
|
{categories?.map((category) => {
|
|
return (
|
|
<CustomMenuItem value={category?.id}>
|
|
{category?.name}
|
|
</CustomMenuItem>
|
|
);
|
|
})}
|
|
</CustomSelect>
|
|
|
|
<Spacer height="15px" />
|
|
|
|
<InputLabel sx={{ fontSize: '14px', marginBottom: '2px' }}>
|
|
Tags
|
|
</InputLabel>
|
|
|
|
<AppPublishTagsContainer>
|
|
<InputBase
|
|
value={tag1}
|
|
onChange={(e) => setTag1(e.target.value)}
|
|
sx={{
|
|
border: `0.5px solid ${theme.palette.action.disabled}`,
|
|
padding: '0px 15px',
|
|
borderRadius: '5px',
|
|
height: '36px',
|
|
width: '100px',
|
|
}}
|
|
placeholder="Tag 1"
|
|
inputProps={{
|
|
'aria-label': 'Tag 1',
|
|
fontSize: '14px',
|
|
fontWeight: 400,
|
|
}}
|
|
/>
|
|
<InputBase
|
|
value={tag2}
|
|
onChange={(e) => setTag2(e.target.value)}
|
|
sx={{
|
|
border: `0.5px solid ${theme.palette.action.disabled}`,
|
|
padding: '0px 15px',
|
|
borderRadius: '5px',
|
|
height: '36px',
|
|
width: '100px',
|
|
}}
|
|
placeholder="Tag 2"
|
|
inputProps={{
|
|
'aria-label': 'Tag 2',
|
|
fontSize: '14px',
|
|
fontWeight: 400,
|
|
}}
|
|
/>
|
|
<InputBase
|
|
value={tag3}
|
|
onChange={(e) => setTag3(e.target.value)}
|
|
sx={{
|
|
border: `0.5px solid ${theme.palette.action.disabled}`,
|
|
padding: '0px 15px',
|
|
borderRadius: '5px',
|
|
height: '36px',
|
|
width: '100px',
|
|
}}
|
|
placeholder="Tag 3"
|
|
inputProps={{
|
|
'aria-label': 'Tag 3',
|
|
fontSize: '14px',
|
|
fontWeight: 400,
|
|
}}
|
|
/>
|
|
<InputBase
|
|
value={tag4}
|
|
onChange={(e) => setTag4(e.target.value)}
|
|
sx={{
|
|
border: `0.5px solid ${theme.palette.action.disabled}`,
|
|
padding: '0px 15px',
|
|
borderRadius: '5px',
|
|
height: '36px',
|
|
width: '100px',
|
|
}}
|
|
placeholder="Tag 4"
|
|
inputProps={{
|
|
'aria-label': 'Tag 4',
|
|
fontSize: '14px',
|
|
fontWeight: 400,
|
|
}}
|
|
/>
|
|
<InputBase
|
|
value={tag5}
|
|
onChange={(e) => setTag5(e.target.value)}
|
|
sx={{
|
|
border: `0.5px solid ${theme.palette.action.disabled}`,
|
|
padding: '0px 15px',
|
|
borderRadius: '5px',
|
|
height: '36px',
|
|
width: '100px',
|
|
}}
|
|
placeholder="Tag 5"
|
|
inputProps={{
|
|
'aria-label': 'Tag 5',
|
|
fontSize: '14px',
|
|
fontWeight: 400,
|
|
}}
|
|
/>
|
|
</AppPublishTagsContainer>
|
|
|
|
<Spacer height="30px" />
|
|
|
|
<PublishQAppInfo>
|
|
Select .zip file containing static content:{' '}
|
|
</PublishQAppInfo>
|
|
|
|
<Spacer height="10px" />
|
|
|
|
<PublishQAppInfo>{`(${
|
|
appType === 'APP' ? '50mb' : '400mb'
|
|
} MB maximum)`}</PublishQAppInfo>
|
|
{file && (
|
|
<>
|
|
<Spacer height="5px" />
|
|
<PublishQAppInfo>{`Selected: (${file?.name})`}</PublishQAppInfo>
|
|
</>
|
|
)}
|
|
|
|
<Spacer height="18px" />
|
|
|
|
<PublishQAppChoseFile {...getRootProps()}>
|
|
{' '}
|
|
<input {...getInputProps()} />
|
|
Choose File
|
|
</PublishQAppChoseFile>
|
|
|
|
<Spacer height="35px" />
|
|
|
|
<PublishQAppCTAButton
|
|
sx={{
|
|
alignSelf: 'center',
|
|
}}
|
|
onClick={publishApp}
|
|
>
|
|
Publish
|
|
</PublishQAppCTAButton>
|
|
</AppsWidthLimiter>
|
|
|
|
<LoadingSnackbar
|
|
open={!!isLoading}
|
|
info={{
|
|
message: isLoading,
|
|
}}
|
|
/>
|
|
<CustomizedSnackbars
|
|
duration={3500}
|
|
open={openSnack}
|
|
setOpen={setOpenSnack}
|
|
info={infoSnack}
|
|
setInfo={setInfoSnack}
|
|
/>
|
|
</AppsLibraryContainer>
|
|
);
|
|
};
|