basic functionality

This commit is contained in:
PhilReact 2024-11-30 10:46:39 +02:00
commit 2260222544
45 changed files with 9526 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

86
README.md Normal file
View File

@ -0,0 +1,86 @@
# Q-Sandbox
**Q-Sandbox** is a developer-focused repository designed to help you explore and experiment with various `qortalRequests` for [Qortal](https://qortal.dev). This sandbox environment allows you to become familiar with the Qortal API, providing a practical space to test and understand its capabilities.
## 🚀 Features
- Test a wide range of `qortalRequests`.
- Experiment in a controlled environment without needing to build your own Q-App.
- Learn the Qortal API in a hands-on way.
- Build confidence and skills for developing on the Qortal ecosystem.
## 📚 Getting Started
1. **Clone the Repository**:
```bash
git clone https://github.com/JustinReact/q-sandbox.git
cd q-sandbox
```
2. **Install Dependencies**:
Ensure you have all necessary dependencies installed:
```bash
npm install
```
3. **Run the Sandbox**:
Start the sandbox environment:
```bash
npm run dev
```
4. **Access the Qortal API**:
Open the project in your preferred editor, and explore preconfigured API requests in the `src/qortalRequests` directory.
## 📖 How to Use
1. Navigate to the `App.jsx` file to find examples of different API requests.
2. Customize these requests to fit your use case or create new ones to explore the full power of the Qortal API.
3. View responses and debug in real time using the Chrome dev console or Qortal Hub console.
## 🛠 Requirements
- **Qortal Node**: A running Qortal node is required for API requests to work, unless you are using a gateway node.
## 💡 Examples
Here are a few examples of what you can do in Q-Sandbox:
1. Fetch user account details:
```javascript
let account = await qortalRequest({
action: "GET_USER_ACCOUNT",
});
```
2. Join a group:
```javascript
const response = await qortalRequest({
action: "JOIN_GROUP",
groupId: groupId,
});
```
3. Send QORT coin:
```javascript
const response = await qortalRequest({
action: "SEND_COIN",
coin: "QORT",
destinationAddress: destinationAddress,
amount: amount, // 1 LTC
fee: 20, // 0.00000020 LTC per byte
});
```
## 🤝 Contributing
Contributions are welcome! If you have ideas for improving the sandbox or additional `qortalRequests` you'd like to see, feel free to open an issue or submit a pull request.
## 🧾 License
This repository is open-source.

BIN
dist.zip Normal file

Binary file not shown.

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Q-Manager</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

4236
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "QManager",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@dnd-kit/core": "^6.2.0",
"@dnd-kit/sortable": "^9.0.0",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.18",
"@mui/material": "^5.14.18",
"@uiw/react-json-view": "^2.0.0-alpha.30",
"copy-to-clipboard": "^3.3.3",
"js-beautify": "^1.15.1",
"prettier": "^3.3.3",
"prism-react-renderer": "^2.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.3.5",
"react-hot-toast": "^2.4.1",
"short-unique-id": "^5.2.0"
},
"devDependencies": {
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@vitejs/plugin-react": "^3.1.0",
"vite": "^4.5.2"
}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

57
src/App.css Normal file
View File

@ -0,0 +1,57 @@
#root {
width: 100vw;
height: 100vh;
margin: 0 auto;
}
.container {
display: flex;
flex-direction: column;
justify-content: center;
}
.flex-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 40px;
width: 100%;
}
.logo-container {
flex-grow: 1; /* Allow the middle div to grow */
display: flex;
justify-content: center; /* Center content inside the middle div */
}
.logo {
width: 150px;
height: 100%;
object-fit: contain;
user-select: none;
}
.card {
display: flex;
flex-direction: column;
gap: 20px;
min-height: 100%;
padding: 20px 35px;
border-radius: 12px;
box-shadow: rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px;
background-color:#ffffff;
}
.info-icon {
margin-left: 10px;
color: #000000;
&:hover {
cursor: pointer;
}
}
button {
outline: none;
border: none;
}

129
src/App.jsx Normal file
View File

@ -0,0 +1,129 @@
import { useCallback, useEffect, useState } from "react";
import { Box, CircularProgress, CssBaseline, MenuItem, Select, ThemeProvider, Tooltip, Typography, createTheme } from "@mui/material";
import "./App.css";
import Container from "./components/Container";
import QSandboxLogo from "./assets/images/QSandboxLogo.png";
import InfoIcon from "@mui/icons-material/Info";
import { categories } from "./constants";
import { ShowCategories } from "./ShowCategories";
import { ShowAction } from "./ShowAction";
import { Manager } from "./Manager";
import { Toaster } from "react-hot-toast";
const theme = createTheme({
palette: {
text: {
primary: "#ffffff", // Set primary text color to white
secondary: "#cccccc", // Optional: Set secondary text color to a lighter shade
},
background: {
default: "rgb(39, 40, 44)", // Optional: Set the default background to white
paper: "rgba(0, 0, 0, 0.1)", // Optional: Set card/paper background to white
},
},
typography: {
allVariants: {
color: "#ffffff", // Ensure all text uses white color by default
},
},
});
function App() {
const [myAddress, setMyaddress] = useState('')
const [isLoading, setIsloading] = useState(true)
const [groups, setGroups] = useState([])
const askForAccountInformation = useCallback(async () => {
try {
const account = await qortalRequest({
action: "GET_USER_ACCOUNT",
});
if(account?.address){
const nameData = await qortalRequest({
action: "GET_ACCOUNT_NAMES",
address: account.address,
});
setMyaddress({...account, name: nameData[0] || ""})
}
} catch (error) {
console.error(error);
} finally {
setIsloading(false)
}
}, []);
const getGroups = useCallback(async (address) => {
try {
const res = await fetch(`/groups/member/${address}`);
const data = await res.json()
setGroups(data)
} catch (error) {
console.error(error);
} finally {
setIsloading(false)
}
}, []);
useEffect(()=> {
askForAccountInformation()
}, [askForAccountInformation])
const handleClose = useCallback(()=> {
setSelectedAction(null)
}, [])
useEffect(()=> {
if(myAddress?.address){
getGroups(myAddress?.address)
}
}, [myAddress?.address])
return (
<ThemeProvider theme={theme}>
<CssBaseline /> {/* Apply the background color globally */}
<div className="container">
{isLoading && (
<Box sx={{
height: '100vh',
width: '100vw',
justifyContent: 'center',
alignItems: 'center',
display: 'flex'
}}>
<CircularProgress />
</Box>
)}
{!isLoading && !myAddress?.name?.name && (
<Box sx={{
height: '100vh',
width: '100vw',
justifyContent: 'center',
alignItems: 'center',
display: 'flex'
}}>
<Typography sx={{
fontSize: '18px'
}}>
To use Q-Manager you need a registered Qortal Name
</Typography>
</Box>
)}
{!isLoading && myAddress?.name?.name && (
<Manager myAddress={myAddress} groups={groups} />
)}
<Toaster
position="top-center"
/>
</div>
</ThemeProvider>
);
}
export default App;

View File

@ -0,0 +1,298 @@
import React, { useState, useRef } from 'react';
import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Menu, MenuItem, Modal, Typography, styled } from '@mui/material';
import PushPinIcon from '@mui/icons-material/PushPin';
import FolderIcon from "@mui/icons-material/Folder";
import DeleteIcon from '@mui/icons-material/Delete';
import DriveFileMoveIcon from '@mui/icons-material/DriveFileMove';
import DriveFileRenameOutlineIcon from '@mui/icons-material/DriveFileRenameOutline';
const CustomStyledMenu = styled(Menu)(({ theme }) => ({
'& .MuiPaper-root': {
backgroundColor: '#f9f9f9',
borderRadius: '12px',
padding: theme.spacing(1),
boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)',
},
'& .MuiMenuItem-root': {
fontSize: '14px',
color: '#444',
transition: '0.3s background-color',
'&:hover': {
backgroundColor: '#f0f0f0',
},
},
}));
export const ContextMenuPinnedFiles = ({ children, removeFile, removeDirectory, type, rename, fileSystem,
moveNode, currentPath, item }) => {
const [menuPosition, setMenuPosition] = useState(null);
const longPressTimeout = useRef(null);
const maxHoldTimeout = useRef(null);
const preventClick = useRef(false);
const [showMoveModal, setShowMoveModal] = useState(false);
const [targetPath, setTargetPath] = useState([]);
const startTouchPosition = useRef({ x: 0, y: 0 }); // Track initial touch position
const handleContextMenu = (event) => {
event.preventDefault();
event.stopPropagation();
preventClick.current = true;
setMenuPosition({
mouseX: event.clientX,
mouseY: event.clientY,
});
};
const handleTouchStart = (event) => {
const { clientX, clientY } = event.touches[0];
startTouchPosition.current = { x: clientX, y: clientY };
longPressTimeout.current = setTimeout(() => {
preventClick.current = true;
event.stopPropagation();
setMenuPosition({
mouseX: clientX,
mouseY: clientY,
});
}, 500);
// Set a maximum hold duration (e.g., 1.5 seconds)
maxHoldTimeout.current = setTimeout(() => {
clearTimeout(longPressTimeout.current);
}, 1500);
};
const handleTouchMove = (event) => {
const { clientX, clientY } = event.touches[0];
const { x, y } = startTouchPosition.current;
// Determine if the touch has moved beyond a small threshold (e.g., 10px)
const movedEnough = Math.abs(clientX - x) > 10 || Math.abs(clientY - y) > 10;
if (movedEnough) {
clearTimeout(longPressTimeout.current);
clearTimeout(maxHoldTimeout.current);
}
};
const handleTouchEnd = (event) => {
clearTimeout(longPressTimeout.current);
clearTimeout(maxHoldTimeout.current);
if (preventClick.current) {
event.preventDefault();
event.stopPropagation();
preventClick.current = false;
}
};
const handleClose = (e) => {
e.preventDefault();
e.stopPropagation();
setMenuPosition(null);
};
const renderDirectoryTree = (directories, currentPathParam = []) => {
return directories.filter((fd)=> fd?.type === 'folder').map((dir) => {
// Construct the fullPath by including the current directory or file name
const fullPath = [...currentPathParam, dir.name];
const currentFullPath = [...currentPathParam, item.name];
// Determine if the current item is the selected one
const isSelected = fullPath.join("/") === targetPath.join("/");
const isCurrentDir = fullPath.join("/") === currentPath.join("/");
const isHoveredDir = fullPath.join("/") === currentFullPath.join("/");
// const isItSelf = dir?.type === 'folder' && dir.name ===
if(dir.type !== "folder" ) return null
return (
<Box key={fullPath.join("/")} sx={{ mb: 1 }}>
{/* Render the current directory or file */}
<ListItem disablePadding>
<ListItemButton
onClick={() => {
if(isCurrentDir || isHoveredDir) return
setTargetPath(fullPath)
}}
sx={{
backgroundColor: (isCurrentDir || isHoveredDir) ? 'inherit' : isSelected ? "#1976d2" : "inherit",
color: (isCurrentDir || isHoveredDir) ? 'inherit' : isSelected ? "#ffffff" : "inherit",
"&:hover": {
backgroundColor: (isCurrentDir || isHoveredDir) ? 'inherit' : "#1976d2",
color: (isCurrentDir || isHoveredDir) ? 'inherit' : "#ffffff"
},
cursor: (isCurrentDir || isHoveredDir) ? 'default' : 'pointer'
}}
>
{dir.type === "folder" && (
<>
<ListItemIcon>
<FolderIcon sx={{ color: isSelected ? "#ffffff" : "inherit" }} />
</ListItemIcon>
<ListItemText
primary={dir.name}
primaryTypographyProps={{
fontWeight: isSelected ? "bold" : "normal",
}}
/>
</>
)}
</ListItemButton>
</ListItem>
{/* Recursively render children if it's a folder */}
{dir.type === "folder" && dir.children && dir.children.length > 0 && (
<Box sx={{ pl: 4 }}>
{renderDirectoryTree(dir.children, fullPath)}
</Box>
)}
</Box>
);
});
};
const openMoveModal = () => {
setShowMoveModal(true);
setMenuPosition(null); // Close the context menu
};
const closeMoveModal = () => {
setShowMoveModal(false);
};
const handleMove = () => {
if (targetPath.length > 0) {
moveNode("name", "type", ["current", "path"], targetPath); // Replace with your logic
closeMoveModal();
}
};
return (
<div
onContextMenu={handleContextMenu}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{ touchAction: 'none' }}
>
{children}
<CustomStyledMenu
disableAutoFocusItem
open={!!menuPosition}
onClose={handleClose}
anchorReference="anchorPosition"
anchorPosition={
menuPosition
? { top: menuPosition.mouseY, left: menuPosition.mouseX }
: undefined
}
onClick={(e) => {
e.stopPropagation();
}}
>
{type === 'file' && (
<MenuItem onClick={(e) => {
handleClose(e);
removeFile()
}}>
<ListItemIcon sx={{ minWidth: '32px' }}>
<DeleteIcon fontSize="small" />
</ListItemIcon>
<Typography variant="inherit" sx={{ fontSize: '14px' }}>
remove file
</Typography>
</MenuItem>
)}
{type === 'folder' && (
<MenuItem onClick={(e) => {
handleClose(e);
removeDirectory()
}}>
<ListItemIcon sx={{ minWidth: '32px' }}>
<PushPinIcon fontSize="small" />
</ListItemIcon>
<Typography variant="inherit" sx={{ fontSize: '14px' }}>
remove directory
</Typography>
</MenuItem>
)}
<MenuItem onClick={(e) => {
handleClose(e);
rename()
}}>
<ListItemIcon sx={{ minWidth: '32px' }}>
<DriveFileRenameOutlineIcon fontSize="small" />
</ListItemIcon>
<Typography variant="inherit" sx={{ fontSize: '14px' }}>
rename
</Typography>
</MenuItem>
<MenuItem
onClick={() =>
openMoveModal()
}
>
<ListItemIcon sx={{ minWidth: "32px" }}>
<DriveFileMoveIcon fontSize="small" />
</ListItemIcon>
<Typography variant="inherit" sx={{ fontSize: "14px" }}>
Move
</Typography>
</MenuItem>
</CustomStyledMenu>
<Modal open={showMoveModal} onClose={closeMoveModal}>
<Box
sx={{
width: 400,
maxWidth: '95%',
margin: "auto",
marginTop: "10%",
backgroundColor: "#27282c",
border: "2px solid #000",
boxShadow: 24,
p: 4,
overflow: 'auto',
maxHeight: '80vh'
}}
>
<Typography variant="h6" component="h2">
Select Target Folder
</Typography>
{renderDirectoryTree(fileSystem)}
<Box mt={2} sx={{
display: 'flex',
gap: '10px',
alignItems: 'center'
}}>
<button onClick={()=> {
moveNode(
item.name,
item.type,
currentPath,
targetPath // Pass the selected targetPath
)
}}>Move Here</button>
<button onClick={closeMoveModal}>Cancel</button>
</Box>
</Box>
</Modal>
</div>
);
};

