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