mirror of
https://github.com/Qortal/names.git
synced 2025-06-14 18:21:21 +00:00
initial commit
This commit is contained in:
commit
effcbb6628
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?
|
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
build
|
||||||
|
dist
|
23
.prettierrc
Normal file
23
.prettierrc
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "always",
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"embeddedLanguageFormatting": "auto",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"experimentalTernaries": false,
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"insertPragma": false,
|
||||||
|
"jsxBracketSameLine": false,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"printWidth": 80,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"requirePragma": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleAttributePerLine": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"useTabs": false,
|
||||||
|
"vueIndentScriptAndStyle": false
|
||||||
|
}
|
1
.qapp-tunnel.pid
Normal file
1
.qapp-tunnel.pid
Normal file
@ -0,0 +1 @@
|
|||||||
|
1256960
|
8
.vite/deps/_metadata.json
Normal file
8
.vite/deps/_metadata.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"hash": "964445a2",
|
||||||
|
"configHash": "294caa02",
|
||||||
|
"lockfileHash": "75101775",
|
||||||
|
"browserHash": "3e05e511",
|
||||||
|
"optimized": {},
|
||||||
|
"chunks": {}
|
||||||
|
}
|
3
.vite/deps/package.json
Normal file
3
.vite/deps/package.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
54
README.md
Normal file
54
README.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default tseslint.config({
|
||||||
|
extends: [
|
||||||
|
// Remove ...tseslint.configs.recommended and replace with this
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
// Alternatively, use this for stricter rules
|
||||||
|
...tseslint.configs.strictTypeChecked,
|
||||||
|
// Optionally, add this for stylistic rules
|
||||||
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
// other options...
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x'
|
||||||
|
import reactDom from 'eslint-plugin-react-dom'
|
||||||
|
|
||||||
|
export default tseslint.config({
|
||||||
|
plugins: {
|
||||||
|
// Add the react-x and react-dom plugins
|
||||||
|
'react-x': reactX,
|
||||||
|
'react-dom': reactDom,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// other rules...
|
||||||
|
// Enable its recommended typescript rules
|
||||||
|
...reactX.configs['recommended-typescript'].rules,
|
||||||
|
...reactDom.configs.recommended.rules,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
28
eslint.config.js
Normal file
28
eslint.config.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
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/x-icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Qortal Q-App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
26
initialize.js
Normal file
26
initialize.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { writeFile } from "fs/promises";
|
||||||
|
import { randomBytes } from "crypto";
|
||||||
|
import { join } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { dirname } from "path";
|
||||||
|
|
||||||
|
// Resolve __dirname in ES Modules
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// Generate a unique public salt (32 bytes, Base64 encoded)
|
||||||
|
const publicSalt = randomBytes(32).toString("base64");
|
||||||
|
|
||||||
|
// Define the TypeScript file content
|
||||||
|
const tsContent = `export const publicSalt = "${publicSalt}";\n`;
|
||||||
|
|
||||||
|
// Define the file path
|
||||||
|
const filePath = join(__dirname, "src", "qapp-config.ts");
|
||||||
|
|
||||||
|
// Write the TypeScript file
|
||||||
|
try {
|
||||||
|
await writeFile(filePath, tsContent, "utf8");
|
||||||
|
console.log("✅ qapp-config.ts has been created with a unique public salt.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Error writing qapp-config.ts:", error);
|
||||||
|
}
|
4125
package-lock.json
generated
Normal file
4125
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
package.json
Normal file
39
package.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "names",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"initialize": "node initialize.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.0",
|
||||||
|
"@mui/icons-material": "^7.0.1",
|
||||||
|
"@mui/material": "^7.0.1",
|
||||||
|
"jotai": "^2.12.3",
|
||||||
|
"qapp-core": "^1.0.27",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.3.0",
|
||||||
|
"react-virtuoso": "^4.12.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.21.0",
|
||||||
|
"@types/react": "^19.0.10",
|
||||||
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"eslint": "^9.21.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
|
"globals": "^15.15.0",
|
||||||
|
"typescript": "~5.7.2",
|
||||||
|
"typescript-eslint": "^8.24.1",
|
||||||
|
"vite": "^6.2.0",
|
||||||
|
"prettier": "^3.5.3"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
10
src/App.tsx
Normal file
10
src/App.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { MyNames } from "./pages/MyNames"
|
||||||
|
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<MyNames/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
23
src/AppWrapper.tsx
Normal file
23
src/AppWrapper.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Routes } from "./Routes";
|
||||||
|
import { GlobalProvider } from "qapp-core";
|
||||||
|
import { publicSalt } from "./qapp-config.ts";
|
||||||
|
|
||||||
|
export const AppWrapper = () => {
|
||||||
|
return (
|
||||||
|
<GlobalProvider
|
||||||
|
config={{
|
||||||
|
auth: {
|
||||||
|
balanceSetting: {
|
||||||
|
interval: 180000,
|
||||||
|
onlyOnMount: false,
|
||||||
|
},
|
||||||
|
authenticateOnMount: true,
|
||||||
|
},
|
||||||
|
publicSalt: publicSalt,
|
||||||
|
appName: 'names'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Routes />
|
||||||
|
</GlobalProvider>
|
||||||
|
);
|
||||||
|
};
|
40
src/Routes.tsx
Normal file
40
src/Routes.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||||
|
import App from './App';
|
||||||
|
import Layout from './styles/Layout';
|
||||||
|
import { Market } from './pages/Market';
|
||||||
|
import { useHandleNameData } from './hooks/useHandleNameData';
|
||||||
|
|
||||||
|
// Use a custom type if you need it
|
||||||
|
interface CustomWindow extends Window {
|
||||||
|
_qdnBase: string;
|
||||||
|
}
|
||||||
|
const customWindow = window as unknown as CustomWindow;
|
||||||
|
const baseUrl = customWindow?._qdnBase || '';
|
||||||
|
|
||||||
|
export function Routes() {
|
||||||
|
useHandleNameData();
|
||||||
|
|
||||||
|
const router = createBrowserRouter(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
element: <Layout />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: <App />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'market',
|
||||||
|
element: <Market />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
basename: baseUrl,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return <RouterProvider router={router} />;
|
||||||
|
}
|
10
src/common/Spinners/BarSpinner/BarSpinner.tsx
Normal file
10
src/common/Spinners/BarSpinner/BarSpinner.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import './barSpinner.css'
|
||||||
|
export const BarSpinner = ({width = '20px', color}) => {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
width,
|
||||||
|
color: color || 'green'
|
||||||
|
}} className="loader-bar"></div>
|
||||||
|
)
|
||||||
|
}
|
27
src/common/Spinners/BarSpinner/barSpinner.css
Normal file
27
src/common/Spinners/BarSpinner/barSpinner.css
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/* HTML: <div class="loader"></div> */
|
||||||
|
.loader-bar {
|
||||||
|
width: 45px;
|
||||||
|
aspect-ratio: 0.75;
|
||||||
|
--c: no-repeat linear-gradient(currentColor 0 0);
|
||||||
|
background: var(--c) 0% 100%, var(--c) 50% 100%, var(--c) 100% 100%;
|
||||||
|
background-size: 20% 65%;
|
||||||
|
animation: l8 1s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes l8 {
|
||||||
|
16.67% {
|
||||||
|
background-position: 0% 0%, 50% 100%, 100% 100%;
|
||||||
|
}
|
||||||
|
33.33% {
|
||||||
|
background-position: 0% 0%, 50% 0%, 100% 100%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 0% 0%, 50% 0%, 100% 0%;
|
||||||
|
}
|
||||||
|
66.67% {
|
||||||
|
background-position: 0% 100%, 50% 0%, 100% 0%;
|
||||||
|
}
|
||||||
|
83.33% {
|
||||||
|
background-position: 0% 100%, 50% 100%, 100% 0%;
|
||||||
|
}
|
||||||
|
}
|
261
src/components/RegisterName.tsx
Normal file
261
src/components/RegisterName.tsx
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
styled,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from '@mui/material';
|
||||||
|
import {
|
||||||
|
dismissToast,
|
||||||
|
showError,
|
||||||
|
showLoading,
|
||||||
|
showSuccess,
|
||||||
|
Spacer,
|
||||||
|
useGlobal,
|
||||||
|
} from 'qapp-core';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked';
|
||||||
|
import { BarSpinner } from '../common/Spinners/BarSpinner/BarSpinner';
|
||||||
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
|
import ErrorIcon from '@mui/icons-material/Error';
|
||||||
|
export enum Availability {
|
||||||
|
NULL = 'null',
|
||||||
|
LOADING = 'loading',
|
||||||
|
AVAILABLE = 'available',
|
||||||
|
NOT_AVAILABLE = 'not-available',
|
||||||
|
}
|
||||||
|
|
||||||
|
const Label = styled('label')`
|
||||||
|
display: block;
|
||||||
|
font-family: 'IBM Plex Sans', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RegisterName = () => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const balance = useGlobal().auth.balance;
|
||||||
|
const [nameValue, setNameValue] = useState('');
|
||||||
|
const [isNameAvailable, setIsNameAvailable] = useState<Availability>(
|
||||||
|
Availability.NULL
|
||||||
|
);
|
||||||
|
const [isLoadingRegisterName, setIsLoadingRegisterName] = useState(false);
|
||||||
|
const theme = useTheme();
|
||||||
|
const [nameFee, setNameFee] = useState(null);
|
||||||
|
const registerNameFunc = async () => {
|
||||||
|
const loadId = showLoading('Registering name...please wait');
|
||||||
|
try {
|
||||||
|
setIsLoadingRegisterName(true);
|
||||||
|
await qortalRequest({
|
||||||
|
action: 'REGISTER_NAME',
|
||||||
|
name: nameValue,
|
||||||
|
});
|
||||||
|
showSuccess('Successfully registered a name');
|
||||||
|
setNameValue('');
|
||||||
|
setIsOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
showError(error?.message || 'Unable to register name');
|
||||||
|
} finally {
|
||||||
|
setIsLoadingRegisterName(false);
|
||||||
|
dismissToast(loadId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkIfNameExisits = async (name) => {
|
||||||
|
if (!name?.trim()) {
|
||||||
|
setIsNameAvailable(Availability.NULL);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsNameAvailable(Availability.LOADING);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/names/` + name);
|
||||||
|
const data = await res.json();
|
||||||
|
if (data?.message === 'name unknown') {
|
||||||
|
setIsNameAvailable(Availability.AVAILABLE);
|
||||||
|
} else {
|
||||||
|
setIsNameAvailable(Availability.NOT_AVAILABLE);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
checkIfNameExisits(nameValue);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// Cleanup timeout if searchValue changes before the timeout completes
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [nameValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nameRegistrationFee = async () => {
|
||||||
|
try {
|
||||||
|
const data = await fetch(`/transactions/unitfee?txType=REGISTER_NAME`);
|
||||||
|
const fee = await data.text();
|
||||||
|
|
||||||
|
setNameFee((Number(fee) / 1e8).toFixed(8));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
nameRegistrationFee();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
// disabled={!nameValue?.trim()}
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
new name
|
||||||
|
</Button>
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
aria-labelledby="alert-dialog-title"
|
||||||
|
aria-describedby="alert-dialog-description"
|
||||||
|
>
|
||||||
|
<DialogTitle id="alert-dialog-title">{'Register name'}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '400px',
|
||||||
|
maxWidth: '90vw',
|
||||||
|
height: '250px',
|
||||||
|
maxHeight: '90vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '10px',
|
||||||
|
padding: '10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Label>Choose a name</Label>
|
||||||
|
<TextField
|
||||||
|
autoComplete="off"
|
||||||
|
autoFocus
|
||||||
|
onChange={(e) => setNameValue(e.target.value)}
|
||||||
|
value={nameValue}
|
||||||
|
placeholder="Choose a name"
|
||||||
|
/>
|
||||||
|
{(!balance || (nameFee && balance && balance < nameFee)) && (
|
||||||
|
<>
|
||||||
|
<Spacer height="10px" />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '5px',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ErrorIcon
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography>
|
||||||
|
Your balance is {balance ?? 0} QORT. A name registration
|
||||||
|
requires a {nameFee} QORT fee
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Spacer height="10px" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Spacer height="5px" />
|
||||||
|
{isNameAvailable === Availability.AVAILABLE && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '5px',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckIcon
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography>{nameValue} is available</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{isNameAvailable === Availability.NOT_AVAILABLE && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '5px',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ErrorIcon
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography>{nameValue} is unavailable</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{isNameAvailable === Availability.LOADING && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '5px',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BarSpinner width="16px" color={theme.palette.text.primary} />
|
||||||
|
<Typography>Checking if name already existis</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
disabled={isLoadingRegisterName}
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setNameValue('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={
|
||||||
|
!nameValue.trim() ||
|
||||||
|
isLoadingRegisterName ||
|
||||||
|
isNameAvailable !== Availability.AVAILABLE ||
|
||||||
|
!balance ||
|
||||||
|
(balance && nameFee && +balance < +nameFee)
|
||||||
|
}
|
||||||
|
variant="contained"
|
||||||
|
onClick={registerNameFunc}
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
Register Name
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RegisterName;
|
139
src/components/Tables/ForSaleTable.tsx
Normal file
139
src/components/Tables/ForSaleTable.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Paper,
|
||||||
|
Button,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
|
import { forwardRef, useMemo } from 'react';
|
||||||
|
import { TableVirtuoso, TableComponents } from 'react-virtuoso';
|
||||||
|
import { forSaleAtom, namesAtom } from '../../state/global/names';
|
||||||
|
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
||||||
|
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
||||||
|
|
||||||
|
interface NameData {
|
||||||
|
name: string;
|
||||||
|
isSelling?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VirtuosoTableComponents: TableComponents<NameData> = {
|
||||||
|
Scroller: forwardRef<HTMLDivElement>((props, ref) => (
|
||||||
|
<TableContainer component={Paper} {...props} ref={ref} />
|
||||||
|
)),
|
||||||
|
Table: (props) => (
|
||||||
|
<Table
|
||||||
|
{...props}
|
||||||
|
sx={{ borderCollapse: 'separate', tableLayout: 'fixed' }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
TableHead: forwardRef<HTMLTableSectionElement>((props, ref) => (
|
||||||
|
<TableHead {...props} ref={ref} />
|
||||||
|
)),
|
||||||
|
TableRow,
|
||||||
|
TableBody: forwardRef<HTMLTableSectionElement>((props, ref) => (
|
||||||
|
<TableBody {...props} ref={ref} />
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
|
||||||
|
function fixedHeaderContent(
|
||||||
|
sortBy: string,
|
||||||
|
sortDirection: string,
|
||||||
|
setSort: (field: 'name' | 'salePrice') => void
|
||||||
|
) {
|
||||||
|
const renderSortIcon = (field: string) => {
|
||||||
|
if (sortBy !== field) return null;
|
||||||
|
|
||||||
|
return sortDirection === 'asc' ? (
|
||||||
|
<ArrowUpwardIcon fontSize="small" sx={{ ml: 0.5 }} />
|
||||||
|
) : (
|
||||||
|
<ArrowDownwardIcon fontSize="small" sx={{ ml: 0.5 }} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortableCellSx = {
|
||||||
|
cursor: 'pointer',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'action.selected',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow sx={{ backgroundColor: 'background.paper' }}>
|
||||||
|
<TableCell onClick={() => setSort('name')} sx={sortableCellSx}>
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
Name {renderSortIcon('name')}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell onClick={() => setSort('salePrice')} sx={sortableCellSx}>
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
Sale Price {renderSortIcon('salePrice')}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>Actions</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowContent(_index: number, row: NameData) {
|
||||||
|
const handleUpdate = () => {
|
||||||
|
console.log('Update:', row.name);
|
||||||
|
// Your logic here
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBuy = async (name: string) => {
|
||||||
|
try {
|
||||||
|
console.log('hello');
|
||||||
|
await qortalRequest({
|
||||||
|
action: 'BUY_NAME',
|
||||||
|
nameForSale: name,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TableCell>{row.name}</TableCell>
|
||||||
|
<TableCell>{row.salePrice}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleBuy(row.name)}
|
||||||
|
>
|
||||||
|
Buy
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ForSaleTable = ({
|
||||||
|
namesForSale,
|
||||||
|
sortDirection,
|
||||||
|
sortBy,
|
||||||
|
handleSort,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
height: 'calc(100vh - 64px - 60px)', // Header + footer height
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableVirtuoso
|
||||||
|
data={namesForSale}
|
||||||
|
components={VirtuosoTableComponents}
|
||||||
|
fixedHeaderContent={() =>
|
||||||
|
fixedHeaderContent(sortBy, sortDirection, handleSort)
|
||||||
|
}
|
||||||
|
itemContent={rowContent}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
718
src/components/Tables/NameTable.tsx
Normal file
718
src/components/Tables/NameTable.tsx
Normal file
@ -0,0 +1,718 @@
|
|||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Paper,
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
Tooltip,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogActions,
|
||||||
|
TextField,
|
||||||
|
useTheme,
|
||||||
|
Typography,
|
||||||
|
CircularProgress,
|
||||||
|
Avatar,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
|
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { TableVirtuoso, TableComponents } from 'react-virtuoso';
|
||||||
|
import { forSaleAtom, namesAtom } from '../../state/global/names';
|
||||||
|
import PersonIcon from '@mui/icons-material/Person';
|
||||||
|
import { useModal } from '../../hooks/useModal';
|
||||||
|
import {
|
||||||
|
dismissToast,
|
||||||
|
ImagePicker,
|
||||||
|
RequestQueueWithPromise,
|
||||||
|
showError,
|
||||||
|
showLoading,
|
||||||
|
showSuccess,
|
||||||
|
Spacer,
|
||||||
|
useGlobal,
|
||||||
|
} from 'qapp-core';
|
||||||
|
import { Availability } from '../RegisterName';
|
||||||
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
|
import ErrorIcon from '@mui/icons-material/Error';
|
||||||
|
import { BarSpinner } from '../../common/Spinners/BarSpinner/BarSpinner';
|
||||||
|
interface NameData {
|
||||||
|
name: string;
|
||||||
|
isSelling?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNameQueue = new RequestQueueWithPromise(2);
|
||||||
|
|
||||||
|
const VirtuosoTableComponents: TableComponents<NameData> = {
|
||||||
|
Scroller: forwardRef<HTMLDivElement>((props, ref) => (
|
||||||
|
<TableContainer component={Paper} {...props} ref={ref} />
|
||||||
|
)),
|
||||||
|
Table: (props) => (
|
||||||
|
<Table
|
||||||
|
{...props}
|
||||||
|
sx={{ borderCollapse: 'separate', tableLayout: 'fixed' }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
TableHead: forwardRef<HTMLTableSectionElement>((props, ref) => (
|
||||||
|
<TableHead {...props} ref={ref} />
|
||||||
|
)),
|
||||||
|
TableRow,
|
||||||
|
TableBody: forwardRef<HTMLTableSectionElement>((props, ref) => (
|
||||||
|
<TableBody {...props} ref={ref} />
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
|
||||||
|
function fixedHeaderContent() {
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
sx={{
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableCell>Name</TableCell>
|
||||||
|
<TableCell>Actions</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ManageAvatar = ({ name, modalFunctionsAvatar }) => {
|
||||||
|
const [hasAvatar, setHasAvatar] = useState<boolean | null>(null);
|
||||||
|
|
||||||
|
const checkIfAvatarExists = useCallback(async (name) => {
|
||||||
|
try {
|
||||||
|
const identifier = `qortal_avatar`;
|
||||||
|
const url = `/arbitrary/resources/searchsimple?mode=ALL&service=THUMBNAIL&identifier=${identifier}&limit=1&name=${name}&includemetadata=false&prefix=true`;
|
||||||
|
const response = await getNameQueue.enqueue(() =>
|
||||||
|
fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const responseData = await response.json();
|
||||||
|
if (responseData?.length > 0) {
|
||||||
|
setHasAvatar(true);
|
||||||
|
} else {
|
||||||
|
setHasAvatar(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!name) return;
|
||||||
|
checkIfAvatarExists(name);
|
||||||
|
}, [name, checkIfAvatarExists]);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
disabled={hasAvatar === null}
|
||||||
|
onClick={() => modalFunctionsAvatar.show({ name, hasAvatar })}
|
||||||
|
>
|
||||||
|
{hasAvatar === null ? (
|
||||||
|
<CircularProgress size={10} />
|
||||||
|
) : hasAvatar ? (
|
||||||
|
'Change avatar'
|
||||||
|
) : (
|
||||||
|
'Set avatar'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function rowContent(
|
||||||
|
_index: number,
|
||||||
|
row: NameData,
|
||||||
|
primaryName?: string,
|
||||||
|
modalFunctions?: any,
|
||||||
|
modalFunctionsUpdateName?: any,
|
||||||
|
modalFunctionsAvatar?: any,
|
||||||
|
modalFunctionsSellName?: any
|
||||||
|
) {
|
||||||
|
const handleUpdate = async (name: string) => {
|
||||||
|
try {
|
||||||
|
const response = await modalFunctionsUpdateName.show();
|
||||||
|
console.log('Update:', row.name);
|
||||||
|
console.log('hello', response);
|
||||||
|
await qortalRequest({
|
||||||
|
action: 'UPDATE_NAME',
|
||||||
|
newName: response,
|
||||||
|
oldName: name,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Your logic here
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSell = async (name: string) => {
|
||||||
|
try {
|
||||||
|
if (name === primaryName) {
|
||||||
|
await modalFunctions.show({ name });
|
||||||
|
}
|
||||||
|
const price = await modalFunctionsSellName.show(name);
|
||||||
|
console.log('hello');
|
||||||
|
await qortalRequest({
|
||||||
|
action: 'SELL_NAME',
|
||||||
|
nameForSale: name,
|
||||||
|
salePrice: price,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = async (name: string) => {
|
||||||
|
try {
|
||||||
|
console.log('hello', name);
|
||||||
|
await qortalRequest({
|
||||||
|
action: 'CANCEL_SELL_NAME',
|
||||||
|
nameForSale: name,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TableCell>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '5px',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{primaryName === row.name && (
|
||||||
|
<Tooltip
|
||||||
|
title="This is your primary name ( identity )"
|
||||||
|
placement="left"
|
||||||
|
arrow
|
||||||
|
sx={{ fontSize: '24' }}
|
||||||
|
>
|
||||||
|
<PersonIcon color="success" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{row.name}
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '5px',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
color={primaryName === row.name ? 'warning' : 'primary'}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleUpdate(row.name)}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
{!row.isSelling ? (
|
||||||
|
<Button
|
||||||
|
color={primaryName === row.name ? 'warning' : 'primary'}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => handleSell(row.name)}
|
||||||
|
>
|
||||||
|
Sell
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
color="error"
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleCancel(row.name)}
|
||||||
|
>
|
||||||
|
Cancel Sell
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<ManageAvatar
|
||||||
|
name={row.name}
|
||||||
|
modalFunctionsAvatar={modalFunctionsAvatar}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NameTable = ({ names, primaryName }) => {
|
||||||
|
// const [names, setNames] = useAtom(namesAtom);
|
||||||
|
const [namesForSale] = useAtom(forSaleAtom);
|
||||||
|
const modalFunctions = useModal();
|
||||||
|
const modalFunctionsUpdateName = useModal();
|
||||||
|
const modalFunctionsAvatar = useModal();
|
||||||
|
const modalFunctionsSellName = useModal();
|
||||||
|
|
||||||
|
console.log('names', names);
|
||||||
|
const namesToDisplay = useMemo(() => {
|
||||||
|
const namesForSaleString = namesForSale.map((item) => item.name);
|
||||||
|
return names.map((name) => {
|
||||||
|
return {
|
||||||
|
name: name.name,
|
||||||
|
isSelling: namesForSaleString.includes(name.name),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [names, namesForSale]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
height: 'calc(100vh - 64px - 60px)', // Header + footer height
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableVirtuoso
|
||||||
|
data={namesToDisplay}
|
||||||
|
components={VirtuosoTableComponents}
|
||||||
|
fixedHeaderContent={fixedHeaderContent}
|
||||||
|
itemContent={(index, row) =>
|
||||||
|
rowContent(
|
||||||
|
index,
|
||||||
|
row,
|
||||||
|
primaryName,
|
||||||
|
modalFunctions,
|
||||||
|
modalFunctionsUpdateName,
|
||||||
|
modalFunctionsAvatar,
|
||||||
|
modalFunctionsSellName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{modalFunctions?.isShow && (
|
||||||
|
<Dialog
|
||||||
|
open={modalFunctions?.isShow}
|
||||||
|
aria-labelledby="alert-dialog-title"
|
||||||
|
aria-describedby="alert-dialog-description"
|
||||||
|
>
|
||||||
|
<DialogTitle id="alert-dialog-title">Warning</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText id="alert-dialog-description">
|
||||||
|
Caution when selling your primary name
|
||||||
|
</DialogContentText>
|
||||||
|
<Spacer height="20px" />
|
||||||
|
<DialogContentText id="alert-dialog-description2">
|
||||||
|
{modalFunctions?.data?.name} is your primary name. If you are an
|
||||||
|
admin of a private group, selling this name will remove your group
|
||||||
|
keys for the group. Make sure another admin re-encrypts the latest
|
||||||
|
keys before selling. Proceed with caution!
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
color="warning"
|
||||||
|
variant="contained"
|
||||||
|
onClick={modalFunctions.onOk}
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
continue
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={modalFunctions.onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
{modalFunctionsUpdateName?.isShow && (
|
||||||
|
<UpdateNameModal modalFunctionsUpdateName={modalFunctionsUpdateName} />
|
||||||
|
)}
|
||||||
|
{modalFunctionsAvatar?.isShow && (
|
||||||
|
<AvatarModal modalFunctionsAvatar={modalFunctionsAvatar} />
|
||||||
|
)}
|
||||||
|
{modalFunctionsSellName?.isShow && (
|
||||||
|
<SellNameModal modalFunctionsSellName={modalFunctionsSellName} />
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AvatarModal = ({ modalFunctionsAvatar }) => {
|
||||||
|
const [arbitraryFee, setArbitraryFee] = useState('');
|
||||||
|
const [pickedAvatar, setPickedAvatar] = useState<any>(null);
|
||||||
|
const [isLoadingPublish, setIsLoadingPublish] = useState(false);
|
||||||
|
const theme = useTheme();
|
||||||
|
console.log('pickedAvatar', pickedAvatar);
|
||||||
|
useEffect(() => {
|
||||||
|
const getArbitraryName = async () => {
|
||||||
|
try {
|
||||||
|
const data = await fetch(`/transactions/unitfee?txType=ARBITRARY`);
|
||||||
|
const fee = await data.text();
|
||||||
|
|
||||||
|
setArbitraryFee((Number(fee) / 1e8).toFixed(8));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getArbitraryName();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const publishAvatar = async () => {
|
||||||
|
const loadId = showLoading('Publishing avatar...please wait');
|
||||||
|
try {
|
||||||
|
setIsLoadingPublish(true);
|
||||||
|
await qortalRequest({
|
||||||
|
action: 'PUBLISH_QDN_RESOURCE',
|
||||||
|
base64: pickedAvatar?.base64,
|
||||||
|
service: 'THUMBNAIL',
|
||||||
|
identifier: 'qortal_avatar',
|
||||||
|
name: modalFunctionsAvatar.data.name,
|
||||||
|
});
|
||||||
|
showSuccess('Successfully published avatar');
|
||||||
|
modalFunctionsAvatar.onOk();
|
||||||
|
} catch (error) {
|
||||||
|
showError(error?.message || 'Unable to publish avatar');
|
||||||
|
} finally {
|
||||||
|
dismissToast(loadId);
|
||||||
|
setIsLoadingPublish(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={modalFunctionsAvatar?.isShow}
|
||||||
|
aria-labelledby="alert-dialog-title"
|
||||||
|
aria-describedby="alert-dialog-description"
|
||||||
|
>
|
||||||
|
<DialogTitle id="alert-dialog-title">Publish Avatar</DialogTitle>
|
||||||
|
<DialogContent
|
||||||
|
sx={{
|
||||||
|
width: '300px',
|
||||||
|
maxWidth: '95vw',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spacer height="20px" />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{modalFunctionsAvatar.data.hasAvatar && !pickedAvatar?.base64 && (
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
height: '138px',
|
||||||
|
width: '138px',
|
||||||
|
}}
|
||||||
|
src={`/arbitrary/THUMBNAIL/${modalFunctionsAvatar.data.name}/qortal_avatar?async=true`}
|
||||||
|
alt={modalFunctionsAvatar.data.name}
|
||||||
|
>
|
||||||
|
<CircularProgress />
|
||||||
|
</Avatar>
|
||||||
|
)}
|
||||||
|
{pickedAvatar?.base64 && (
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
height: '138px',
|
||||||
|
width: '138px',
|
||||||
|
}}
|
||||||
|
src={`data:image/webp;base64,${pickedAvatar?.base64}`}
|
||||||
|
alt={modalFunctionsAvatar.data.name}
|
||||||
|
>
|
||||||
|
<CircularProgress />
|
||||||
|
</Avatar>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pickedAvatar?.name && (
|
||||||
|
<>
|
||||||
|
<Spacer height="10px" />
|
||||||
|
<Typography variant="body2">{pickedAvatar?.name}</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Spacer height="20px" />
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
(500 KB max. for GIFS){' '}
|
||||||
|
</Typography>
|
||||||
|
<ImagePicker onPick={(file) => setPickedAvatar(file)} mode="single">
|
||||||
|
<Button variant="contained">Choose Image</Button>
|
||||||
|
</ImagePicker>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
disabled={!pickedAvatar?.base64 || isLoadingPublish}
|
||||||
|
variant="contained"
|
||||||
|
onClick={publishAvatar}
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
publish
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={modalFunctionsAvatar.onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UpdateNameModal = ({ modalFunctionsUpdateName }) => {
|
||||||
|
const [step, setStep] = useState(1);
|
||||||
|
const [newName, setNewName] = useState('');
|
||||||
|
const [isNameAvailable, setIsNameAvailable] = useState<Availability>(
|
||||||
|
Availability.NULL
|
||||||
|
);
|
||||||
|
const [nameFee, setNameFee] = useState(null);
|
||||||
|
const balance = useGlobal().auth.balance;
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const checkIfNameExisits = async (name) => {
|
||||||
|
if (!name?.trim()) {
|
||||||
|
setIsNameAvailable(Availability.NULL);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsNameAvailable(Availability.LOADING);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/names/` + name);
|
||||||
|
const data = await res.json();
|
||||||
|
if (data?.message === 'name unknown') {
|
||||||
|
setIsNameAvailable(Availability.AVAILABLE);
|
||||||
|
} else {
|
||||||
|
setIsNameAvailable(Availability.NOT_AVAILABLE);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
checkIfNameExisits(newName);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// Cleanup timeout if searchValue changes before the timeout completes
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [newName]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nameRegistrationFee = async () => {
|
||||||
|
try {
|
||||||
|
const data = await fetch(`/transactions/unitfee?txType=REGISTER_NAME`);
|
||||||
|
const fee = await data.text();
|
||||||
|
|
||||||
|
setNameFee((Number(fee) / 1e8).toFixed(8));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
nameRegistrationFee();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={modalFunctionsUpdateName?.isShow}
|
||||||
|
aria-labelledby="alert-dialog-title"
|
||||||
|
aria-describedby="alert-dialog-description"
|
||||||
|
>
|
||||||
|
{step === 1 && (
|
||||||
|
<>
|
||||||
|
<DialogTitle id="alert-dialog-title">Warning</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText id="alert-dialog-description">
|
||||||
|
Caution when updating your name
|
||||||
|
</DialogContentText>
|
||||||
|
<Spacer height="20px" />
|
||||||
|
<DialogContentText id="alert-dialog-description2">
|
||||||
|
If you update your Name, you will forfeit the resources associated
|
||||||
|
with the original Name. In other words, you will lose ownership of
|
||||||
|
the content under the original Name on QDN. Proceed with caution!
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
color="warning"
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => setStep(2)}
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
continue
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={modalFunctionsUpdateName.onCancel}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{step === 2 && (
|
||||||
|
<>
|
||||||
|
<DialogTitle id="alert-dialog-title">Warning</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText id="alert-dialog-description">
|
||||||
|
Choose new name
|
||||||
|
</DialogContentText>
|
||||||
|
<Spacer height="20px" />
|
||||||
|
<TextField
|
||||||
|
autoComplete="off"
|
||||||
|
autoFocus
|
||||||
|
onChange={(e) => setNewName(e.target.value)}
|
||||||
|
value={newName}
|
||||||
|
placeholder="Choose a name"
|
||||||
|
/>
|
||||||
|
{(!balance || (nameFee && balance && balance < nameFee)) && (
|
||||||
|
<>
|
||||||
|
<Spacer height="10px" />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '5px',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ErrorIcon
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography>
|
||||||
|
Your balance is {balance ?? 0} QORT. A name registration
|
||||||
|
requires a {nameFee} QORT fee
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Spacer height="10px" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Spacer height="5px" />
|
||||||
|
{isNameAvailable === Availability.AVAILABLE && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '5px',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckIcon
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography>{newName} is available</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{isNameAvailable === Availability.NOT_AVAILABLE && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '5px',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ErrorIcon
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography>{newName} is unavailable</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{isNameAvailable === Availability.LOADING && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '5px',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BarSpinner width="16px" color={theme.palette.text.primary} />
|
||||||
|
<Typography>Checking if name already existis</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
variant="contained"
|
||||||
|
disabled={
|
||||||
|
!newName?.trim() ||
|
||||||
|
isNameAvailable !== Availability.AVAILABLE ||
|
||||||
|
!balance ||
|
||||||
|
(balance && nameFee && +balance < +nameFee)
|
||||||
|
}
|
||||||
|
onClick={() => modalFunctionsUpdateName.onOk(newName.trim())}
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
continue
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
variant="contained"
|
||||||
|
onClick={modalFunctionsUpdateName.onCancel}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SellNameModal = ({ modalFunctionsSellName }) => {
|
||||||
|
const [price, setPrice] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={modalFunctionsSellName?.isShow}
|
||||||
|
aria-labelledby="alert-dialog-title"
|
||||||
|
aria-describedby="alert-dialog-description"
|
||||||
|
>
|
||||||
|
<DialogTitle id="alert-dialog-title">Selling name</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText id="alert-dialog-description">
|
||||||
|
Choose selling price
|
||||||
|
</DialogContentText>
|
||||||
|
<Spacer height="20px" />
|
||||||
|
<TextField
|
||||||
|
autoComplete="off"
|
||||||
|
autoFocus
|
||||||
|
onChange={(e) => setPrice(+e.target.value)}
|
||||||
|
value={price}
|
||||||
|
type="number"
|
||||||
|
placeholder="Choose a name"
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
variant="contained"
|
||||||
|
disabled={!price}
|
||||||
|
onClick={() => modalFunctionsSellName.onOk(price)}
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
continue
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
variant="contained"
|
||||||
|
onClick={modalFunctionsSellName.onCancel}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
51
src/hooks/useHandleNameData.tsx
Normal file
51
src/hooks/useHandleNameData.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { useSetAtom } from 'jotai';
|
||||||
|
import { forSaleAtom, namesAtom } from '../state/global/names';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { useGlobal } from 'qapp-core';
|
||||||
|
|
||||||
|
export const useHandleNameData = () => {
|
||||||
|
const setNamesForSale = useSetAtom(forSaleAtom);
|
||||||
|
const setNames = useSetAtom(namesAtom);
|
||||||
|
const address = useGlobal().auth.address;
|
||||||
|
|
||||||
|
const getNamesForSale = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/names/forsale?limit=0&reverse=true');
|
||||||
|
const data = await res.json();
|
||||||
|
setNamesForSale(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}, [setNamesForSale]);
|
||||||
|
|
||||||
|
const getMyNames = useCallback(async () => {
|
||||||
|
if (!address) return;
|
||||||
|
try {
|
||||||
|
const res = await qortalRequest({
|
||||||
|
action: 'GET_ACCOUNT_NAMES',
|
||||||
|
address,
|
||||||
|
limit: 0,
|
||||||
|
offset: 0,
|
||||||
|
reverse: false,
|
||||||
|
});
|
||||||
|
setNames(res);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}, [address, setNames]);
|
||||||
|
|
||||||
|
// Initial fetch + interval
|
||||||
|
useEffect(() => {
|
||||||
|
getNamesForSale();
|
||||||
|
const interval = setInterval(getNamesForSale, 120_000); // every 2 minutes
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [getNamesForSale]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getMyNames();
|
||||||
|
const interval = setInterval(getMyNames, 120_000); // every 2 minutes
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [getMyNames]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
50
src/hooks/useIframeListener.tsx
Normal file
50
src/hooks/useIframeListener.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { To, useNavigate } from "react-router-dom";
|
||||||
|
import { EnumTheme, themeAtom } from "../state/global/system";
|
||||||
|
import { useSetAtom } from "jotai";
|
||||||
|
|
||||||
|
interface CustomWindow extends Window {
|
||||||
|
_qdnTheme: string;
|
||||||
|
}
|
||||||
|
const customWindow = window as unknown as CustomWindow;
|
||||||
|
|
||||||
|
export const useIframe = () => {
|
||||||
|
const setTheme = useSetAtom(themeAtom);
|
||||||
|
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('customWindow', customWindow?._qdnTheme)
|
||||||
|
const themeColorDefault = customWindow?._qdnTheme
|
||||||
|
if(themeColorDefault === 'dark'){
|
||||||
|
setTheme(EnumTheme.DARK)
|
||||||
|
} else if(themeColorDefault === 'light'){
|
||||||
|
setTheme(EnumTheme.LIGHT)
|
||||||
|
}
|
||||||
|
function handleNavigation(event: { data: { action: string; path: To; theme: 'dark' | 'light' }; }) {
|
||||||
|
if (event.data?.action === "NAVIGATE_TO_PATH" && event.data.path) {
|
||||||
|
navigate(event.data.path); // Navigate directly to the specified path
|
||||||
|
|
||||||
|
// Send a response back to the parent window after navigation is handled
|
||||||
|
window.parent.postMessage(
|
||||||
|
{ action: "NAVIGATION_SUCCESS", path: event.data.path },
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
} else if (event.data?.action === "THEME_CHANGED" && event.data.theme) {
|
||||||
|
const themeColor = event.data.theme
|
||||||
|
if(themeColor === 'dark'){
|
||||||
|
setTheme(EnumTheme.DARK)
|
||||||
|
} else if(themeColor === 'light'){
|
||||||
|
setTheme(EnumTheme.LIGHT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("message", handleNavigation);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("message", handleNavigation);
|
||||||
|
};
|
||||||
|
}, [navigate, setTheme]);
|
||||||
|
return { navigate };
|
||||||
|
};
|
50
src/hooks/useModal.tsx
Normal file
50
src/hooks/useModal.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { useRef, useState, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
isShow: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useModal = () => {
|
||||||
|
const [state, setState] = useState<State>({ isShow: false });
|
||||||
|
const [data, setData] = useState(null);
|
||||||
|
const promiseConfig = useRef<any>(null);
|
||||||
|
|
||||||
|
const show = useCallback((data) => {
|
||||||
|
setData(data);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
promiseConfig.current = { resolve, reject };
|
||||||
|
setState({ isShow: true });
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hide = useCallback(() => {
|
||||||
|
setState({ isShow: false });
|
||||||
|
setData(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onOk = useCallback(
|
||||||
|
(payload: any) => {
|
||||||
|
const { resolve } = promiseConfig.current || {};
|
||||||
|
hide();
|
||||||
|
resolve?.(payload);
|
||||||
|
},
|
||||||
|
[hide]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onCancel = useCallback(() => {
|
||||||
|
const { reject } = promiseConfig.current || {};
|
||||||
|
hide();
|
||||||
|
reject?.();
|
||||||
|
}, [hide]);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
show,
|
||||||
|
onOk,
|
||||||
|
onCancel,
|
||||||
|
isShow: state.isShow,
|
||||||
|
data,
|
||||||
|
}),
|
||||||
|
[show, onOk, onCancel, state.isShow, data]
|
||||||
|
);
|
||||||
|
};
|
29
src/index.css
Normal file
29
src/index.css
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('./styles/fonts/Inter-SemiBold.ttf') format('truetype');
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('./styles/fonts/Inter-ExtraBold.ttf') format('truetype');
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('./styles/fonts/Inter-Bold.ttf') format('truetype');
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('./styles/fonts/Inter-Regular.ttf') format('truetype');
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
line-height: 1.2;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: 'Inter';
|
||||||
|
}
|
13
src/main.tsx
Normal file
13
src/main.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import "./index.css";
|
||||||
|
import ThemeProviderWrapper from "./styles/theme/theme-provider.tsx";
|
||||||
|
import { AppWrapper } from "./AppWrapper.tsx";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<ThemeProviderWrapper>
|
||||||
|
<AppWrapper />
|
||||||
|
</ThemeProviderWrapper>
|
||||||
|
</StrictMode>
|
||||||
|
);
|
98
src/pages/Market.tsx
Normal file
98
src/pages/Market.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { Box, TextField } from '@mui/material';
|
||||||
|
import { ForSaleTable } from '../components/Tables/ForSaleTable';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { forSaleAtom } from '../state/global/names';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
|
|
||||||
|
export const Market = () => {
|
||||||
|
const [namesForSale] = useAtom(forSaleAtom);
|
||||||
|
const [sortBy, setSortBy] = useState<'name' | 'salePrice'>('name');
|
||||||
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||||
|
const [filterValue, setFilterValue] = useState('');
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
const namesForSaleFiltered = useMemo(() => {
|
||||||
|
const lowerFilter = filterValue.trim().toLowerCase();
|
||||||
|
|
||||||
|
const filtered = !lowerFilter
|
||||||
|
? namesForSale
|
||||||
|
: namesForSale.filter((item) =>
|
||||||
|
item.name.toLowerCase().includes(lowerFilter)
|
||||||
|
);
|
||||||
|
|
||||||
|
return [...filtered].sort((a, b) => {
|
||||||
|
let aVal: string | number = a[sortBy];
|
||||||
|
let bVal: string | number = b[sortBy];
|
||||||
|
|
||||||
|
// Convert salePrice strings to numbers for comparison
|
||||||
|
if (sortBy === 'salePrice') {
|
||||||
|
aVal = parseFloat(aVal as string);
|
||||||
|
bVal = parseFloat(bVal as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aVal == null) return 1;
|
||||||
|
if (bVal == null) return -1;
|
||||||
|
|
||||||
|
if (sortDirection === 'asc') {
|
||||||
|
return aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
|
||||||
|
} else {
|
||||||
|
return aVal < bVal ? 1 : aVal > bVal ? -1 : 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [namesForSale, sortBy, sortDirection, filterValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setFilterValue(value);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// Cleanup timeout if searchValue changes before the timeout completes
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleSort = useCallback(
|
||||||
|
(field: 'name' | 'salePrice') => {
|
||||||
|
if (sortBy === field) {
|
||||||
|
// Toggle direction
|
||||||
|
setSortDirection((prev) => (prev === 'asc' ? 'desc' : 'asc'));
|
||||||
|
} else {
|
||||||
|
// Change field and reset direction
|
||||||
|
setSortBy(field);
|
||||||
|
setSortDirection('asc');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[sortBy]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('sortDirection', sortDirection);
|
||||||
|
console.log('sortBy', sortBy);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: '60px',
|
||||||
|
padding: '10px',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
placeholder="Filter names"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<ForSaleTable
|
||||||
|
namesForSale={namesForSaleFiltered}
|
||||||
|
sortBy={sortBy}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
handleSort={handleSort}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
62
src/pages/MyNames.tsx
Normal file
62
src/pages/MyNames.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { useAtom, useSetAtom } from 'jotai';
|
||||||
|
import { useGlobal } from 'qapp-core';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { namesAtom } from '../state/global/names';
|
||||||
|
import { NameTable } from '../components/Tables/NameTable';
|
||||||
|
import { Box, Button, TextField } from '@mui/material';
|
||||||
|
import RegisterName from '../components/RegisterName';
|
||||||
|
|
||||||
|
export const MyNames = () => {
|
||||||
|
const [names] = useAtom(namesAtom);
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
const [filterValue, setFilterValue] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setFilterValue(value);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// Cleanup timeout if searchValue changes before the timeout completes
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const filteredNames = useMemo(() => {
|
||||||
|
const lowerFilter = filterValue.trim().toLowerCase();
|
||||||
|
console.log('lowerFilter', lowerFilter, names);
|
||||||
|
const filtered = !lowerFilter
|
||||||
|
? names
|
||||||
|
: names.filter((item) => item.name.toLowerCase().includes(lowerFilter));
|
||||||
|
return filtered;
|
||||||
|
}, [names, filterValue]);
|
||||||
|
|
||||||
|
const primaryName = useMemo(() => {
|
||||||
|
return names[0]?.name || '';
|
||||||
|
}, [names]);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: '60px',
|
||||||
|
padding: '10px',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
<TextField
|
||||||
|
placeholder="Filter names"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<RegisterName />
|
||||||
|
</Box>
|
||||||
|
<NameTable names={filteredNames} primaryName={primaryName} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
1
src/qapp-config.ts
Normal file
1
src/qapp-config.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const publicSalt = "DgRg1PJmFOJI8MibmWzgPX4k4xZo8Hl2mKGhqJ9Zemw=";
|
4
src/state/global/names.ts
Normal file
4
src/state/global/names.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
|
export const namesAtom = atom([]);
|
||||||
|
export const forSaleAtom = atom([]);
|
9
src/state/global/system.ts
Normal file
9
src/state/global/system.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
|
export enum EnumTheme {
|
||||||
|
LIGHT = 1,
|
||||||
|
DARK = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atom to hold the current theme
|
||||||
|
export const themeAtom = atom<EnumTheme>(EnumTheme.DARK);
|
64
src/styles/Layout.tsx
Normal file
64
src/styles/Layout.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import { useIframe } from '../hooks/useIframeListener';
|
||||||
|
import { AppBar, Toolbar, Button, Box, useTheme } from '@mui/material';
|
||||||
|
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
|
||||||
|
import StorefrontIcon from '@mui/icons-material/Storefront';
|
||||||
|
|
||||||
|
const Layout = () => {
|
||||||
|
useIframe();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ label: 'My names', path: '/', Icon: FormatListBulletedIcon },
|
||||||
|
{ label: 'Names for sale', path: '/market', Icon: StorefrontIcon },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: '100vh',
|
||||||
|
width: '100%',
|
||||||
|
// overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AppBar
|
||||||
|
position="sticky"
|
||||||
|
color="default"
|
||||||
|
elevation={1}
|
||||||
|
sx={{ height: 64 }}
|
||||||
|
>
|
||||||
|
<Toolbar
|
||||||
|
sx={{
|
||||||
|
gap: '25px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{navItems.map(({ label, path, Icon }) => (
|
||||||
|
<Button
|
||||||
|
key={path}
|
||||||
|
startIcon={<Icon />}
|
||||||
|
onClick={() => navigate(path)}
|
||||||
|
sx={{
|
||||||
|
backgroundColor:
|
||||||
|
location.pathname === path
|
||||||
|
? theme.palette.action.selected
|
||||||
|
: 'unset',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
|
||||||
|
<Box component="main">
|
||||||
|
<main>
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
BIN
src/styles/fonts/Inter-Black.ttf
Normal file
BIN
src/styles/fonts/Inter-Black.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/Inter-Bold.ttf
Normal file
BIN
src/styles/fonts/Inter-Bold.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/Inter-ExtraBold.ttf
Normal file
BIN
src/styles/fonts/Inter-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/Inter-ExtraLight.ttf
Normal file
BIN
src/styles/fonts/Inter-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/Inter-Light.ttf
Normal file
BIN
src/styles/fonts/Inter-Light.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/Inter-Medium.ttf
Normal file
BIN
src/styles/fonts/Inter-Medium.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/Inter-Regular.ttf
Normal file
BIN
src/styles/fonts/Inter-Regular.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/Inter-SemiBold.ttf
Normal file
BIN
src/styles/fonts/Inter-SemiBold.ttf
Normal file
Binary file not shown.
BIN
src/styles/fonts/Inter-Thin.ttf
Normal file
BIN
src/styles/fonts/Inter-Thin.ttf
Normal file
Binary file not shown.
26
src/styles/theme/theme-provider.tsx
Normal file
26
src/styles/theme/theme-provider.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { ThemeProvider } from "@emotion/react";
|
||||||
|
|
||||||
|
import {lightTheme, darkTheme} from "./theme"
|
||||||
|
import { CssBaseline } from "@mui/material";
|
||||||
|
import { EnumTheme, themeAtom } from "../../state/global/system";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
|
interface ThemeProviderWrapperProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeProviderWrapper: FC<ThemeProviderWrapperProps> = ({ children }) => {
|
||||||
|
const [theme] = useAtom(themeAtom);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={theme === EnumTheme.LIGHT ? lightTheme : darkTheme}>
|
||||||
|
<CssBaseline />
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeProviderWrapper;
|
119
src/styles/theme/theme.ts
Normal file
119
src/styles/theme/theme.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { createTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
|
const commonThemeOptions = {
|
||||||
|
typography: {
|
||||||
|
fontFamily: ['Inter'].join(','),
|
||||||
|
h1: {
|
||||||
|
fontSize: '2rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontSize: '1.75rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
h4: {
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
h5: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
h6: {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
body1: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
letterSpacing: '0.5px',
|
||||||
|
},
|
||||||
|
|
||||||
|
body2: {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
letterSpacing: '0.2px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
spacing: 8,
|
||||||
|
shape: {
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
breakpoints: {
|
||||||
|
values: {
|
||||||
|
xs: 0,
|
||||||
|
sm: 600,
|
||||||
|
md: 900,
|
||||||
|
lg: 1200,
|
||||||
|
xl: 1536,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiDialog: {
|
||||||
|
styleOverrides: {
|
||||||
|
paper: {
|
||||||
|
backgroundImage: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiPopover: {
|
||||||
|
styleOverrides: {
|
||||||
|
paper: {
|
||||||
|
backgroundImage: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const lightTheme = createTheme({
|
||||||
|
...commonThemeOptions,
|
||||||
|
palette: {
|
||||||
|
mode: 'light',
|
||||||
|
primary: {
|
||||||
|
main: 'rgb(63, 81, 181)',
|
||||||
|
dark: 'rgb(113, 198, 212)',
|
||||||
|
light: 'rgb(180, 200, 235)',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: 'rgba(194, 222, 236, 1)',
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: 'rgba(250, 250, 250, 1)',
|
||||||
|
paper: 'rgb(220, 220, 220)', // darker card background
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: 'rgba(0, 0, 0, 0.87)', // 87% black (slightly softened)
|
||||||
|
secondary: 'rgba(0, 0, 0, 0.6)', // 60% black
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const darkTheme = createTheme({
|
||||||
|
...commonThemeOptions,
|
||||||
|
palette: {
|
||||||
|
mode: 'dark',
|
||||||
|
primary: {
|
||||||
|
main: 'rgb(100, 155, 240)',
|
||||||
|
dark: 'rgb(45, 92, 201)',
|
||||||
|
light: 'rgb(130, 185, 255)',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: 'rgb(69, 173, 255)',
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: 'rgb(49, 51, 56)',
|
||||||
|
paper: 'rgb(62, 64, 68)',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: 'rgb(255, 255, 255)',
|
||||||
|
secondary: 'rgb(179, 179, 179)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { lightTheme, darkTheme };
|
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" />
|
27
tsconfig.app.json
Normal file
27
tsconfig.app.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"types": ["qapp-core/global"]
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
25
tsconfig.node.json
Normal file
25
tsconfig.node.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"types": ["qapp-core/global"]
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
11
vite.config.ts
Normal file
11
vite.config.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
base: "",
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ["@mui/material", "@mui/styled-engine", "@mui/system"],
|
||||||
|
},
|
||||||
|
})
|
Loading…
x
Reference in New Issue
Block a user