284
src/File.tsx Normal file
View File

@ -0,0 +1,284 @@
import React, { useEffect, useState } from "react";
import {
Button,
ButtonBase,
Avatar,
Box,
Typography,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
AppBar,
IconButton,
Toolbar,
Select,
MenuItem,
} from "@mui/material";
import { styled } from "@mui/system";
import { Transition } from "./ShowAction";
import CloseIcon from "@mui/icons-material/Close";
import { Label, PUBLISH_QDN_RESOURCE } from "./actions/PUBLISH_QDN_RESOURCE";
import { base64ToUint8Array, uint8ArrayToObject } from "./utils";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
import { Spacer } from "./components/Spacer";
import WarningIcon from "@mui/icons-material/Warning";
import { openToast } from "./components/openToast";
export const SelectedFile = ({
selectedFile,
setSelectedFile,
updateByPath,
mode,
groups,
selectedGroup
}) => {
const [selectedType, setSelectedType] = useState(0);
const [isExpandMore, setIsExpandMore] = useState(false);
const [customFileName, setCustomFileName] = useState(selectedFile?.name)
useEffect(() => {
if (selectedFile?.mimeType?.toLowerCase()?.includes("image")) {
setSelectedType("IMAGE");
} else {
setSelectedType("ATTACHMENT");
}
}, [selectedFile?.mimeType]);
const createEmbedLink = async () => {
const promise = (async ()=> {
try {
if (mode === "public") {
await qortalRequest({
action: "CREATE_AND_COPY_EMBED_LINK",
type: selectedType,
name: selectedFile.qortalName,
identifier: selectedFile.identifier,
service: selectedFile.service,
mimeType: selectedFile?.mimeType,
fileName: customFileName
});
return;
}
if (mode === "group") {
await qortalRequest({
action: "CREATE_AND_COPY_EMBED_LINK",
type: selectedType,
name: selectedFile.qortalName,
identifier: selectedFile.identifier,
service: selectedFile.service,
mimeType: selectedFile?.mimeType,
fileName: customFileName,
encryptionType: 'group',
});
return;
}
const res = await fetch(
`/arbitrary/${selectedFile.service}/${selectedFile.qortalName}/${selectedFile.identifier}?encoding=base64`
);
const base64Data = await res.text();
const decryptedData = await qortalRequest({
action: "DECRYPT_DATA",
encryptedData: base64Data,
});
const decryptToUnit8Array = base64ToUint8Array(decryptedData);
const responseData = uint8ArrayToObject(decryptToUnit8Array);
if (!responseData?.key)
throw new Error("Could not find key in encrypted data");
await qortalRequest({
action: "CREATE_AND_COPY_EMBED_LINK",
type: selectedType,
name: selectedFile.qortalName,
identifier: selectedFile.identifier,
service: selectedFile.service,
encryptionType: 'private',
key: responseData.key,
mimeType: selectedFile?.mimeType,
fileName: customFileName
});
return true
} catch (error) {
throw error
}
})()
await openToast(promise, {
loading: "Downloading resource and fetching link... please wait.",
success: "Copied successfully!",
error: (err) => `Failed to copy: ${err.error || err.message || err}`,
});
};
return (
<div>
<Dialog
fullScreen
open={!!selectedFile}
onClose={() => setSelectedFile(null)}
TransitionComponent={Transition}
PaperProps={{
style: {
backgroundColor: "rgb(39, 40, 44)",
color: "white !important",
},
}}
>
<AppBar
sx={{ position: "relative", backgroundColor: "rgb(39, 40, 44)" }}
>
<Toolbar>
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div">
{selectedFile?.name}
</Typography>
<IconButton
edge="start"
color="inherit"
onClick={() => setSelectedFile(null)}
aria-label="close"
>
<ExpandMoreIcon
sx={{
fontSize: "35px",
}}
/>
</IconButton>
</Toolbar>
</AppBar>
<Box
sx={{
padding: "8px",
display: "flex",
gap: "10px",
alignItems: "flex-end",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
}}
>
<Label>Embed type</Label>
<Select
size="small"
labelId="label-select-category"
id="id-select-category"
value={selectedType}
displayEmpty
onChange={(e) =>
setSelectedType((prev) => {
return e.target.value;
})
}
sx={{
width: "175px",
}}
MenuProps={{
PaperProps: {
sx: {
backgroundColor: "#333333", // Background of the dropdown
color: "#ffffff", // Text color
},
},
}}
>
<MenuItem value={0}>
<em>No type selected</em>
</MenuItem>
<MenuItem value="IMAGE">IMAGE</MenuItem>
<MenuItem value="ATTACHMENT">ATTACHMENT</MenuItem>
</Select>
</Box>
<Button
onClick={createEmbedLink}
disabled={!selectedType}
variant="contained"
>
Copy embed link
</Button>
</Box>
<Spacer height="10px" />
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
padding: '8px'
}}
>
<Label>Filename to show</Label>
<input
type="text"
className="custom-input"
placeholder="filename"
value={customFileName}
onChange={(e) => {
setCustomFileName(e.target.value);
}}
style={{
background: 'transparent',
color: 'white',
maxWidth: '100%'
}}
/>
<Spacer height="10px" />
{mode === 'private' && (
<Box
sx={{
width: "100%",
display: "flex",
gap: "20px",
alignItems: "center",
}}
>
<WarningIcon
sx={{
color: "#ff9800",
}}
/>
<Typography>
Encrypted resource! Be careful where you paste this link.
</Typography>
</Box>
)}
<Spacer height="20px" />
<Box>
<ButtonBase onClick={() => setIsExpandMore((prev) => !prev)}>
<Box
sx={{
padding: "10px",
display: "flex",
gap: "20px",
alignItems: "center",
}}
>
<Typography>Edit publish</Typography>
{isExpandMore ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</Box>
</ButtonBase>
<Spacer height="40px" />
<Box
sx={{
display: isExpandMore ? "block" : "none",
}}
>
<PUBLISH_QDN_RESOURCE
existingFile={selectedFile}
updateByPath={updateByPath}
mode={mode}
groups={groups}
selectedGroup={selectedGroup}
/>
</Box>
</Box>
</Box>
</Dialog>
</div>
);
};

View File

@ -0,0 +1,37 @@
import React from "react";
import { Typography, Breadcrumbs, Link } from "@mui/material";
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
export const FileSystemBreadcrumbs = ({ currentPath, setCurrentPath }) => {
const handleClick = (index) => {
// Update the path to the selected directory
setCurrentPath(currentPath.slice(0, index + 1));
};
return (
<Breadcrumbs separator={<NavigateNextIcon fontSize="small" />}
aria-label="breadcrumb">
{currentPath.map((dir, index) => {
const isLast = index === currentPath.length - 1;
return isLast ? (
<Typography sx={{
fontSize: '16px'
}} key={index} fontWeight="bold">
{dir}
</Typography>
) : (
<Link
key={index}
component="button"
variant="body1"
underline="hover"
color="inherit"
onClick={() => handleClick(index)}
>
{dir}
</Link>
);
})}
</Breadcrumbs>
);
};

1160
src/Manager.tsx Normal file

File diff suppressed because it is too large Load Diff

88
src/ShowAction.jsx Normal file
View File

