mirror of
https://github.com/Qortal/Q-Manager.git
synced 2025-06-14 23:11:21 +00:00
basic functionality
This commit is contained in:
commit
2260222544
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
86
README.md
Normal 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.
|
13
index.html
Normal file
13
index.html
Normal 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
4236
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal 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
1
public/vite.svg
Normal 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
57
src/App.css
Normal 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
129
src/App.jsx
Normal 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;
|
||||
|
||||
|
298
src/ContextMenuPinnedFiles.tsx
Normal file
298
src/ContextMenuPinnedFiles.tsx
Normal 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
284
src/File.tsx
Normal 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>
|
||||
);
|
||||
};
|
37
src/FileSystemBreadcrumbs.tsx
Normal file
37
src/FileSystemBreadcrumbs.tsx
Normal 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
1160
src/Manager.tsx
Normal file
File diff suppressed because it is too large
Load Diff
88
src/ShowAction.jsx
Normal file
88
src/ShowAction.jsx
Normal 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
80
src/ShowCategories.jsx
Normal 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
189
src/actions/CREATE_POLL.jsx
Normal 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>
|
||||
);
|
||||
};
|
146
src/actions/OPEN_NEW_TAB.jsx
Normal file
146
src/actions/OPEN_NEW_TAB.jsx
Normal 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>
|
||||
);
|
||||
};
|
211
src/actions/PUBLISH_MULTIPLE_QDN_RESOURCES.jsx
Normal file
211
src/actions/PUBLISH_MULTIPLE_QDN_RESOURCES.jsx
Normal 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>
|
||||
);
|
||||
};
|
383
src/actions/PUBLISH_QDN_RESOURCE.jsx
Normal file
383
src/actions/PUBLISH_QDN_RESOURCE.jsx
Normal 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>
|
||||
);
|
||||
};
|
152
src/actions/VOTE_ON_POLL.jsx
Normal file
152
src/actions/VOTE_ON_POLL.jsx
Normal 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>
|
||||
);
|
||||
};
|
BIN
src/assets/fonts/Kanit-Bold.ttf
Normal file
BIN
src/assets/fonts/Kanit-Bold.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Kanit-Regular.ttf
Normal file
BIN
src/assets/fonts/Kanit-Regular.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Oxygen.ttf
Normal file
BIN
src/assets/fonts/Oxygen.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Raleway.ttf
Normal file
BIN
src/assets/fonts/Raleway.ttf
Normal file
Binary file not shown.
BIN
src/assets/images/QSandboxLogo.png
Normal file
BIN
src/assets/images/QSandboxLogo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
18
src/components/Button.jsx
Normal file
18
src/components/Button.jsx
Normal 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;
|
214
src/components/Common-styles.jsx
Normal file
214
src/components/Common-styles.jsx
Normal 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"
|
||||
}
|
||||
}));
|
488
src/components/Container.jsx
Normal file
488
src/components/Container.jsx
Normal 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;
|
69
src/components/DisplayCode.jsx
Normal file
69
src/components/DisplayCode.jsx
Normal 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>
|
||||
);
|
||||
};
|
71
src/components/DisplayCodeResponse.tsx
Normal file
71
src/components/DisplayCodeResponse.tsx
Normal 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>
|
||||
);
|
||||
};
|
72
src/components/OptionsManager.tsx
Normal file
72
src/components/OptionsManager.tsx
Normal 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>
|
||||
);
|
||||
}
|
45
src/components/QortalSVG.tsx
Normal file
45
src/components/QortalSVG.tsx
Normal 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
15
src/components/Spacer.tsx
Normal 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
27
src/components/button.css
Normal 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;
|
||||
}
|
131
src/components/container.css
Normal file
131
src/components/container.css
Normal 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;
|
||||
}
|
45
src/components/openToast.jsx
Normal file
45
src/components/openToast.jsx
Normal 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
266
src/constants.js
Normal 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
51
src/global.d.ts
vendored
Normal 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
132
src/index.css
Normal 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
10
src/main.jsx
Normal 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
63
src/storage.ts
Normal 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
54
src/useModal.tsx
Normal 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
138
src/utils.ts
Normal 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
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
8
vite.config.js
Normal file
8
vite.config.js
Normal 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: "",
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user