initial commit

This commit is contained in:
PhilReact 2025-05-07 23:49:22 +03:00
commit effcbb6628
48 changed files with 6222 additions and 0 deletions

24
.gitignore vendored Normal file
View File

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

3
.prettierignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
build
dist

23
.prettierrc Normal file
View 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
View File

@ -0,0 +1 @@
1256960

View File

@ -0,0 +1,8 @@
{
"hash": "964445a2",
"configHash": "294caa02",
"lockfileHash": "75101775",
"browserHash": "3e05e511",
"optimized": {},
"chunks": {}
}

3
.vite/deps/package.json Normal file
View File

@ -0,0 +1,3 @@
{
"type": "module"
}

54
README.md Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

39
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

10
src/App.tsx Normal file
View File

@ -0,0 +1,10 @@
import { MyNames } from "./pages/MyNames"
function App() {
return (
<MyNames/>
)
}
export default App

23
src/AppWrapper.tsx Normal file
View 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
View 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} />;
}

View 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>
)
}

View 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%;
}
}

View 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;

View 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>
);
};

View 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>
);
};

View 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;
};

View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
export const publicSalt = "DgRg1PJmFOJI8MibmWzgPX4k4xZo8Hl2mKGhqJ9Zemw=";

View File

@ -0,0 +1,4 @@
import { atom } from 'jotai';
export const namesAtom = atom([]);
export const forSaleAtom = atom([]);

View 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
View 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;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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
View 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
View File

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

27
tsconfig.app.json Normal file
View 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
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

25
tsconfig.node.json Normal file
View 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
View 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"],
},
})