@ -0,0 +1,88 @@
import {
AppBar,
Box,
Dialog,
IconButton,
Slide,
Toolbar,
Typography,
} from "@mui/material";
import React, { useMemo } from "react";
import { VOTE_ON_POLL } from "./actions/VOTE_ON_POLL";
import { CREATE_POLL } from "./actions/CREATE_POLL";
import { PUBLISH_QDN_RESOURCE } from "./actions/PUBLISH_QDN_RESOURCE";
import { PUBLISH_MULTIPLE_QDN_RESOURCES } from "./actions/PUBLISH_MULTIPLE_QDN_RESOURCES";
import { OPEN_NEW_TAB } from "./actions/OPEN_NEW_TAB";
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
export const Transition = React.forwardRef(function Transition(props, ref) {
return <Slide direction="up" ref={ref} {...props} />;
});
export const ShowAction = ({ selectedAction, handleClose, myName, addNodeByPath, mode , groups, selectedGroup}) => {
const ActionComponent = useMemo(() => {
switch (selectedAction?.action) {
case "PUBLISH_QDN_RESOURCE":
return PUBLISH_QDN_RESOURCE;
case "PUBLISH_MULTIPLE_QDN_RESOURCES":
return PUBLISH_MULTIPLE_QDN_RESOURCES;
default:
return EmptyActionComponent;
}
}, [selectedAction?.action]);
if (!selectedAction) return null;
return (
<div>
<Dialog
fullScreen
open={!!selectedAction}
onClose={handleClose}
TransitionComponent={Transition}
PaperProps={{
style: {
backgroundColor: "rgb(39, 40, 44)",
color: 'white !important'
},
}}
>
<AppBar sx={{ position: "relative", backgroundColor: "rgb(39, 40, 44)"}}>
<Toolbar>
<Typography sx={{ ml: 2, flex: 1 , fontSize: '16px'}} component="div">
{selectedAction?.action === 'PUBLISH_QDN_RESOURCE' && 'Publish file'} {`(${mode})`}
</Typography>
<IconButton
edge="start"
color="inherit"
onClick={handleClose}
aria-label="close"
>
<ExpandMoreIcon sx={{
fontSize: '35px'
}} />
</IconButton>
</Toolbar>
</AppBar>
<Box
sx={{
flexGrow: 1,
overflowY: "auto",
}}
>
<ActionComponent myName={myName} addNodeByPath={addNodeByPath} mode={mode} groups={groups} selectedGroup={selectedGroup} />
</Box>
{/* <LoadingSnackbar
open={false}
info={{
message: "Loading member list with names... please wait.",
}}
/> */}
</Dialog>
</div>
);
};
const EmptyActionComponent = () => {
return null;
};

80
src/ShowCategories.jsx Normal file
View File

@ -0,0 +1,80 @@
import { Box, ButtonBase, Chip, Stack } from "@mui/material";
import React, { useMemo } from "react";
import { actions, categories } from "./constants";
export const ShowCategories = ({ selectedCategory, setSelectedAction }) => {
const actionsToShow = useMemo(() => {
if (selectedCategory === 0) {
return categories?.map((category) => {
return {
category,
actions: Object.keys(actions)
.filter((action) => {
const actionCategory = actions[action].category;
if (actionCategory === category) return true;
return false;
})
.map((key) => {
return {
...actions[key],
action: key,
};
}),
};
});
}
return [
{
category: selectedCategory,
actions: Object.keys(actions)
.filter((action) => {
const actionCategory = actions[action].category;
if (actionCategory === selectedCategory) return true;
return false;
})
.map((key) => {
return {
...actions[key],
action: key,
};
}),
},
];
}, [selectedCategory, actions, categories]);
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "20px",
}}
>
{actionsToShow?.map((category) => {
return (
<>
<div className="row">{category?.category}</div>
<Stack
direction="row"
sx={{
flexWrap: "wrap",
gap: "20px",
}}
>
{category?.actions?.map((action) => {
return (
<ButtonBase key={action.action} onClick={()=> {
setSelectedAction(action)
}}>
<Chip label={action.action} variant="outlined" />
</ButtonBase>
);
})}
</Stack>
</>
);
})}
</Box>
);
};

189
src/actions/CREATE_POLL.jsx Normal file
View File

@ -0,0 +1,189 @@
import React, { useState } from "react";
import { Box, CircularProgress, styled } from "@mui/material";
import { DisplayCode } from "../components/DisplayCode";
import { DisplayCodeResponse } from "../components/DisplayCodeResponse";
import beautify from "js-beautify";
import Button from "../components/Button";
import { OptionsManager } from "../components/OptionsManager";
export const Label = styled("label")(
({ theme }) => `
font-family: 'IBM Plex Sans', sans-serif;
font-size: 14px;
display: block;
margin-bottom: 4px;
font-weight: 400;
`
);
export const formatResponse = (code) => {
return beautify.js(code, {
indent_size: 2, // Number of spaces for indentation
space_in_empty_paren: true, // Add spaces inside parentheses
});
};
export const CREATE_POLL = ({myAddress}) => {
const [isLoading, setIsLoading] = useState(false);
const [requestData, setRequestData] = useState({
pollName: "A test poll 3",
pollDescription: "Test description",
pollOptions: ['option1', 'option2', 'option3'],
pollOwnerAddress: myAddress
});
const [responseData, setResponseData] = useState(
formatResponse(`{
"type": "CREATE_POLL",
"timestamp": 1697285826221,
"reference": "3Svgda6JMSoKW8xQreHRWwXfzWUqCG7NXae5bJDcezbGgK2km8VVbRGZXdEA3Q6LSDvG6hfk1xjXBawpBgxSAa2B",
"fee": "0.01000000",
"signature": "3jU9WpEPAvu9iL3cMfVd2AUmn9AijJRzkGCxVtXfpuUFZubM8AFDcbk5XA9m5AhPfsbMDFkSDzPJnkjeLA5GA59E",
"txGroupId": 0,
"approvalStatus": "NOT_REQUIRED",
"creatorAddress": "Qhxphh7g5iNtxAyLLpPMZzp4X85yf2tVam",
"owner": "QbpZL12Lh7K2y6xPZure4pix5jH6ViVrF2",
"pollName": "A test poll 3",
"description": "test description",
"pollOptions": [
{
"optionName": "option1"
},
{
"optionName": "option2"
},
{
"optionName": "option3"
}
]
}`)
);
const codePollName = `
await qortalRequest({
action: "CREATE_POLL",
pollName: "${requestData?.pollName}",
pollDescription: "${requestData?.pollDescription}",
pollOptions: ${JSON.stringify(requestData.pollOptions)},
pollOwnerAddress: "${requestData?.pollOwnerAddress}"
});
`.trim();
const executeQortalRequest = async () => {
try {
setIsLoading(true)
let account = await qortalRequest({
action: "CREATE_POLL",
pollName: requestData?.pollName,
pollDescription: requestData?.pollDescription,
pollOptions: requestData.pollOptions,
pollOwnerAddress: requestData?.pollOwnerAddress
});
setResponseData(formatResponse(JSON.stringify(account)));
} catch (error) {
setResponseData(formatResponse(JSON.stringify(error)));
console.error(error);
} finally {
setIsLoading(false)
}
};
const handleChange = (e) => {
setRequestData((prev) => {
return {
...prev,
[e.target.name]: e.target.value,
};
});
};
return (
<div
style={{
padding: "10px",
}}
>
<div className="card">
<div className="message-row">
<Label>Poll name</Label>
<input
type="text"
className="custom-input"
placeholder="Poll name"
value={requestData.pollName}
name="pollName"
onChange={handleChange}
/>
<Label>Poll description</Label>
<input
type="text"
className="custom-input"
placeholder="Poll description"
value={requestData.pollDescription}
name="pollDescription"
onChange={handleChange}
/>
<Label>Owner address</Label>
<input
type="text"
className="custom-input"
placeholder="Owner address"
value={requestData.pollOwnerAddress}
name="pollOwnerAddress"
onChange={handleChange}
/>
<Label>Options</Label>
<OptionsManager items={requestData.pollOptions} setItems={(items)=> {
setRequestData((prev)=> {
return {
...prev,
pollOptions: items
}
})
}} />
<Button
name="Create poll"
bgColor="#309ed1"
onClick={executeQortalRequest}
/>
</div>
</div>
<Box
sx={{
display: "flex",
gap: "20px",
}}
>
<Box
sx={{
width: "50%",
}}
>
<h3>Request</h3>
<DisplayCode codeBlock={codePollName} language="javascript" />
</Box>
<Box
sx={{
width: "50%",
}}
>
<h3>Response</h3>
{isLoading ? (
<Box
sx={{
display: "flex",
width: "100%",
justifyContent: "center",
}}
>
<CircularProgress />
</Box>
) : (
<DisplayCodeResponse codeBlock={responseData} language="javascript" />
)}
</Box>
</Box>
</div>
);
};

View File

@ -0,0 +1,146 @@
import React, { useState } from "react";
import { Box, CircularProgress, styled } from "@mui/material";
import { DisplayCode } from "../components/DisplayCode";
import { DisplayCodeResponse } from "../components/DisplayCodeResponse";
import beautify from "js-beautify";
import Button from "../components/Button";
export const Label = styled("label")(
({ theme }) => `
font-family: 'IBM Plex Sans', sans-serif;
font-size: 14px;
display: block;
margin-bottom: 4px;
font-weight: 400;
`
);
export const formatResponse = (code) => {
return beautify.js(code, {
indent_size: 2, // Number of spaces for indentation
space_in_empty_paren: true, // Add spaces inside parentheses
});
};
export const OPEN_NEW_TAB = () => {
const [requestData, setRequestData] = useState({
qortalLink: 'qortal://APP/Q-Tube'
});
const [isLoading, setIsLoading] = useState(false);
const [responseData, setResponseData] = useState(
formatResponse(`{
"type": "OPEN_NEW_TAB",
"timestamp": 1697286687406,
"reference": "3jU9WpEPAvu9iL3cMfVd2AUmn9AijJRzkGCxVtXfpuUFZubM8AFDcbk5XA9m5AhPfsbMDFkSDzPJnkjeLA5GA59E",
"fee": "0.01000000",
"signature": "3QJ1EUvX3rskVNaP3RWvJwb9DsGgHPvneWqBWS62PCcuCj5N4Ei9Tr4nFj4nQeMqMU2qNkVD3Sb59e7iUWkawH3s",
"txGroupId": 0,
"approvalStatus": "NOT_REQUIRED",
"creatorAddress": "Qhxphh7g5iNtxAyLLpPMZzp4X85yf2tVam",
"voterPublicKey": "C5spuNU1BAHZDEkxF3wnrAPRDuNrVceaDJ6tDKitenko",
"pollName": "A test poll 3",
"optionIndex": 1
}`)
);
const codePollName = `
await qortalRequest({
action: "OPEN_NEW_TAB",
qortalLink: "${requestData?.qortalLink}",
});
`.trim();
const executeQortalRequest = async () => {
try {
setIsLoading(true)
// let account = await qortalRequest({
// action: "OPEN_NEW_TAB",
// qortalLink: requestData?.qortalLink,
// });
let account = await qortalRequest({
action: "CREATE_AND_COPY_EMBED_LINK",
name: 'SHOULD MINTING REQUIRE A NAME?',
type: 'POLL',
ref: 'qortal://APP/Qombo'
});
setResponseData(formatResponse(JSON.stringify(account)));
} catch (error) {
setResponseData(formatResponse(JSON.stringify(error)));
console.error(error);
} finally {
setIsLoading(false)
}
};
const handleChange = (e) => {
setRequestData((prev) => {
return {
...prev,
[e.target.name]: e.target.value,
};
});
};
return (
<div
style={{
padding: "10px",
}}
>
<div className="card">
<div className="message-row">
<Label>Qortal Link</Label>
<input
type="text"
className="custom-input"
placeholder="Qortal Link"
value={requestData.qortalLink}
name="qortalLink"
onChange={handleChange}
/>
<Button
name="Open tab"
bgColor="#309ed1"
onClick={executeQortalRequest}
/>
</div>
</div>
<Box
sx={{
display: "flex",
gap: "20px",
}}
>
<Box
sx={{
width: "50%",
}}
>
<h3>Request</h3>
<DisplayCode codeBlock={codePollName} language="javascript" />
</Box>
<Box
sx={{
width: "50%",
}}
>
<h3>Response</h3>
{isLoading ? (
<Box
sx={{
display: "flex",
width: "100%",
justifyContent: "center",
}}
>
<CircularProgress />
</Box>
) : (
<DisplayCodeResponse codeBlock={responseData} language="javascript" />
)}
</Box>
</Box>
</div>
);
};

View File

@ -0,0 +1,211 @@
import React, { useState } from "react";
import { Box, ButtonBase, CircularProgress, MenuItem, Select, styled } from "@mui/material";
import { DisplayCode } from "../components/DisplayCode";
import { DisplayCodeResponse } from "../components/DisplayCodeResponse";
import beautify from "js-beautify";
import Button from "../components/Button";
import { useDropzone } from "react-dropzone";
import { services } from "../constants";
export const Label = styled("label")(
({ theme }) => `
font-family: 'IBM Plex Sans', sans-serif;
font-size: 14px;
display: block;
margin-bottom: 4px;
font-weight: 400;
`
);
export const formatResponse = (code) => {
return beautify.js(code, {
indent_size: 2, // Number of spaces for indentation
space_in_empty_paren: true, // Add spaces inside parentheses
});
};
export const PUBLISH_MULTIPLE_QDN_RESOURCES = () => {
const [requestData, setRequestData] = useState({
service: "DOCUMENT",
identifier: "test-identifier",
});
const { getRootProps, getInputProps } = useDropzone({
maxFiles: 1,
onDrop: async (acceptedFiles) => {
const fileSelected = acceptedFiles[0];
if (fileSelected) {
setFile(fileSelected);
}
},
});
const [isLoading, setIsLoading] = useState(false);
const [file, setFile] = useState(null);
const [responseData, setResponseData] = useState(
formatResponse(`{
"type": "PUBLISH_MULTIPLE_QDN_RESOURCES",
"timestamp": 1697286687406,
"reference": "3jU9WpEPAvu9iL3cMfVd2AUmn9AijJRzkGCxVtXfpuUFZubM8AFDcbk5XA9m5AhPfsbMDFkSDzPJnkjeLA5GA59E",
"fee": "0.01000000",
"signature": "3QJ1EUvX3rskVNaP3RWvJwb9DsGgHPvneWqBWS62PCcuCj5N4Ei9Tr4nFj4nQeMqMU2qNkVD3Sb59e7iUWkawH3s",
"txGroupId": 0,
"approvalStatus": "NOT_REQUIRED",
"creatorAddress": "Qhxphh7g5iNtxAyLLpPMZzp4X85yf2tVam",
"voterPublicKey": "C5spuNU1BAHZDEkxF3wnrAPRDuNrVceaDJ6tDKitenko",
"pollName": "A test poll 3",
"optionIndex": 1
}`)
);
const codePollName = `
await qortalRequest({
action: "PUBLISH_MULTIPLE_QDN_RESOURCES",
service: "${requestData?.service}",
identifier: "${requestData?.identifier}", // optional
data64: ${requestData?.data64 ? `"${requestData?.data64}"` : "empty"}, // base64 string. Remove this param if you are putting in a FILE object
file: ${file ? 'FILE OBJECT' : "empty"} // File Object. Remove this param if you are putting in a base64 string.
});
`.trim();
const executeQortalRequest = async () => {
try {
setIsLoading(true);
let account = await qortalRequest({
action: "PUBLISH_MULTIPLE_QDN_RESOURCES",
service: requestData?.service,
identifier: requestData?.identifier,
file,
data64: requestData?.data64
});
setResponseData(formatResponse(JSON.stringify(account)));
} catch (error) {
setResponseData(formatResponse(JSON.stringify(error)));
console.error(error);
} finally {
setIsLoading(false);
}
};
const handleChange = (e) => {
setRequestData((prev) => {
return {
...prev,
[e.target.name]: e.target.value,
};
});
};
return (
<div
style={{
padding: "10px",
}}
>
<div className="card">
<div className="message-row">
<Label>Service</Label>
<Select
size="small"
labelId="label-select-category"
id="id-select-category"
value={requestData?.service}
displayEmpty
onChange={(e) => setRequestData((prev)=> {
return {
...prev,
service: e.target.value
}
})}
sx={{
width: '300px'
}}
>
<MenuItem value={0}>
<em>No service selected</em>
</MenuItem>
{services?.map((service) => {
return (
<MenuItem key={service.name} value={service.name}>
{`${service.name} - max ${service.sizeLabel}`}
</MenuItem>
);
})}
</Select>
<Label>Index option</Label>
<input
type="text"
className="custom-input"
placeholder="identifier"
value={requestData.identifier}
name="identifier"
onChange={handleChange}
/>
<button {...getRootProps()} style={{
width: '150px'
}}>
<input {...getInputProps()} />
Select file
</button>
{file && (
<ButtonBase sx={{
width: '150px'
}} onClick={()=> {
setFile(null)
}}>Remove file</ButtonBase>
)}
<Label>Base64 string</Label>
<input
type="text"
className="custom-input"
name="data64"
value={requestData?.data64}
onChange={handleChange}
/>
<Button
name="Publish"
bgColor="#309ed1"
onClick={executeQortalRequest}
/>
</div>
</div>
<Box
sx={{
display: "flex",
gap: "20px",
}}
>
<Box
sx={{
width: "50%",
}}
>
<h3>Request</h3>
<DisplayCode codeBlock={codePollName} language="javascript" />
</Box>
<Box
sx={{
width: "50%",
}}
>
<h3>Response</h3>
{isLoading ? (
<Box
sx={{
display: "flex",
width: "100%",
justifyContent: "center",
}}
>
<CircularProgress />
</Box>
) : (
<DisplayCodeResponse
codeBlock={responseData}
language="javascript"
/>
)}
</Box>
</Box>
</div>
);
};

View File

@ -0,0 +1,383 @@
import React, { useState } from "react";
import {
Box,
ButtonBase,
CircularProgress,
MenuItem,
Select,
Typography,
styled,
} from "@mui/material";
import { DisplayCode } from "../components/DisplayCode";
import { DisplayCodeResponse } from "../components/DisplayCodeResponse";
import ShortUniqueId from "short-unique-id";
import Button from "../components/Button";
import { useDropzone } from "react-dropzone";
import { privateServices, services } from "../constants";
import { fileToBase64 } from "../utils";
import toast from 'react-hot-toast';
import { openToast } from "../components/openToast";
const uid = new ShortUniqueId({ length: 10 });
export const Label = styled("label")(
({ theme }) => `
font-family: 'IBM Plex Sans', sans-serif;
font-size: 14px;
display: block;
margin-bottom: 4px;
font-weight: 400;
`
);
export const PUBLISH_QDN_RESOURCE = ({ addNodeByPath, myName, mode, existingFile, updateByPath , groups, selectedGroup}) => {
const [requestData, setRequestData] = useState({
service: existingFile?.service || "DOCUMENT"
});
const { getRootProps, getInputProps } = useDropzone({
maxFiles: 1,
onDrop: async (acceptedFiles) => {
const fileSelected = acceptedFiles[0];
if (fileSelected) {
setFile(fileSelected);
}
},
});
const [isLoading, setIsLoading] = useState(false);
const [file, setFile] = useState(null);
const executeQortalRequestGroup = async () => {
const promise = (async () => {
try {
if (!file) throw new Error('Please select a file')
if(!selectedGroup) throw new Error('Please select a group')
const findGroup = groups?.find((group)=> group.groupId === selectedGroup)
if(!findGroup) throw new Error('Cannot find group')
setIsLoading(true);
const fileExtension = file?.name?.includes(".") ? file.name.split(".").pop() : "";
const fileTitle =
file?.name
?.split(".")
.slice(0, -1)
.join(".")
.replace(/ /g, "_")
.slice(0, 20) || "Untitled";
const filename = fileExtension ? `${fileTitle}.${fileExtension}` : fileTitle;
const constructedIdentifier = existingFile?.identifier || `grp-q-manager-858-${uid.rnd()}`;
const base64File = await fileToBase64(file);
const encryptedData = await qortalRequest({
action: "ENCRYPT_QORTAL_GROUP_DATA",
data64: base64File,
groupId: selectedGroup
});
if(!encryptedData) throw new Error('Unable to encrypt data')
let account = await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
service: existingFile?.service || requestData?.service,
identifier: constructedIdentifier,
data64: encryptedData,
externalEncrypt: true,
});
if (account?.identifier) {
if (!!existingFile) {
updateByPath({
...existingFile,
mimeType: file?.type,
});
setFile("");
return true; // Success
}
addNodeByPath(
undefined,
{
type: "file",
name: filename,
mimeType: file?.type,
qortalName: myName,
identifier: constructedIdentifier,
service: requestData?.service,
group: selectedGroup,
groupName: findGroup?.groupName
},
undefined
);
return true; // Success
} else {
throw new Error("Unable to publish the file");
}
} catch (error) {
console.error("Error:", error);
throw error; // Ensure the error is propagated to the toast
} finally {
setIsLoading(false);
}
})();
await openToast(promise, {
loading: "Publishing the file...",
success: "File published successfully!",
error: (err) => `Failed to publish: ${err?.error || err?.message || "An unknown error occurred"}`,
});
};
const executeQortalRequestPrivate = async () => {
const promise = (async () => {
try {
if (!file) return;
setIsLoading(true);
const fileExtension = file?.name?.includes(".") ? file.name.split(".").pop() : "";
const fileTitle =
file?.name
?.split(".")
.slice(0, -1)
.join(".")
.replace(/ /g, "_")
.slice(0, 20) || "Untitled";
const filename = fileExtension ? `${fileTitle}.${fileExtension}` : fileTitle;
const constructedIdentifier = existingFile?.identifier || `p-q-manager-858-${uid.rnd()}`;
const base64File = await fileToBase64(file);
const encryptedData = await qortalRequest({
action: "ENCRYPT_DATA_WITH_SHARING_KEY",
data64: base64File,
});
if(!encryptedData) throw new Error('Unable to encrypt data')
let account = await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
service: existingFile?.service || requestData?.service,
identifier: constructedIdentifier,
data64: encryptedData,
});
if (account?.identifier) {
if (!!existingFile) {
updateByPath({
...existingFile,
mimeType: file?.type,
});
setFile("");
return true; // Success
}
addNodeByPath(
undefined,
{
type: "file",
name: filename,
mimeType: file?.type,
qortalName: myName,
identifier: constructedIdentifier,
service: requestData?.service,
},
undefined
);
return true; // Success
} else {
throw new Error("Unable to publish the file");
}
} catch (error) {
console.error("Error:", error);
throw error; // Ensure the error is propagated to the toast
} finally {
setIsLoading(false);
}
})();
await openToast(promise, {
loading: "Publishing the file...",
success: "File published successfully!",
error: (err) => `Failed to publish: ${err?.error || err?.message || "An unknown error occurred"}`,
});
};
const executeQortalRequest = async () => {
try {
setIsLoading(true);
const promise = (async () => {
const fileExtension = file?.name?.includes(".")
? file.name.split(".").pop()
: "";
const fileTitle =
file?.name
?.split(".")
.slice(0, -1)
.join(".")
.replace(/ /g, "_")
.slice(0, 20) || "Untitled";
const filename = fileExtension
? `${fileTitle}.${fileExtension}`
: fileTitle;
const constructedIdentifier =
existingFile?.identifier || `q-manager-858-${uid.rnd()}`;
const account = await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
service: existingFile?.service || requestData?.service,
identifier: constructedIdentifier,
file,
filename,
});
if (account?.identifier) {
if (!!existingFile) {
updateByPath({
...existingFile,
mimeType: file?.type,
});
setFile("");
return;
}
addNodeByPath(
undefined,
{
type: "file",
name: filename,
mimeType: file?.type,
qortalName: myName,
identifier: constructedIdentifier,
service: requestData?.service,
},
undefined
);
if(!existingFile){
setFile(null)
}
return;
} else {
throw new Error("Unable to publish the file");
}
})();
await openToast(promise, {
loading: "Publishing the file...",
success: "File published successfully!",
error: (err) => `Failed to publish: ${err.error || err.message || err}`,
});
} catch (error) {
console.error("Error during publishing:", error);
} finally {
setIsLoading(false);
}
};
return (
<div
style={{
padding: "10px",
}}
>
<div
className="card"
style={{
background: "rgba(0, 0, 0, 0.1)",
}}
>
<div className="message-row">
<Label>Service</Label>
<Select
disabled={!!existingFile}
size="small"
labelId="label-select-category"
id="id-select-category"
value={requestData?.service}
displayEmpty
onChange={(e) =>
setRequestData((prev) => {
return {
...prev,
service: e.target.value,
};
})
}
sx={{
width: "300px",
}}
MenuProps={{
PaperProps: {
sx: {
backgroundColor: "#333333", // Background of the dropdown
color: "#ffffff", // Text color
},
},
}}
>
<MenuItem value={0}>
<em>No service selected</em>
</MenuItem>
{(mode === 'private' ? privateServices : services)?.map((service) => {
return (
<MenuItem key={service.name} value={service.name}>
{`${service.name} - max ${service.sizeLabel}`}
</MenuItem>
);
})}
</Select>
<button
{...getRootProps()}
style={{
width: "150px",
}}
>
<input {...getInputProps()} />
Select file
</button>
<Typography>{file?.name}</Typography>
{file && (
<Button
name="Remove file"
bgColor="pink"
onClick={() => {
setFile(null);
}}
>
Remove file
</Button>
)}
<Button
name={!!existingFile ? "Edit Publish" :"Publish"}
bgColor="#309ed1"
onClick={()=> {
if(mode ==='group'){
executeQortalRequestGroup()
}
else if(mode === 'private'){
executeQortalRequestPrivate()
} else {
executeQortalRequest()
}
}}
/>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,152 @@
import React, { useState } from "react";
import { Box, CircularProgress, styled } from "@mui/material";
import { DisplayCode } from "../components/DisplayCode";
import { DisplayCodeResponse } from "../components/DisplayCodeResponse";
import beautify from "js-beautify";
import Button from "../components/Button";
export const Label = styled("label")(
({ theme }) => `
font-family: 'IBM Plex Sans', sans-serif;
font-size: 14px;
display: block;
margin-bottom: 4px;
font-weight: 400;
`
);
export const formatResponse = (code) => {
return beautify.js(code, {
indent_size: 2, // Number of spaces for indentation
space_in_empty_paren: true, // Add spaces inside parentheses
});
};
export const VOTE_ON_POLL = () => {
const [requestData, setRequestData] = useState({
pollName: "myPoll",
optionIndex: 1,
});
const [isLoading, setIsLoading] = useState(false);
const [responseData, setResponseData] = useState(
formatResponse(`{
"type": "VOTE_ON_POLL",
"timestamp": 1697286687406,
"reference": "3jU9WpEPAvu9iL3cMfVd2AUmn9AijJRzkGCxVtXfpuUFZubM8AFDcbk5XA9m5AhPfsbMDFkSDzPJnkjeLA5GA59E",
"fee": "0.01000000",
"signature": "3QJ1EUvX3rskVNaP3RWvJwb9DsGgHPvneWqBWS62PCcuCj5N4Ei9Tr4nFj4nQeMqMU2qNkVD3Sb59e7iUWkawH3s",
"txGroupId": 0,
"approvalStatus": "NOT_REQUIRED",
"creatorAddress": "Qhxphh7g5iNtxAyLLpPMZzp4X85yf2tVam",
"voterPublicKey": "C5spuNU1BAHZDEkxF3wnrAPRDuNrVceaDJ6tDKitenko",
"pollName": "A test poll 3",
"optionIndex": 1
}`)
);
const codePollName = `
await qortalRequest({
action: "VOTE_ON_POLL",
pollName: "${requestData?.pollName}",
optionIndex: ${requestData?.optionIndex},
});
`.trim();
const executeQortalRequest = async () => {
try {
setIsLoading(true)
let account = await qortalRequest({
action: "VOTE_ON_POLL",
pollName: requestData?.pollName,
optionIndex: requestData?.optionIndex,
});
setResponseData(formatResponse(JSON.stringify(account)));
} catch (error) {
setResponseData(formatResponse(JSON.stringify(error)));
console.error(error);
} finally {
setIsLoading(false)
}
};
const handleChange = (e) => {
setRequestData((prev) => {
return {
...prev,
[e.target.name]: e.target.value,
};
});
};
return (
<div
style={{
padding: "10px",
}}
>
<div className="card">
<div className="message-row">
<Label>Poll name</Label>
<input
type="text"
className="custom-input"
placeholder="Poll name"
value={requestData.pollName}
name="pollName"
onChange={handleChange}
/>
<Label>Index option</Label>
<input
type="number"
className="custom-input"
placeholder="Index option"
value={requestData.optionIndex}
name="optionIndex"
onChange={handleChange}
/>
<Button
name="Vote"
bgColor="#309ed1"
onClick={executeQortalRequest}
/>
</div>
</div>
<Box
sx={{
display: "flex",
gap: "20px",
}}
>
<Box
sx={{
width: "50%",
}}
>
<h3>Request</h3>
<DisplayCode codeBlock={codePollName} language="javascript" />
</Box>
<Box
sx={{
width: "50%",
}}
>
<h3>Response</h3>
{isLoading ? (
<Box
sx={{
display: "flex",
width: "100%",
justifyContent: "center",
}}
>
<CircularProgress />
</Box>
) : (
<DisplayCodeResponse codeBlock={responseData} language="javascript" />
)}
</Box>
</Box>
</div>
);
};

Binary file not shown.

Binary file not shown.

BIN
src/assets/fonts/Oxygen.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

18
src/components/Button.jsx Normal file
View File

@ -0,0 +1,18 @@
import React from "react";
import "./button.css";
const Button = ({ name, onClick, bgColor }) => {
return (
<div className="button-container">
<button
style={{ backgroundColor: bgColor }}
className="button"
onClick={onClick}
>
{name}
</button>
</div>
);
};
export default Button;

View File

@ -0,0 +1,214 @@
import { styled } from "@mui/system";
import { Box, Typography } from "@mui/material";
import { QortalSVG } from "./QortalSVG";
import ContentPasteTwoToneIcon from "@mui/icons-material/ContentPasteTwoTone";
export const MainBox = styled(Box)(({ theme }) => ({
position: "relative",
minHeight: "100px",
width: "100%",
padding: "20px 35px",
[theme.breakpoints.down("sm")]: {
padding: 0
}
}));
export const SectionContainer = styled(Box)(({ theme }) => ({
width: "100%",
display: "flex",
gap: "50px"
}));
export const ParagraphContainer = styled(Box)(({ theme }) => ({
width: "100%",
display: "flex",
flexDirection: "column"
}));
export const SectionTitleText = styled(Typography)(({ theme }) => ({
fontFamily: "Oxygen",
fontWeight: "400",
letterSpacing: "0.3px",
fontSize: "32px",
[theme.breakpoints.down("sm")]: {
textAlign: "center",
lineHeight: "40px",
marginTop: "10px",
overflowWrap: "anywhere"
}
}));
export const SubTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Oxygen",
fontWeight: "400",
letterSpacing: "0.3px",
fontSize: "24px",
marginTop: "10px",
[theme.breakpoints.down("sm")]: {
textAlign: "center",
lineHeight: "40px",
marginTop: "10px",
overflowWrap: "anywhere"
}
}));
export const SectionParagraph = styled(Typography)(({ theme }) => ({
marginTop: "20px",
fontFamily: "Inter",
fontSize: "19.5px",
lineHeight: "33px",
letterSpacing: "0.2px",
fontWeight: theme.palette.mode === "dark" ? "300" : "400",
textIndent: "20px",
width: "fit-content",
color: theme.palette.text.primary,
"& a": {
textDecoration: "none",
color: theme.palette.secondary.main,
transition: "all 0.2s ease-in-out",
"&:hover": {
cursor: "pointer",
filter: "brightness(1.2)"
}
}
}));
export const Code = styled("code")(({ theme }) => ({
padding: "0.2em 0.4em",
margin: 0,
fontSize: "16.5px",
backgroundColor: "#c7f3ff",
borderRadius: "3px",
fontFamily: "'Courier New', monospace",
color: "#333"
}));
export const CodeWrapper = styled(Box)(({ theme }) => ({
display: "flex",
position: "relative"
}));
export const CopyCodeIcon = styled(ContentPasteTwoToneIcon)(({ theme }) => ({
position: "absolute",
right: "20px",
top: "25px",
fontSize: "20px",
color: "white",
cursor: "pointer"
}));
export const RowContainer = styled(Box)(({ theme }) => ({
display: "flex",
gap: "10px",
alignItems: "center"
}));
export const ColumnContainer = styled(Box)(({ theme }) => ({
display: "flex",
gap: "10px",
flexDirection: "column",
padding: "10px 0"
}));
export const InformationParagraph = styled(Typography)(({ theme }) => ({
fontSize: "18px",
lineHeight: "28px",
fontFamily: "Roboto",
color: theme.palette.mode === "light" ? "#425061" : "#bfc0c2",
[theme.breakpoints.down("sm")]: {
overflowWrap: "anywhere"
}
}));
export const CustomUnorderedList = styled("ul")(({ theme }) => ({
listStyleType: "none",
margin: "10px 0 0 0"
}));
export const CustomListItem = styled("li")(({ theme }) => ({
fontFamily: "Inter",
fontSize: "18px",
lineHeight: "33px",
letterSpacing: "0.2px",
fontWeight: "400",
margin: "5px 0"
}));
export const QortalIcon = styled(QortalSVG)(({ theme }) => ({
transform: "translateY(3px)",
marginRight: "15px"
}));
export const ServiceItem = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
gap: "15px",
fontFamily: "Inter",
fontSize: "19.5px",
lineHeight: "33px",
letterSpacing: "0.2px",
fontWeight: "400",
color: theme.palette.text.primary
}));
export const DisplayCodePre = styled("pre")(({ theme }) => ({
padding: "30px 10px 20px 10px",
overflowX: "auto",
borderRadius: "7px",
width: "100%",
maxHeight: "800px",
whiteSpace: "pre-wrap",
overflowWrap: "anywhere",
textAlign: "left",
"&::-webkit-scrollbar-track": {
backgroundColor: theme.palette.mode === "light" ? "#282c34" : "#011627"
},
"&::-webkit-scrollbar-track:hover": {
backgroundColor: theme.palette.mode === "light" ? "#282c34" : "#011627"
},
"&::-webkit-scrollbar": {
width: "16px",
height: "10px",
backgroundColor: theme.palette.mode === "light" ? "#282c34" : "#011627"
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.mode === "light" ? "#545a64" : "#072f50",
borderRadius: "8px",
backgroundClip: "content-box",
border: "4px solid transparent"
},
"&::-webkit-scrollbar-thumb:hover": {
backgroundColor: theme.palette.mode === "light" ? "#4b5058" : "#06233b"
}
}));
export const DisplayCodeResponsePre = styled("pre")(({ theme }) => ({
padding: "10px",
overflowX: "auto",
borderRadius: "7px",
maxHeight: "800px",
width: "100%",
whiteSpace: "pre-wrap",
overflowWrap: "anywhere",
textAlign: "left",
"&::-webkit-scrollbar-track": {
backgroundColor: theme.palette.mode === "light" ? "#f6f8fa" : "#292d3e"
},
"&::-webkit-scrollbar-track:hover": {
backgroundColor: theme.palette.mode === "light" ? "#f6f8fa" : "#292d3e"
},
"&::-webkit-scrollbar": {
width: "16px",
height: "10px",
backgroundColor: theme.palette.mode === "light" ? "#f6f8fa" : "#292d3e"
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.mode === "light" ? "#d3d9e1" : "#414763",
borderRadius: "8px",
backgroundClip: "content-box",
border: "4px solid transparent"
},
"&::-webkit-scrollbar-thumb:hover": {
backgroundColor: theme.palette.mode === "light" ? "#b7bcc4" : "#40455f"
}
}));

View File

@ -0,0 +1,488 @@
import { useState } from "react";
import "./container.css";
import Button from "./Button";
const Container = ({
destinationAddress,
setDestinationAddress,
amount,
setAmount,
selectedCoin,
setSelectedCoin,
selectedCoinWallet,
setSelectedCoinWallet,
selectedCoinWalletInfo,
setSelectedCoinWalletInfo,
message,
setMessage,
messageReceiver,
setMessageReceiver,
name,
setName,
userName,
setUserName,
service,
setService,
base64,
setBase64,
identifier,
setIdentifier,
groupId,
setGroupId,
getProfileProperty,
setGetProfileProperty,
setProfilePropertyName,
setSetProfilePropertyName,
setProfilePropertyObjectKey,
setSetProfilePropertyObjectKey,
setProfilePropertyObjectValue,
setSetProfilePropertyObjectValue,
buttonData
}) => {
const [coinType] = useState(["QORT", "LTC", "DOGE", "RVN", "ARRR"]);
const [coinTypeWalletInfo] = useState(["BTC", "LTC", "DOGE", "RVN"]);
return (
<div className="wrapper">
<div className="main-row">
<div className="card">
<div className="row">Send Coin (QORT)</div>
<input
type="text"
className="custom-input"
placeholder="Destination Address"
value={destinationAddress}
onChange={(e) => {
setDestinationAddress(e.target.value);
}}
/>
<input
type="number"
placeholder="QORT"
className="custom-number-input"
value={amount}
onChange={(e) => {
setAmount(e.target.value);
}}
/>
{buttonData
.filter(button => button.name === "Send coin to address")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
<div className="card">
<div className="row">Check for balance</div>
<div className="coin-type-row">
{coinType.map((coin, index) => {
return (
<div
onClick={() => {
setSelectedCoin(coin);
}}
style={{ backgroundColor: selectedCoin === coin && "#13ecff" }}
className="coin"
key={index}
>
{coin}
</div>
);
})}
</div>
{buttonData
.filter(button => button.name === "Get wallet balance")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
<div className="card">
<div className="row">Send Message</div>
<div className="message-row">
<input
type="text"
className="custom-input"
placeholder="Message Receiver Address"
value={messageReceiver}
onChange={(e) => {
setMessageReceiver(e.target.value);
}}
/>
<input
type="text"
placeholder="Message"
className="custom-input"
value={message}
onChange={(e) => {
setMessage(e.target.value);
}}
/>
</div>
{buttonData
.filter(button => button.name === "Send a private chat message")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
<div className="card">
<div className="row">Create a poll</div>
<div className="message-row">
{buttonData
.filter(button => button.name === "Create a poll")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
</div>
<div className="card">
<div className="row">Vote on a poll</div>
<div className="message-row">
{buttonData
.filter(button => button.name === "Vote on a poll")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
</div>
<div className="card">
<div className="row">Deploy an AT</div>
<div className="message-row">
{buttonData
.filter(button => button.name === "Deploy an AT")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
</div>
<div className="card">
<div className="row">Get user wallet info</div>
<div className="coin-type-row">
{coinTypeWalletInfo.map((coin, index) => {
return (
<div
onClick={() => {
setSelectedCoinWalletInfo(coin);
}}
style={{ backgroundColor: selectedCoinWalletInfo === coin && "#2600ffdf" }}
className="coin"
key={index}
>
{coin}
</div>
);
})}
</div>
{buttonData
.filter(button => button.name === "Get user wallet info")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
</div>
<div className="main-row">
<div className="card">
<div className="row">Publish</div>
<div className="message-row">
<input
type="text"
className="custom-input"
placeholder="Your name"
value={name}
onChange={(e) => {
setName(e.target.value);
}}
/>
<input
type="text"
placeholder="Service"
className="custom-input"
value={service}
onChange={(e) => {
setService(e.target.value);
}}
/>
<input
type="text"
placeholder="Service"
className="custom-input"
value={base64}
onChange={(e) => {
setBase64(e.target.value);
}}
/>
<input
type="text"
placeholder="Identifier"
className="custom-input"
value={identifier}
onChange={(e) => {
setIdentifier(e.target.value);
}}
/>
{buttonData
.filter(button => button.name === "Publish QDN resource")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
</div>
<div className="card">
<div className="row">Join Group</div>
<div className="message-row">
<input
type="text"
className="custom-input"
placeholder="GroupId"
value={groupId}
onChange={(e) => {
setGroupId(e.target.value);
}}
/>
{buttonData
.filter(button => button.name === "Join Group")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
</div>
<div className="card">
<div className="row">Send local notification</div>
{buttonData
.filter(button => button.name === "Send local notification")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
<div className="card">
<div className="row">Get user wallet</div>
<div className="coin-type-row">
{coinType.map((coin, index) => {
return (
<div
onClick={() => {
setSelectedCoinWallet(coin);
}}
style={{ backgroundColor: selectedCoinWallet === coin && "#ffa600" }}
className="coin"
key={index}
>
{coin}
</div>
);
})}
</div>
{buttonData
.filter(button => button.name === "Get user wallet")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
<div className="card">
<div className="row">Get day summary</div>
<div className="message-row">
{buttonData
.filter(button => button.name === "Get day summary")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
</div>
<div className="card">
<div className="row">Get friends list</div>
<div className="message-row">
{buttonData
.filter(button => button.name === "Get friends list")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
</div>
<div className="card">
<div className="row">Encrypt data</div>
<div className="message-row">
{buttonData
.filter(button => button.name === "Encrypt data")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
</div>
</div>
<div className="main-row">
<div className="card">
<div className="row">Open user profile</div>
<div className="message-row">
<input
type="text"
className="custom-input"
placeholder="User name"
value={userName}
onChange={(e) => {
setUserName(e.target.value);
}}
/>
</div>
{buttonData
.filter(button => button.name === "Open user profile")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
<div className="card">
<div className="row">Get Profile Data Property</div>
<div className="message-row">
<input
type="text"
className="custom-input"
placeholder="Profile Property"
value={getProfileProperty}
onChange={(e) => {
setGetProfileProperty(e.target.value);
}}
/>
{buttonData
.filter(button => button.name === "Get profile property")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
</div>
<div className="card">
<div className="row">Set Profile Data Property Name</div>
<div className="message-row">
<input
type="text"
className="custom-input"
placeholder="Profile Property Name"
value={setProfilePropertyName}
onChange={(e) => {
setSetProfilePropertyName(e.target.value);
}}
/>
</div>
<div className="row">Set Profile Data Object Key</div>
<div className="message-row">
<input
type="text"
className="custom-input"
placeholder="Profile Property Object Key"
value={setProfilePropertyObjectKey}
onChange={(e) => {
setSetProfilePropertyObjectKey(e.target.value);
}}
/>
</div>
<div className="row">Set Profile Data Object Value</div>
<div className="message-row">
<input
type="text"
className="custom-input"
placeholder="Profile Property Object Value"
value={setProfilePropertyObjectValue}
onChange={(e) => {
setSetProfilePropertyObjectValue(e.target.value);
}}
/>
</div>
{buttonData
.filter(button => button.name === "Set profile property")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
<div className="card">
<div className="row">Get Logged In User Address</div>
<div className="message-row">
{buttonData
.filter(button => button.name === "Get address of logged in account")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
</div>
<div className="card">
<div className="row">Open a new tab</div>
<div className="message-row">
{buttonData
.filter(button => button.name === "Open a new tab")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
</div>
<div className="card">
<div className="row">Get Permission for Notifications from User</div>
<div className="message-row">
{buttonData
.filter(button => button.name === "Get Permission for Notifications from User")
.map((button, index) => {
return (
<Button key={index} bgColor={button.bgColor} onClick={button.onClick} name={button.name} />
)
})
}
</div>
</div>
</div>
</div>
);
};
export default Container;

View File

@ -0,0 +1,69 @@
import { useState } from "react";
import { Highlight, themes } from "prism-react-renderer";
import copy from "copy-to-clipboard";
import { Tooltip } from "@mui/material";
import { CodeWrapper, CopyCodeIcon, DisplayCodePre } from "./Common-styles";
import { useTheme } from "@mui/material";
export const DisplayCode = ({ codeBlock, language = "javascript" }) => {
const [copyText, setCopyText] = useState("Copy");
const handleCopy = () => {
try {
copy(codeBlock);
setCopyText("Copied!");
setTimeout(() => {
setCopyText("Copy!");
}, 3000);
} catch (error) {}
};
return (
<CodeWrapper>
<Tooltip title={copyText} arrow placement="top">
<CopyCodeIcon onClick={handleCopy} />
</Tooltip>
<Highlight
theme={
themes.palenight
}
code={codeBlock}
language="javascript"
>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<DisplayCodePre
className={`${className} stripe-code-block`}
style={{ ...style, margin: 0 }}
>
{tokens.map((line, i) => (
<div
key={i}
{...getLineProps({ line, key: i })}
style={{ display: "flex" }}
>
<span
style={{
display: "inline-block",
width: "2em",
userSelect: "none",
opacity: "0.5",
marginRight: "8px",
fontSize: "14px"
}}
>
{i + 1}
</span>
<span style={{ flex: 1, fontSize: "18px" }}>
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token, key })} />
))}
</span>
</div>
))}
</DisplayCodePre>
)}
</Highlight>
</CodeWrapper>
);
};

View File

@ -0,0 +1,71 @@
import { useState } from "react";
import { Highlight, themes } from "prism-react-renderer";
import { Typography, Box, useTheme } from "@mui/material";
import { CodeWrapper, DisplayCodeResponsePre } from "./Common-styles";
export const DisplayCodeResponse = ({
codeBlock,
language = "javascript"
}) => {
const theme = useTheme();
const [copyText, setCopyText] = useState("Copy");
return (
<CodeWrapper>
<Highlight
theme={
themes.palenight
}
code={codeBlock}
language="javascript"
>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<DisplayCodeResponsePre
className={`${className} stripe-code-block`}
style={{ ...style, margin: 0 }}
>
<Box
sx={{
padding: "5px",
backgroundColor:
theme.palette.mode === "dark" ? "#767ea0" : "#d3d9e1",
color: theme.palette.text.primary,
borderTopRightRadius: "7px",
borderTopLeftRadius: "7px",
marginBottom: "10px"
}}
>
<Typography>RESPONSE</Typography>
</Box>
{tokens.map((line, i) => (
<div
key={i}
{...getLineProps({ line, key: i })}
style={{ display: "flex" }}
>
<span
style={{
display: "inline-block",
userSelect: "none",
opacity: "0.5",
marginRight: "8px",
fontSize: "16px"
}}
>
{i + 1}
</span>
<span style={{ flex: 1, fontSize: "18px" }}>
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token, key })} />
))}
</span>
</div>
))}
</DisplayCodeResponsePre>
)}
</Highlight>
</CodeWrapper>
);
};

View File

@ -0,0 +1,72 @@
import React, { useState } from "react";
import { TextField, Button, Chip, Box, Stack, IconButton } from "@mui/material";
import { Edit, Delete } from "@mui/icons-material";
export function OptionsManager({ items, setItems, label = "Item" }) {
const [inputValue, setInputValue] = useState("");
const [editIndex, setEditIndex] = useState(null);
const handleAddOrUpdateItem = () => {
if (inputValue.trim() === "") return;
if (editIndex !== null) {
// Update item
const updatedItems = [...items];
updatedItems[editIndex] = inputValue;
setItems(updatedItems);
setEditIndex(null);
} else {
// Add new item
if (!items.includes(inputValue)) {
setItems([...items, inputValue]);
}
}
setInputValue(""); // Clear the input
};
const handleDeleteItem = (index) => {
setItems(items.filter((_, i) => i !== index));
};
const handleEditItem = (index) => {
setInputValue(items[index]);
setEditIndex(index);
};
return (
<Box>
<Stack direction="row" spacing={2} alignItems="center">
<TextField
label={editIndex !== null ? `Edit ${label}` : `Add ${label}`}
variant="outlined"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<Button
variant="contained"
onClick={handleAddOrUpdateItem}
>
{editIndex !== null ? "Update" : "Add"}
</Button>
</Stack>
<Box mt={2}>
{items.map((item, index) => (
<Chip
key={index}
label={item}
onDelete={() => handleDeleteItem(index)}
onClick={() => handleEditItem(index)}
// deleteIcon={
// <IconButton onClick={() => handleEditItem(index)} size="small">
// <Edit fontSize="small" />
// </IconButton>
// }
sx={{ margin: 0.5 }}
/>
))}
</Box>
</Box>
);
}

View File

@ -0,0 +1,45 @@
export const QortalSVG = ({
color,
height,
width,
className
}) => {
return (
<svg
className={className}
fill={color}
version="1.0"
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
viewBox="0 0 695.000000 754.000000"
preserveAspectRatio="xMidYMid meet"
>
<g
transform="translate(0.000000,754.000000) scale(0.100000,-0.100000)"
stroke="none"
>
<path
d="M3035 7289 c-374 -216 -536 -309 -1090 -629 -409 -236 -1129 -652
-1280 -739 -82 -48 -228 -132 -322 -186 l-173 -100 0 -1882 0 -1883 38 -24
c20 -13 228 -134 462 -269 389 -223 1779 -1026 2335 -1347 127 -73 268 -155
314 -182 56 -32 95 -48 118 -48 33 0 207 97 991 552 l102 60 0 779 c0 428 -2
779 -4 779 -3 0 -247 -140 -543 -311 -296 -170 -544 -308 -553 -306 -8 2 -188
104 -400 226 -212 123 -636 368 -942 544 l-558 322 0 1105 c0 1042 1 1106 18
1116 9 6 107 63 217 126 110 64 421 243 690 398 270 156 601 347 736 425 l247
142 363 -210 c200 -115 551 -317 779 -449 228 -132 495 -286 594 -341 l178
-102 -6 -1889 -6 -1888 23 14 c12 8 318 185 680 393 l657 379 0 1887 0 1886
-77 46 c-43 25 -458 264 -923 532 -465 268 -1047 605 -1295 748 -646 373 -965
557 -968 557 -1 0 -182 -104 -402 -231z"
/>
<path
d="M3010 4769 c-228 -133 -471 -274 -540 -313 l-125 -72 0 -633 0 -632
295 -171 c162 -94 407 -235 544 -315 137 -79 255 -142 261 -139 6 2 200 113
431 247 230 133 471 272 534 308 l115 66 2 635 3 635 -536 309 c-294 169 -543
310 -552 312 -9 2 -204 -105 -432 -237z"
/>
</g>
</svg>
);
};

15
src/components/Spacer.tsx Normal file
View File

@ -0,0 +1,15 @@
import { Box } from "@mui/material";
export const Spacer = ({ height, width, ...props }: any) => {
return (
<Box
sx={{
height: height ? height : '0px',
display: 'flex',
flexShrink: 0,
width: width ? width : '0px',
...(props || {})
}}
/>
);
};

27
src/components/button.css Normal file
View File

@ -0,0 +1,27 @@
.button-container {
display: flex;
align-items: center;
}
.button {
color: black;
font-family: Roboto, sans-serif;
letter-spacing: 0.3px;
font-weight: 300;
font-size: 16px;
border: none;
border-radius: 5px;
padding: 8px 10px;
transition: all 0.3s ease-in-out;
outline: none;
}
.button:hover {
cursor: pointer;
filter: brightness(1.1);
box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px;
}
.button:focus {
outline: none;
}

View File

@ -0,0 +1,131 @@
.wrapper {
display: grid;
grid-template-columns: repeat(3, 1fr);
justify-content: space-evenly;
width: 100%;
align-items: flex-start;
}
/* For medium screens, use 2 columns */
@media (max-width: 1500px) {
.wrapper {
grid-template-columns: repeat(2, 1fr);
row-gap: 40px;
}
}
/* For small screens, use 1 column */
@media (max-width: 1000px) {
.wrapper {
grid-template-columns: 1fr; /* All items in a single column */
row-gap: 40px;
}
}
.main-row {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 50px;
}
.row {
display: flex;
font-family: Oxygen, sans-serif;
letter-spacing: 0.3px;
font-size: 20px;
font-weight: bold;
align-items: center;
color: white;
}
.custom-input {
width: 320px;
outline: 0;
border-width: 0 0 2px;
border-color: #5a71b1;
background-color: #f8fafb;
padding: 10px;
font-family: Raleway, sans-serif;
font-weight: 300;
letter-spacing: 0.3px;
font-size: 16px;
color: black;
border-radius: 3px;
}
.custom-input::selection {
background-color: #60688f;
color: white;
}
.custom-input::-moz-selection {
background-color: #60688f;
color: white;
}
.custom-number-input {
width: 120px;
border-width: 1px;
border-color: #c0bfbf;
background-color: #f8fafb;
font-family: Roboto, sans-serif;
font-weight: 300;
letter-spacing: 0.3px;
font-size: 20px;
color: #403c3c;
border: 2px solid whitesmoke;
border-radius: 3px;
padding: 10px 15px;
}
.custom-number-input::selection {
background-color: #919bf4;
color: white;
}
.custom-number-input::-moz-selection {
background-color: #919bf4;
color: white;
}
.custom-number-input:focus {
box-shadow: none;
border-color: #5a71b1;
outline: none;
}
.coin-type-row {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 10px;
}
.coin {
padding: 5px 10px;
font-family: Raleway, sans-serif;
font-size: 16px;
background-color: rgb(221, 221, 221);
border: none;
color: black;
border-radius: 5px;
transition: all 0.3s ease-in-out;
outline: none;
user-select: none;
}
.coin:hover {
background-color: #cccbcc;
box-shadow: rgba(50, 50, 93, 0.25) 0px 2px 5px -1px, rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;
cursor: pointer;
}
.message-row {
display: flex;
flex-direction: column;
gap: 15px;
}

View File

@ -0,0 +1,45 @@
import toast from "react-hot-toast";
export const openToast = (promise, messages) => {
return toast.promise(
promise,
{
loading: (
<div style={{ display: "flex", alignItems: "center" }}>
<span role="img" aria-label="loading-icon">
</span>
<span style={{ marginLeft: 8 }}>{messages.loading}</span>
</div>
),
success: (
<div style={{ display: "flex", alignItems: "center" }}>
<span role="img" aria-label="success-icon">
👏
</span>
<span style={{ marginLeft: 8 }}>{messages.success}</span>
</div>
),
error: (err) => (
<div style={{ display: "flex", alignItems: "center" }}>
<span role="img" aria-label="error-icon">
</span>
<span style={{ marginLeft: 8 }}>
{typeof messages.error === "function"
? messages.error(err)
: messages.error || `Error: ${err.message || err}`}
</span>
</div>
),
},
{
style: {
borderRadius: "10px",
background: "#333",
color: "#fff",
},
}
);
};

266
src/constants.js Normal file
View File

@ -0,0 +1,266 @@
export const categories = ['Payment', 'Account', 'Poll', 'List', 'Data', 'Chat', 'Group', 'AT', 'System', 'Other']
export const actions = {
GET_USER_ACCOUNT: {
category: "Account",
isTx: false,
requiresApproval: true,
isGatewayDisabled: false,
explanation: ""
},
DECRYPT_DATA: {
category: "Data",
isTx: false,
requiresApproval: false,
isGatewayDisabled: false,
explaination: ""
},
SEND_COIN: {
category: "Payment",
isTx: true,
requiresApproval: true,
isGatewayDisabled: null,
isGatewayDisabledExplanation: "Only QORT is permitted through gateways"
},
GET_LIST_ITEMS: {
category: "List",
isTx: false,
requiresApproval: true,
isGatewayDisabled: true,
},
ADD_LIST_ITEMS: {
category: "List",
isTx: false,
requiresApproval: true,
isGatewayDisabled: true,
},
DELETE_LIST_ITEM: {
category: "List",
isTx: false,
requiresApproval: true,
isGatewayDisabled: true,
},
VOTE_ON_POLL: {
category: "Poll",
isTx: true,
requiresApproval: true,
isGatewayDisabled: false,
},
CREATE_POLL: {
category: "Poll",
isTx: true,
requiresApproval: true,
isGatewayDisabled: false,
},
SEND_CHAT_MESSAGE: {
category: "Chat",
isTx: true,
txType: 'Unconfirmed',
requiresApproval: true,
isGatewayDisabled: false,
},
JOIN_GROUP: {
category: "Group",
isTx: true,
requiresApproval: true,
isGatewayDisabled: false,
},
DEPLOY_AT: {
category: "AT",
isTx: true,
requiresApproval: true,
isGatewayDisabled: false,
},
GET_USER_WALLET: {
category: "Account",
isTx: false,
requiresApproval: true,
isGatewayDisabled: false,
},
GET_WALLET_BALANCE: {
category: "Account",
isTx: false,
requiresApproval: true,
isGatewayDisabled: false,
},
GET_USER_WALLET_INFO: {
category: "Account",
isTx: false,
requiresApproval: true,
isGatewayDisabled: false,
},
GET_CROSSCHAIN_SERVER_INFO: {
category: "Payment",
isTx: false,
requiresApproval: false,
isGatewayDisabled: false,
},
GET_TX_ACTIVITY_SUMMARY: {
category: "Payment",
isTx: false,
requiresApproval: false,
isGatewayDisabled: false,
},
GET_FOREIGN_FEE: {
category: "Payment",
isTx: false,
requiresApproval: false,
isGatewayDisabled: false,
},
UPDATE_FOREIGN_FEE: {
category: "Payment",
isTx: false,
requiresApproval: false, // TODO
isGatewayDisabled: true,
},
GET_SERVER_CONNECTION_HISTORY: {
category: "Payment",
isTx: false,
requiresApproval: false,
isGatewayDisabled: false,
},
SET_CURRENT_FOREIGN_SERVER: {
category: "Payment",
isTx: false,
requiresApproval: false, // TODO
isGatewayDisabled: true,
},
ADD_FOREIGN_SERVER: {
category: "Payment",
isTx: false,
requiresApproval: false, // TODO
isGatewayDisabled: true,
},
REMOVE_FOREIGN_SERVER: {
category: "Payment",
isTx: false,
requiresApproval: false, // TODO
isGatewayDisabled: true,
},
GET_DAY_SUMMARY: {
category: "Payment",
isTx: false,
requiresApproval: false,
isGatewayDisabled: false,
},
CREATE_TRADE_BUY_ORDER: {
category: "Payment",
isTx: true,
requiresApproval: true,
isGatewayDisabled: false,
},
CREATE_TRADE_SELL_ORDER: {
category: "Payment",
isTx: true,
requiresApproval: true,
isGatewayDisabled: true,
},
CANCEL_TRADE_SELL_ORDER: {
category: "Payment",
isTx: true,
requiresApproval: true,
isGatewayDisabled: true,
},
IS_USING_GATEWAY: {
category: "System",
isTx: false,
requiresApproval: false,
isGatewayDisabled: false,
},
ADMIN_ACTION: {
category: "System",
isTx: false,
requiresApproval: true,
isGatewayDisabled: true,
},
SIGN_TRANSACTION: {
category: "Other",
isTx: true,
requiresApproval: true,
isGatewayDisabled: false,
},
PUBLISH_MULTIPLE_QDN_RESOURCES: {
category: "Data",
isTx: true,
requiresApproval: true,
isGatewayDisabled: false,
},
PUBLISH_QDN_RESOURCE: {
category: "Data",
isTx: true,
requiresApproval: true,
isGatewayDisabled: false,
},
ENCRYPT_DATA: {
category: "Data",
isTx: false,
requiresApproval: false,
isGatewayDisabled: false,
},
OPEN_NEW_TAB: {
category: "System",
isTx: false,
requiresApproval: false,
isGatewayDisabled: false,
},
};
export const services = [
{ name: "ARBITRARY_DATA", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "QCHAT_ATTACHMENT", sizeInBytes: 1 * 1024 * 1024, sizeLabel: "1 MB" },
{ name: "ATTACHMENT", sizeInBytes: 50 * 1024 * 1024, sizeLabel: "50 MB" },
{ name: "FILE", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "FILES", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "CHAIN_DATA", sizeInBytes: 239, sizeLabel: "239 B" },
{ name: "WEBSITE", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "IMAGE", sizeInBytes: 10 * 1024 * 1024, sizeLabel: "10 MB" },
{ name: "THUMBNAIL", sizeInBytes: 500 * 1024, sizeLabel: "500 KB" },
{ name: "QCHAT_IMAGE", sizeInBytes: 500 * 1024, sizeLabel: "500 KB" },
{ name: "VIDEO", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "AUDIO", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "QCHAT_AUDIO", sizeInBytes: 10 * 1024 * 1024, sizeLabel: "10 MB" },
{ name: "QCHAT_VOICE", sizeInBytes: 10 * 1024 * 1024, sizeLabel: "10 MB" },
{ name: "VOICE", sizeInBytes: 10 * 1024 * 1024, sizeLabel: "10 MB" },
{ name: "PODCAST", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "BLOG", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "BLOG_POST", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "BLOG_COMMENT", sizeInBytes: 500 * 1024, sizeLabel: "500 KB" },
{ name: "DOCUMENT", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "LIST", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "PLAYLIST", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "APP", sizeInBytes: 50 * 1024 * 1024, sizeLabel: "50 MB" },
{ name: "METADATA", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "JSON", sizeInBytes: 25 * 1024, sizeLabel: "25 KB" },
{ name: "GIF_REPOSITORY", sizeInBytes: 25 * 1024 * 1024, sizeLabel: "25 MB" },
{ name: "STORE", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "PRODUCT", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "OFFER", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "COUPON", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "CODE", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "PLUGIN", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "EXTENSION", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "GAME", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "ITEM", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "NFT", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "DATABASE", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "SNAPSHOT", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" },
{ name: "COMMENT", sizeInBytes: 500 * 1024, sizeLabel: "500 KB" },
{ name: "CHAIN_COMMENT", sizeInBytes: 239, sizeLabel: "239 B" },
{ name: "MAIL", sizeInBytes: 1 * 1024 * 1024, sizeLabel: "1 MB" },
{ name: "MESSAGE", sizeInBytes: 1 * 1024 * 1024, sizeLabel: "1 MB" }
];
export const privateServices = [
{ name: "QCHAT_ATTACHMENT_PRIVATE", sizeInBytes: 1 * 1024 * 1024, sizeLabel: "1 MB" },
{ name: "ATTACHMENT_PRIVATE", sizeInBytes: 50 * 1024 * 1024, sizeLabel: "50 MB" },
{ name: "FILE_PRIVATE", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" }, // Default size
{ name: "IMAGE_PRIVATE", sizeInBytes: 10 * 1024 * 1024, sizeLabel: "10 MB" },
{ name: "VIDEO_PRIVATE", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" }, // Default size
{ name: "AUDIO_PRIVATE", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" }, // Default size
{ name: "VOICE_PRIVATE", sizeInBytes: 10 * 1024 * 1024, sizeLabel: "10 MB" },
{ name: "DOCUMENT_PRIVATE", sizeInBytes: 500 * 1024 * 1024, sizeLabel: "500 MB" }, // Default size
{ name: "MAIL_PRIVATE", sizeInBytes: 5 * 1024 * 1024, sizeLabel: "5 MB" },
{ name: "MESSAGE_PRIVATE", sizeInBytes: 1 * 1024 * 1024, sizeLabel: "1 MB" }
];

51
src/global.d.ts vendored Normal file
View File

@ -0,0 +1,51 @@
// 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
prefix?: boolean
exactMatchNames?: boolean
}
declare function qortalRequest(options: QortalRequestOptions): Promise<any>
declare function qortalRequestWithTimeout(
options: QortalRequestOptions,
time: number
): Promise<any>
declare global {
interface Window {
_qdnBase: any // Replace 'any' with the appropriate type if you know it
_qdnTheme: string
}
}

132
src/index.css Normal file
View File

@ -0,0 +1,132 @@
@font-face {
font-family: "Raleway";
src: url("./assets/fonts/Raleway.ttf") format("truetype");
}
@font-face {
font-family: "Oxygen";
src: url("./assets/fonts/Oxygen.ttf") format("truetype");
}
@font-face {
font-family: "Kanit";
src: url("./assets/fonts/Kanit-Regular.ttf") format("truetype");
}
@font-face {
font-family: "Kanit";
src: url("./assets/fonts/Kanit-Bold.ttf") format("truetype");
}
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
background-color: rgb(39, 40, 44);
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-track:hover {
background-color: transparent;
}
::-webkit-scrollbar {
width: 14px;
height: 10px;
}
::-webkit-scrollbar-thumb {
background-color: #444444;
border-radius: 8px;
background-clip: content-box;
border: 4px solid transparent;
}
body::-webkit-scrollbar-thumb:hover {
border: 4px solid #60688f;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
/* button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
} */
p {
margin: 0;
}
.MuiTooltip-tooltip {
background-color: #2c2b2b !important;
color: #ffffff !important;
font-family: Raleway, sans-serif !important;
font-size: 18px !important;
padding: 10px 15px !important;
font-weight: 400 !important;
letter-spacing: 0.3px !important;
line-height: 25px !important;
margin-left: 2px !important;
}
.MuiTooltip-arrow {
color: #2c2b2b !important;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

10
src/main.jsx Normal file
View File

@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

63
src/storage.ts Normal file
View File

@ -0,0 +1,63 @@
const initializeDB = () => {
return new Promise((resolve, reject) => {
const request = indexedDB.open("FileSystemDB", 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains("fileSystemQManager")) {
db.createObjectStore("fileSystemQManager", { keyPath: "id" }); // `id` will be used as the key
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = (event) => reject(event.target.error);
});
};
export const saveFileSystemQManagerToDB = async (fileSystemQManager) => {
try {
const db = await initializeDB();
const transaction = db.transaction("fileSystemQManager", "readwrite");
const store = transaction.objectStore("fileSystemQManager");
// Clear existing data
store.clear();
// Save new data
store.put({ id: 1, data: fileSystemQManager });
return new Promise((resolve, reject) => {
transaction.oncomplete = () => resolve("FileSystemQManager saved successfully");
transaction.onerror = (event) => reject(event.target.error);
});
} catch (error) {
console.error("Error saving fileSystemQManager to IndexedDB:", error);
}
};
export const getFileSystemQManagerFromDB = async () => {
try {
const db = await initializeDB();
const transaction = db.transaction("fileSystemQManager", "readonly");
const store = transaction.objectStore("fileSystemQManager");
return new Promise((resolve, reject) => {
const request = store.get(1);
request.onsuccess = (event) => {
if (event.target.result) {
resolve(event.target.result.data);
} else {
resolve(null); // No data found
}
};
request.onerror = (event) => reject(event.target.error);
});
} catch (error) {
console.error("Error retrieving fileSystemQManager from IndexedDB:", error);
}
};

54
src/useModal.tsx Normal file
View File

@ -0,0 +1,54 @@
import { useRef, useState } from 'react';
interface State {
isShow: boolean;
}
export const useModal = () => {
const [state, setState] = useState<State>({
isShow: false,
});
const [type, setType] = useState('');
const promiseConfig = useRef<any>(null);
const show = async (data) => {
setType(data)
return new Promise((resolve, reject) => {
promiseConfig.current = {
resolve,
reject,
};
setState({
isShow: true,
});
});
};
const hide = () => {
setState({
isShow: false,
});
setType('')
};
const onOk = (payload:any) => {
const { resolve } = promiseConfig.current;
setType('')
hide();
resolve(payload);
};
const onCancel = () => {
const { reject } = promiseConfig.current;
hide();
reject();
setType('')
};
return {
show,
onOk,
onCancel,
isShow: state.isShow,
type
};
};

138
src/utils.ts Normal file
View File

@ -0,0 +1,138 @@
export function objectToBase64(obj: Object) {
// Step 1: Convert the object to a JSON string
const jsonString = JSON.stringify(obj)
// Step 2: Create a Blob from the JSON string
const blob = new Blob([jsonString], { type: 'application/json' })
// Step 3: Create a FileReader to read the Blob as a base64-encoded string
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = () => {
if (typeof reader.result === 'string') {
// Remove 'data:application/json;base64,' prefix
const base64 = reader.result.replace(
'data:application/json;base64,',
''
)
resolve(base64)
} else {
reject(new Error('Failed to read the Blob as a base64-encoded string'))
}
}
reader.onerror = () => {
reject(reader.error)
}
reader.readAsDataURL(blob)
})
}
export function base64ToUint8Array(base64: string) {
const binaryString = atob(base64)
const len = binaryString.length
const bytes = new Uint8Array(len)
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
return bytes
}
export function uint8ArrayToObject(uint8Array: Uint8Array) {
// Decode the byte array using TextDecoder
const decoder = new TextDecoder()
const jsonString = decoder.decode(uint8Array)
// Convert the JSON string back into an object
const obj = JSON.parse(jsonString)
return obj
}
export const handleImportClick = async () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.base64,.txt';
// Create a promise to handle file selection and reading synchronously
return await new Promise((resolve, reject) => {
fileInput.onchange = () => {
const file = fileInput.files[0];
if (!file) {
reject(new Error('No file selected'));
return;
}
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target.result); // Resolve with the file content
};
reader.onerror = () => {
reject(new Error('Error reading file'));
};
reader.readAsText(file); // Read the file as text (Base64 string)
};
// Trigger the file input dialog
fileInput.click();
});
}
class Semaphore {
constructor(count) {
this.count = count
this.waiting = []
}
acquire() {
return new Promise(resolve => {
if (this.count > 0) {
this.count--
resolve()
} else {
this.waiting.push(resolve)
}
})
}
release() {
if (this.waiting.length > 0) {
const resolve = this.waiting.shift()
resolve()
} else {
this.count++
}
}
}
let semaphore = new Semaphore(1)
let reader = new FileReader()
export const fileToBase64 = (file) => new Promise(async (resolve, reject) => {
if (!reader) {
reader = new FileReader()
}
await semaphore.acquire()
reader.readAsDataURL(file)
reader.onload = () => {
const dataUrl = reader.result
if (typeof dataUrl === "string") {
const base64String = dataUrl.split(',')[1]
reader.onload = null
reader.onerror = null
resolve(base64String)
} else {
reader.onload = null
reader.onerror = null
reject(new Error('Invalid data URL'))
}
semaphore.release()
}
reader.onerror = (error) => {
reader.onload = null
reader.onerror = null
reject(error)
semaphore.release()
}
})

1
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

8
vite.config.js Normal file
View File

@ -0,0 +1,8 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
base: "",
});