fixed ts , added update and sell conditions

This commit is contained in:
PhilReact 2025-05-20 14:34:38 +03:00
parent 39ca19b079
commit d1233cc1ad
17 changed files with 481 additions and 208 deletions

9
package-lock.json generated
View File

@ -13,7 +13,7 @@
"@mui/icons-material": "^7.0.1", "@mui/icons-material": "^7.0.1",
"@mui/material": "^7.0.1", "@mui/material": "^7.0.1",
"jotai": "^2.12.3", "jotai": "^2.12.3",
"qapp-core": "^1.0.27", "qapp-core": "^1.0.30",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-router-dom": "^7.3.0", "react-router-dom": "^7.3.0",
@ -3429,9 +3429,10 @@
} }
}, },
"node_modules/qapp-core": { "node_modules/qapp-core": {
"version": "1.0.27", "version": "1.0.30",
"resolved": "https://registry.npmjs.org/qapp-core/-/qapp-core-1.0.27.tgz", "resolved": "https://registry.npmjs.org/qapp-core/-/qapp-core-1.0.30.tgz",
"integrity": "sha512-RNoHo1vx2K592X3w26+BAUSmDjfk4PIwDdUEyl+YIipYMRZDSCf1+TP3Rh93UGBMIxgcF58lQyXnKV1ttlHNQA==", "integrity": "sha512-6UBeFrsFyOKMRNpQiWDENCuF9PXlbgkwiChh542i46cVFEUd7yuT+i8vlP3wWOrbQwsZtAAs4NI+zpiQuA7+KQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/react-virtual": "^3.13.2", "@tanstack/react-virtual": "^3.13.2",
"bloom-filters": "^3.0.4", "bloom-filters": "^3.0.4",

View File

@ -16,7 +16,7 @@
"@mui/icons-material": "^7.0.1", "@mui/icons-material": "^7.0.1",
"@mui/material": "^7.0.1", "@mui/material": "^7.0.1",
"jotai": "^2.12.3", "jotai": "^2.12.3",
"qapp-core": "^1.0.27", "qapp-core": "^1.0.30",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-router-dom": "^7.3.0", "react-router-dom": "^7.3.0",

View File

@ -2,6 +2,7 @@ import { Routes } from './Routes';
import { GlobalProvider } from 'qapp-core'; import { GlobalProvider } from 'qapp-core';
import { publicSalt } from './qapp-config.ts'; import { publicSalt } from './qapp-config.ts';
import { PendingTxsProvider } from './state/contexts/PendingTxsProvider.tsx'; import { PendingTxsProvider } from './state/contexts/PendingTxsProvider.tsx';
import { FetchNamesProvider } from './state/contexts/FetchNamesProvider.tsx';
export const AppWrapper = () => { export const AppWrapper = () => {
return ( return (
@ -18,9 +19,11 @@ export const AppWrapper = () => {
appName: 'names', appName: 'names',
}} }}
> >
<PendingTxsProvider> <FetchNamesProvider>
<Routes /> <PendingTxsProvider>
</PendingTxsProvider> <Routes />
</PendingTxsProvider>
</FetchNamesProvider>
</GlobalProvider> </GlobalProvider>
); );
}; };

View File

@ -1,10 +1,17 @@
import React from 'react' import './barSpinner.css';
import './barSpinner.css'
export const BarSpinner = ({width = '20px', color}) => { interface BarSpinnerProps {
return ( width: string | number;
<div style={{ color: string;
width,
color: color || 'green'
}} className="loader-bar"></div>
)
} }
export const BarSpinner = ({ width = '20px', color }: BarSpinnerProps) => {
return (
<div
style={{
width,
color: color || 'green',
}}
className="loader-bar"
></div>
);
};

View File

@ -24,12 +24,7 @@ import CheckIcon from '@mui/icons-material/Check';
import ErrorIcon from '@mui/icons-material/Error'; import ErrorIcon from '@mui/icons-material/Error';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { namesAtom, pendingTxsAtom } from '../state/global/names'; import { namesAtom, pendingTxsAtom } from '../state/global/names';
export enum Availability { import { Availability } from '../interfaces';
NULL = 'null',
LOADING = 'loading',
AVAILABLE = 'available',
NOT_AVAILABLE = 'not-available',
}
const Label = styled('label')` const Label = styled('label')`
display: block; display: block;
@ -53,7 +48,7 @@ const RegisterName = () => {
const [isLoadingRegisterName, setIsLoadingRegisterName] = useState(false); const [isLoadingRegisterName, setIsLoadingRegisterName] = useState(false);
const theme = useTheme(); const theme = useTheme();
const [nameFee, setNameFee] = useState(null); const [nameFee, setNameFee] = useState<number | null>(null);
const registerNameFunc = async () => { const registerNameFunc = async () => {
if (!address) return; if (!address) return;
const loadId = showLoading('Registering name...please wait'); const loadId = showLoading('Registering name...please wait');
@ -88,14 +83,18 @@ const RegisterName = () => {
setNameValue(''); setNameValue('');
setIsOpen(false); setIsOpen(false);
} catch (error) { } catch (error) {
showError(error?.message || 'Unable to register name'); if (error instanceof Error) {
showError(error.message);
} else {
showError('Unable to register name');
}
} finally { } finally {
setIsLoadingRegisterName(false); setIsLoadingRegisterName(false);
dismissToast(loadId); dismissToast(loadId);
} }
}; };
const checkIfNameExisits = async (name) => { const checkIfNameExisits = async (name: string) => {
if (!name?.trim()) { if (!name?.trim()) {
setIsNameAvailable(Availability.NULL); setIsNameAvailable(Availability.NULL);
@ -112,7 +111,6 @@ const RegisterName = () => {
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally {
} }
}; };
@ -133,7 +131,7 @@ const RegisterName = () => {
const data = await fetch(`/transactions/unitfee?txType=REGISTER_NAME`); const data = await fetch(`/transactions/unitfee?txType=REGISTER_NAME`);
const fee = await data.text(); const fee = await data.text();
setNameFee((Number(fee) / 1e8).toFixed(8)); setNameFee(Number((Number(fee) / 1e8).toFixed(8)));
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
@ -263,13 +261,13 @@ const RegisterName = () => {
Close Close
</Button> </Button>
<Button <Button
disabled={ disabled={Boolean(
!nameValue.trim() || !nameValue.trim() ||
isLoadingRegisterName || isLoadingRegisterName ||
isNameAvailable !== Availability.AVAILABLE || isNameAvailable !== Availability.AVAILABLE ||
!balance || !balance ||
(balance && nameFee && +balance < +nameFee) (balance && nameFee && +balance < +nameFee)
} )}
variant="contained" variant="contained"
onClick={registerNameFunc} onClick={registerNameFunc}
autoFocus autoFocus

View File

@ -8,30 +8,24 @@ import {
Paper, Paper,
Button, Button,
} from '@mui/material'; } from '@mui/material';
import { useAtom, useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { forwardRef, useMemo } from 'react'; import { forwardRef } from 'react';
import { TableVirtuoso, TableComponents } from 'react-virtuoso'; import { TableVirtuoso, TableComponents } from 'react-virtuoso';
import { import {
forSaleAtom, forSaleAtom,
Names,
namesAtom, namesAtom,
NamesForSale,
pendingTxsAtom, pendingTxsAtom,
PendingTxsState,
} from '../../state/global/names'; } from '../../state/global/names';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import { import { dismissToast, showError, showLoading, showSuccess } from 'qapp-core';
dismissToast, import { SetStateAction } from 'jotai';
showError, import { SortBy, SortDirection } from '../../interfaces';
showLoading,
showSuccess,
useGlobal,
} from 'qapp-core';
interface NameData { const VirtuosoTableComponents: TableComponents<NamesForSale> = {
name: string;
isSelling?: boolean;
}
const VirtuosoTableComponents: TableComponents<NameData> = {
Scroller: forwardRef<HTMLDivElement>((props, ref) => ( Scroller: forwardRef<HTMLDivElement>((props, ref) => (
<TableContainer component={Paper} {...props} ref={ref} /> <TableContainer component={Paper} {...props} ref={ref} />
)), )),
@ -53,7 +47,7 @@ const VirtuosoTableComponents: TableComponents<NameData> = {
function fixedHeaderContent( function fixedHeaderContent(
sortBy: string, sortBy: string,
sortDirection: string, sortDirection: string,
setSort: (field: 'name' | 'salePrice') => void setSort: (field: SortBy) => void
) { ) {
const renderSortIcon = (field: string) => { const renderSortIcon = (field: string) => {
if (sortBy !== field) return null; if (sortBy !== field) return null;
@ -89,13 +83,18 @@ function fixedHeaderContent(
); );
} }
type SetPendingTxs = (update: SetStateAction<PendingTxsState>) => void;
type SetNames = (update: SetStateAction<Names[]>) => void;
type SetNamesForSale = (update: SetStateAction<NamesForSale[]>) => void;
function rowContent( function rowContent(
_index: number, _index: number,
row: NameData, row: NamesForSale,
setPendingTxs, setPendingTxs: SetPendingTxs,
setNames, setNames: SetNames,
setNamesForSale, setNamesForSale: SetNamesForSale
address
) { ) {
const handleBuy = async (name: string) => { const handleBuy = async (name: string) => {
const loadId = showLoading('Attempting to purchase name...please wait'); const loadId = showLoading('Attempting to purchase name...please wait');
@ -131,9 +130,11 @@ function rowContent(
}; };
}); });
} catch (error) { } catch (error) {
showError(error?.message || 'Unable to purchase name'); if (error instanceof Error) {
showError(error?.message);
console.log('error', error); return;
}
showError('Unable to purchase name');
} finally { } finally {
dismissToast(loadId); dismissToast(loadId);
} }
@ -156,13 +157,19 @@ function rowContent(
); );
} }
interface ForSaleTable {
namesForSale: NamesForSale[];
sortDirection: SortDirection;
sortBy: SortBy;
handleSort: (sortBy: SortBy) => void;
}
export const ForSaleTable = ({ export const ForSaleTable = ({
namesForSale, namesForSale,
sortDirection, sortDirection,
sortBy, sortBy,
handleSort, handleSort,
}) => { }: ForSaleTable) => {
const address = useGlobal().auth.address;
const setNames = useSetAtom(namesAtom); const setNames = useSetAtom(namesAtom);
const setNamesForSale = useSetAtom(forSaleAtom); const setNamesForSale = useSetAtom(forSaleAtom);
const setPendingTxs = useSetAtom(pendingTxsAtom); const setPendingTxs = useSetAtom(pendingTxsAtom);
@ -180,15 +187,8 @@ export const ForSaleTable = ({
fixedHeaderContent={() => fixedHeaderContent={() =>
fixedHeaderContent(sortBy, sortDirection, handleSort) fixedHeaderContent(sortBy, sortDirection, handleSort)
} }
itemContent={(index, row) => itemContent={(index, row: NamesForSale) =>
rowContent( rowContent(index, row, setPendingTxs, setNames, setNamesForSale)
index,
row,
setPendingTxs,
setNames,
setNamesForSale,
address
)
} }
/> />
</Paper> </Paper>

View File

@ -20,19 +20,26 @@ import {
CircularProgress, CircularProgress,
Avatar, Avatar,
} from '@mui/material'; } from '@mui/material';
import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { useAtom, useSetAtom } from 'jotai';
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react'; import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
import { TableVirtuoso, TableComponents } from 'react-virtuoso'; import { TableVirtuoso, TableComponents } from 'react-virtuoso';
import { import {
forceRefreshAtom, forceRefreshAtom,
forSaleAtom, forSaleAtom,
Names,
namesAtom, namesAtom,
NamesForSale,
pendingTxsAtom, pendingTxsAtom,
PendingTxsState,
refreshAtom, refreshAtom,
sortedPendingTxsByCategoryAtom,
} from '../../state/global/names'; } from '../../state/global/names';
import PersonIcon from '@mui/icons-material/Person'; import PersonIcon from '@mui/icons-material/Person';
import { useModal } from '../../hooks/useModal'; import {
ModalFunctions,
ModalFunctionsAvatar,
ModalFunctionsSellName,
useModal,
} from '../../hooks/useModal';
import { import {
dismissToast, dismissToast,
ImagePicker, ImagePicker,
@ -43,11 +50,13 @@ import {
Spacer, Spacer,
useGlobal, useGlobal,
} from 'qapp-core'; } from 'qapp-core';
import { Availability } from '../RegisterName';
import CheckIcon from '@mui/icons-material/Check'; import CheckIcon from '@mui/icons-material/Check';
import ErrorIcon from '@mui/icons-material/Error'; import ErrorIcon from '@mui/icons-material/Error';
import { BarSpinner } from '../../common/Spinners/BarSpinner/BarSpinner'; import { BarSpinner } from '../../common/Spinners/BarSpinner/BarSpinner';
import { usePendingTxs } from '../../hooks/useHandlePendingTxs'; import { usePendingTxs } from '../../hooks/useHandlePendingTxs';
import { FetchPrimaryNameType, useFetchNames } from '../../hooks/useFetchNames';
import { Availability } from '../../interfaces';
import { SetStateAction } from 'jotai';
interface NameData { interface NameData {
name: string; name: string;
isSelling?: boolean; isSelling?: boolean;
@ -87,40 +96,48 @@ function fixedHeaderContent() {
); );
} }
const ManageAvatar = ({ name, modalFunctionsAvatar }) => { interface ManageAvatarProps {
name: string;
modalFunctionsAvatar: ModalFunctionsAvatar;
}
const ManageAvatar = ({ name, modalFunctionsAvatar }: ManageAvatarProps) => {
const { setHasAvatar, getHasAvatar } = usePendingTxs(); const { setHasAvatar, getHasAvatar } = usePendingTxs();
const [refresh] = useAtom(refreshAtom); // just to subscribe const [refresh] = useAtom(refreshAtom); // just to subscribe
const [hasAvatarState, setHasAvatarState] = useState<boolean | null>(null); const [hasAvatarState, setHasAvatarState] = useState<boolean | null>(null);
const checkIfAvatarExists = useCallback(async (name) => { const checkIfAvatarExists = useCallback(
try { async (name: string) => {
const res = getHasAvatar(name); try {
if (res !== null) { const res = getHasAvatar(name);
setHasAvatarState(res); if (res !== null) {
return; setHasAvatarState(res);
} return;
const identifier = `qortal_avatar`; }
const url = `/arbitrary/resources/searchsimple?mode=ALL&service=THUMBNAIL&identifier=${identifier}&limit=1&name=${name}&includemetadata=false&prefix=true`; const identifier = `qortal_avatar`;
const response = await getNameQueue.enqueue(() => const url = `/arbitrary/resources/searchsimple?mode=ALL&service=THUMBNAIL&identifier=${identifier}&limit=1&name=${name}&includemetadata=false&prefix=true`;
fetch(url, { const response = await getNameQueue.enqueue(() =>
method: 'GET', fetch(url, {
headers: { method: 'GET',
'Content-Type': 'application/json', headers: {
}, 'Content-Type': 'application/json',
}) },
); })
);
const responseData = await response.json(); const responseData = await response.json();
if (responseData?.length > 0) { if (responseData?.length > 0) {
setHasAvatarState(true); setHasAvatarState(true);
setHasAvatar(name, true); setHasAvatar(name, true);
} else { } else {
setHasAvatarState(false); setHasAvatarState(false);
}
} catch (error) {
console.log(error);
} }
} catch (error) { },
console.log(error); [getHasAvatar, setHasAvatar]
} );
}, []);
useEffect(() => { useEffect(() => {
if (!name) return; if (!name) return;
checkIfAvatarExists(name); checkIfAvatarExists(name);
@ -131,7 +148,7 @@ const ManageAvatar = ({ name, modalFunctionsAvatar }) => {
size="small" size="small"
disabled={hasAvatarState === null} disabled={hasAvatarState === null}
onClick={() => onClick={() =>
modalFunctionsAvatar.show({ name, hasAvatar: hasAvatarState }) modalFunctionsAvatar.show({ name, hasAvatar: Boolean(hasAvatarState) })
} }
> >
{hasAvatarState === null ? ( {hasAvatarState === null ? (
@ -145,23 +162,35 @@ const ManageAvatar = ({ name, modalFunctionsAvatar }) => {
); );
}; };
type SetPendingTxs = (update: SetStateAction<PendingTxsState>) => void;
type SetNames = (update: SetStateAction<Names[]>) => void;
type SetNamesForSale = (update: SetStateAction<NamesForSale[]>) => void;
function rowContent( function rowContent(
_index: number, _index: number,
row: NameData, row: NameData,
primaryName?: string, primaryName: string,
modalFunctions?: any, address: string,
modalFunctionsUpdateName?: any, fetchPrimaryName: FetchPrimaryNameType,
modalFunctionsAvatar?: any, numberOfNames: number,
modalFunctionsSellName?: any, modalFunctions: ModalFunctions,
setPendingTxs?: any, modalFunctionsUpdateName: ReturnType<typeof useModal>,
setNames?: any, modalFunctionsAvatar: ModalFunctionsAvatar,
setNamesForSale?: any modalFunctionsSellName: ReturnType<typeof useModal>,
setPendingTxs: SetPendingTxs,
setNames: SetNames,
setNamesForSale: SetNamesForSale
) { ) {
const handleUpdate = async (name: string) => { const handleUpdate = async (name: string) => {
if (name === primaryName && numberOfNames > 1) {
showError('Cannot update primary name while having other names');
return;
}
const loadId = showLoading('Updating name...please wait'); const loadId = showLoading('Updating name...please wait');
try { try {
const response = await modalFunctionsUpdateName.show(); const response = await modalFunctionsUpdateName.show(undefined);
if (typeof response !== 'string') throw new Error('Invalid name');
const res = await qortalRequest({ const res = await qortalRequest({
action: 'UPDATE_NAME', action: 'UPDATE_NAME',
newName: response, newName: response,
@ -189,13 +218,18 @@ function rowContent(
}; };
return copyArray; return copyArray;
}); });
fetchPrimaryName(address);
}, },
}, // add or overwrite this transaction }, // add or overwrite this transaction
}, },
}; };
}); });
} catch (error) { } catch (error) {
showError(error?.message || 'Unable to update name'); if (error instanceof Error) {
showError(error?.message);
return;
}
showError('Unable to update name');
console.log('error', error); console.log('error', error);
} finally { } finally {
dismissToast(loadId); dismissToast(loadId);
@ -205,16 +239,22 @@ function rowContent(
}; };
const handleSell = async (name: string) => { const handleSell = async (name: string) => {
if (name === primaryName && numberOfNames > 1) {
showError('Cannot sell primary name while having other names');
return;
}
const loadId = showLoading('Placing name for sale...please wait'); const loadId = showLoading('Placing name for sale...please wait');
try { try {
if (name === primaryName) { if (name === primaryName) {
await modalFunctions.show({ name }); await modalFunctions.show({ name });
} }
const price = await modalFunctionsSellName.show(name); const price = await modalFunctionsSellName.show(name);
if (typeof price !== 'string' && typeof price !== 'number')
throw new Error('Invalid price');
const res = await qortalRequest({ const res = await qortalRequest({
action: 'SELL_NAME', action: 'SELL_NAME',
nameForSale: name, nameForSale: name,
salePrice: price, salePrice: +price,
}); });
showSuccess('Placed name for sale'); showSuccess('Placed name for sale');
setPendingTxs((prev) => { setPendingTxs((prev) => {
@ -241,7 +281,12 @@ function rowContent(
}; };
}); });
} catch (error) { } catch (error) {
showError(error?.message || 'Unable to place name for sale'); if (error instanceof Error) {
showError(error?.message);
return;
}
showError('Unable to place name for sale');
console.log('error', error); console.log('error', error);
} finally { } finally {
dismissToast(loadId); dismissToast(loadId);
@ -275,7 +320,11 @@ function rowContent(
}); });
showSuccess('Removed name from market'); showSuccess('Removed name from market');
} catch (error) { } catch (error) {
showError(error?.message || 'Unable to remove name from market'); if (error instanceof Error) {
showError(error?.message);
return;
}
showError('Unable to remove name from market');
console.log('error', error); console.log('error', error);
} finally { } finally {
dismissToast(loadId); dismissToast(loadId);
@ -349,18 +398,20 @@ function rowContent(
); );
} }
export const NameTable = ({ names, primaryName }) => { interface NameTableProps {
names: Names[];
primaryName: string;
}
export const NameTable = ({ names, primaryName }: NameTableProps) => {
const setNames = useSetAtom(namesAtom); const setNames = useSetAtom(namesAtom);
const { auth } = useGlobal();
const [namesForSale, setNamesForSale] = useAtom(forSaleAtom); const [namesForSale, setNamesForSale] = useAtom(forSaleAtom);
const modalFunctions = useModal(); const modalFunctions = useModal<{ name: string }>();
const modalFunctionsUpdateName = useModal(); const modalFunctionsUpdateName = useModal();
const modalFunctionsAvatar = useModal(); const modalFunctionsAvatar = useModal<{ name: string; hasAvatar: boolean }>();
const modalFunctionsSellName = useModal(); const modalFunctionsSellName = useModal();
const categoryAtom = useMemo( const { fetchPrimaryName } = useFetchNames();
() => sortedPendingTxsByCategoryAtom('REGISTER_NAME'),
[]
);
const txs = useAtomValue(categoryAtom);
const setPendingTxs = useSetAtom(pendingTxsAtom); const setPendingTxs = useSetAtom(pendingTxsAtom);
const namesToDisplay = useMemo(() => { const namesToDisplay = useMemo(() => {
@ -389,6 +440,9 @@ export const NameTable = ({ names, primaryName }) => {
index, index,
row, row,
primaryName, primaryName,
auth?.address || '',
fetchPrimaryName,
names?.length,
modalFunctions, modalFunctions,
modalFunctionsUpdateName, modalFunctionsUpdateName,
modalFunctionsAvatar, modalFunctionsAvatar,
@ -422,7 +476,7 @@ export const NameTable = ({ names, primaryName }) => {
<Button <Button
color="warning" color="warning"
variant="contained" variant="contained"
onClick={modalFunctions.onOk} onClick={() => modalFunctions.onOk(undefined)}
autoFocus autoFocus
> >
continue continue
@ -446,45 +500,45 @@ export const NameTable = ({ names, primaryName }) => {
); );
}; };
const AvatarModal = ({ modalFunctionsAvatar }) => { interface PickedAvatar {
base64: string;
name: string;
}
interface AvatarModalProps {
modalFunctionsAvatar: ModalFunctionsAvatar;
}
const AvatarModal = ({ modalFunctionsAvatar }: AvatarModalProps) => {
const { setHasAvatar } = usePendingTxs(); const { setHasAvatar } = usePendingTxs();
const forceRefresh = useSetAtom(forceRefreshAtom); const forceRefresh = useSetAtom(forceRefreshAtom);
const [arbitraryFee, setArbitraryFee] = useState(''); const [pickedAvatar, setPickedAvatar] = useState<null | PickedAvatar>(null);
const [pickedAvatar, setPickedAvatar] = useState<any>(null);
const [isLoadingPublish, setIsLoadingPublish] = useState(false); const [isLoadingPublish, setIsLoadingPublish] = useState(false);
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 publishAvatar = async () => {
const loadId = showLoading('Publishing avatar...please wait'); const loadId = showLoading('Publishing avatar...please wait');
try { try {
if (!modalFunctionsAvatar?.data || !pickedAvatar?.base64)
throw new Error('Missing data');
setIsLoadingPublish(true); setIsLoadingPublish(true);
await qortalRequest({ await qortalRequest({
action: 'PUBLISH_QDN_RESOURCE', action: 'PUBLISH_QDN_RESOURCE',
base64: pickedAvatar?.base64, base64: pickedAvatar?.base64,
service: 'THUMBNAIL', service: 'THUMBNAIL',
identifier: 'qortal_avatar', identifier: 'qortal_avatar',
name: modalFunctionsAvatar.data.name, name: modalFunctionsAvatar?.data?.name,
}); });
setHasAvatar(modalFunctionsAvatar.data.name, true); setHasAvatar(modalFunctionsAvatar?.data?.name, true);
forceRefresh(); forceRefresh();
showSuccess('Successfully published avatar'); showSuccess('Successfully published avatar');
modalFunctionsAvatar.onOk(); modalFunctionsAvatar.onOk(undefined);
} catch (error) { } catch (error) {
showError(error?.message || 'Unable to publish avatar'); if (error instanceof Error) {
showError(error?.message);
return;
}
showError('Unable to publish avatar');
} finally { } finally {
dismissToast(loadId); dismissToast(loadId);
setIsLoadingPublish(false); setIsLoadingPublish(false);
@ -513,7 +567,7 @@ const AvatarModal = ({ modalFunctionsAvatar }) => {
width: '100%', width: '100%',
}} }}
> >
{modalFunctionsAvatar.data.hasAvatar && !pickedAvatar?.base64 && ( {modalFunctionsAvatar?.data?.hasAvatar && !pickedAvatar?.base64 && (
<Avatar <Avatar
sx={{ sx={{
height: '138px', height: '138px',
@ -532,7 +586,7 @@ const AvatarModal = ({ modalFunctionsAvatar }) => {
width: '138px', width: '138px',
}} }}
src={`data:image/webp;base64,${pickedAvatar?.base64}`} src={`data:image/webp;base64,${pickedAvatar?.base64}`}
alt={modalFunctionsAvatar.data.name} alt={modalFunctionsAvatar?.data?.name}
> >
<CircularProgress /> <CircularProgress />
</Avatar> </Avatar>
@ -575,18 +629,24 @@ const AvatarModal = ({ modalFunctionsAvatar }) => {
); );
}; };
const UpdateNameModal = ({ modalFunctionsUpdateName }) => { interface UpdateNameModalProps {
modalFunctionsUpdateName: ReturnType<typeof useModal>;
}
const UpdateNameModal = ({
modalFunctionsUpdateName,
}: UpdateNameModalProps) => {
const [step, setStep] = useState(1); const [step, setStep] = useState(1);
const [newName, setNewName] = useState(''); const [newName, setNewName] = useState('');
const [isNameAvailable, setIsNameAvailable] = useState<Availability>( const [isNameAvailable, setIsNameAvailable] = useState<Availability>(
Availability.NULL Availability.NULL
); );
const [nameFee, setNameFee] = useState(null); const [nameFee, setNameFee] = useState<null | number>(null);
const balance = useGlobal().auth.balance; const balance = useGlobal().auth.balance;
const theme = useTheme(); const theme = useTheme();
const checkIfNameExisits = async (name) => { const checkIfNameExisits = async (name: string) => {
if (!name?.trim()) { if (!name?.trim()) {
setIsNameAvailable(Availability.NULL); setIsNameAvailable(Availability.NULL);
@ -603,7 +663,6 @@ const UpdateNameModal = ({ modalFunctionsUpdateName }) => {
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally {
} }
}; };
@ -624,7 +683,7 @@ const UpdateNameModal = ({ modalFunctionsUpdateName }) => {
const data = await fetch(`/transactions/unitfee?txType=REGISTER_NAME`); const data = await fetch(`/transactions/unitfee?txType=REGISTER_NAME`);
const fee = await data.text(); const fee = await data.text();
setNameFee((Number(fee) / 1e8).toFixed(8)); setNameFee(+(Number(fee) / 1e8).toFixed(8));
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
@ -758,12 +817,12 @@ const UpdateNameModal = ({ modalFunctionsUpdateName }) => {
<Button <Button
color="primary" color="primary"
variant="contained" variant="contained"
disabled={ disabled={Boolean(
!newName?.trim() || !newName?.trim() ||
isNameAvailable !== Availability.AVAILABLE || isNameAvailable !== Availability.AVAILABLE ||
!balance || !balance ||
(balance && nameFee && +balance < +nameFee) (balance && nameFee && +balance < +nameFee)
} )}
onClick={() => modalFunctionsUpdateName.onOk(newName.trim())} onClick={() => modalFunctionsUpdateName.onOk(newName.trim())}
autoFocus autoFocus
> >
@ -783,7 +842,11 @@ const UpdateNameModal = ({ modalFunctionsUpdateName }) => {
); );
}; };
const SellNameModal = ({ modalFunctionsSellName }) => { interface SellNameModalProps {
modalFunctionsSellName: ModalFunctionsSellName;
}
const SellNameModal = ({ modalFunctionsSellName }: SellNameModalProps) => {
const [price, setPrice] = useState(0); const [price, setPrice] = useState(0);
return ( return (

View File

@ -11,15 +11,13 @@ import {
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import { TableVirtuoso, TableComponents } from 'react-virtuoso'; import { TableVirtuoso, TableComponents } from 'react-virtuoso';
import { allSortedPendingTxsAtom } from '../../state/global/names'; import {
allSortedPendingTxsAtom,
NameTransactions,
} from '../../state/global/names';
import { Spacer } from 'qapp-core'; import { Spacer } from 'qapp-core';
interface NameData { const VirtuosoTableComponents: TableComponents<NameTransactions> = {
name: string;
isSelling?: boolean;
}
const VirtuosoTableComponents: TableComponents<NameData> = {
Scroller: forwardRef<HTMLDivElement>((props, ref) => ( Scroller: forwardRef<HTMLDivElement>((props, ref) => (
<TableContainer component={Paper} {...props} ref={ref} /> <TableContainer component={Paper} {...props} ref={ref} />
)), )),
@ -47,7 +45,7 @@ function fixedHeaderContent() {
); );
} }
function rowContent(_index: number, row: NameData) { function rowContent(_index: number, row: NameTransactions) {
return ( return (
<> <>
<TableCell>{row.type}</TableCell> <TableCell>{row.type}</TableCell>

View File

@ -0,0 +1,20 @@
// PendingTxsContext.tsx
import { createContext, useContext } from 'react';
export type FetchPrimaryNameType = (address: string) => void;
type FetchNamesContextType = {
fetchPrimaryName: FetchPrimaryNameType;
};
export const FetchNamesContext = createContext<
FetchNamesContextType | undefined
>(undefined);
export const useFetchNames = () => {
const context = useContext(FetchNamesContext);
if (!context) {
throw new Error('useFetchNames must be used within a FetchNamesProvider');
}
return context;
};

View File

@ -1,15 +1,16 @@
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { forSaleAtom, namesAtom } from '../state/global/names'; import { forSaleAtom, Names, namesAtom } from '../state/global/names';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { useGlobal } from 'qapp-core'; import { useGlobal } from 'qapp-core';
import { usePendingTxs } from './useHandlePendingTxs'; import { usePendingTxs } from './useHandlePendingTxs';
import { useFetchNames } from './useFetchNames';
export const useHandleNameData = () => { export const useHandleNameData = () => {
const setNamesForSale = useSetAtom(forSaleAtom); const setNamesForSale = useSetAtom(forSaleAtom);
const setNames = useSetAtom(namesAtom); const setNames = useSetAtom(namesAtom);
const address = useGlobal().auth.address; const address = useGlobal().auth.address;
const { clearPendingTxs } = usePendingTxs(); const { clearPendingTxs } = usePendingTxs();
const { fetchPrimaryName } = useFetchNames();
const getNamesForSale = useCallback(async () => { const getNamesForSale = useCallback(async () => {
try { try {
const res = await fetch('/names/forsale?limit=0&reverse=true'); const res = await fetch('/names/forsale?limit=0&reverse=true');
@ -33,13 +34,22 @@ export const useHandleNameData = () => {
clearPendingTxs( clearPendingTxs(
'REGISTER_NAMES', 'REGISTER_NAMES',
'name', 'name',
res?.map((item) => item.name) res?.map((item: Names) => item.name)
); );
setNames(res); setNames(res);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
}, [address, setNames]); }, [address, setNames, clearPendingTxs]);
const getPrimaryName = useCallback(async () => {
if (!address) return;
try {
fetchPrimaryName(address);
} catch (error) {
console.error(error);
}
}, [address, fetchPrimaryName]);
// Initial fetch + interval // Initial fetch + interval
useEffect(() => { useEffect(() => {
@ -50,9 +60,8 @@ export const useHandleNameData = () => {
useEffect(() => { useEffect(() => {
getMyNames(); getMyNames();
// const interval = setInterval(getMyNames, 120_000); // every 2 minutes getPrimaryName();
// return () => clearInterval(interval); }, [getMyNames, getPrimaryName]);
}, [getMyNames]);
return null; return null;
}; };

View File

@ -4,13 +4,34 @@ interface State {
isShow: boolean; isShow: boolean;
} }
export const useModal = () => { export type ModalFunctions<
const [state, setState] = useState<State>({ isShow: false }); TData = { name: string },
const [data, setData] = useState(null); TResult = unknown,
const promiseConfig = useRef<any>(null); > = ReturnType<typeof useModal<TData, TResult>>;
const show = useCallback((data) => { export type ModalFunctionsAvatar<
setData(data); TData = { name: string; hasAvatar: boolean },
TResult = unknown,
> = ReturnType<typeof useModal<TData, TResult>>;
export type ModalFunctionsSellName<
TData = unknown,
TResult = unknown,
> = ReturnType<typeof useModal<TData, TResult>>;
export const useModal = <TData = unknown, TResult = unknown>() => {
const [state, setState] = useState<State>({ isShow: false });
const [data, setData] = useState<TData | undefined>(undefined);
const promiseConfig = useRef<{
resolve: (value: TResult) => void;
reject: () => void;
} | null>(null);
const show = useCallback((inputData?: TData): Promise<TResult> => {
if (inputData !== undefined && inputData !== null) {
setData(inputData);
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
promiseConfig.current = { resolve, reject }; promiseConfig.current = { resolve, reject };
setState({ isShow: true }); setState({ isShow: true });
@ -19,14 +40,14 @@ export const useModal = () => {
const hide = useCallback(() => { const hide = useCallback(() => {
setState({ isShow: false }); setState({ isShow: false });
setData(null); setData(undefined);
}, []); }, []);
const onOk = useCallback( const onOk = useCallback(
(payload: any) => { (result: TResult) => {
const { resolve } = promiseConfig.current || {}; const { resolve } = promiseConfig.current || {};
hide(); hide();
resolve?.(payload); resolve?.(result);
}, },
[hide] [hide]
); );

9
src/interfaces/index.ts Normal file
View File

@ -0,0 +1,9 @@
export enum Availability {
NULL = 'null',
LOADING = 'loading',
AVAILABLE = 'available',
NOT_AVAILABLE = 'not-available',
}
export type SortDirection = 'asc' | 'desc';
export type SortBy = 'name' | 'salePrice';

View File

@ -3,11 +3,12 @@ import { ForSaleTable } from '../components/Tables/ForSaleTable';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { forSaleAtom } from '../state/global/names'; import { forSaleAtom } from '../state/global/names';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { SortBy, SortDirection } from '../interfaces';
export const Market = () => { export const Market = () => {
const [namesForSale] = useAtom(forSaleAtom); const [namesForSale] = useAtom(forSaleAtom);
const [sortBy, setSortBy] = useState<'name' | 'salePrice'>('name'); const [sortBy, setSortBy] = useState<SortBy>('name');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
const [filterValue, setFilterValue] = useState(''); const [filterValue, setFilterValue] = useState('');
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const namesForSaleFiltered = useMemo(() => { const namesForSaleFiltered = useMemo(() => {
@ -52,7 +53,7 @@ export const Market = () => {
}, [value]); }, [value]);
const handleSort = useCallback( const handleSort = useCallback(
(field: 'name' | 'salePrice') => { (field: SortBy) => {
if (sortBy === field) { if (sortBy === field) {
// Toggle direction // Toggle direction
setSortDirection((prev) => (prev === 'asc' ? 'desc' : 'asc')); setSortDirection((prev) => (prev === 'asc' ? 'desc' : 'asc'));

View File

@ -1,16 +1,15 @@
import { useAtom, useSetAtom } from 'jotai'; import { useAtom } from 'jotai';
import { useGlobal } from 'qapp-core'; import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { namesAtom, primaryNameAtom } from '../state/global/names';
import { namesAtom } from '../state/global/names';
import { NameTable } from '../components/Tables/NameTable'; import { NameTable } from '../components/Tables/NameTable';
import { Box, Button, TextField } from '@mui/material'; import { Box, TextField } from '@mui/material';
import RegisterName from '../components/RegisterName'; import RegisterName from '../components/RegisterName';
export const MyNames = () => { export const MyNames = () => {
const [names] = useAtom(namesAtom); const [names] = useAtom(namesAtom);
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [filterValue, setFilterValue] = useState(''); const [filterValue, setFilterValue] = useState('');
const [primaryName] = useAtom(primaryNameAtom);
useEffect(() => { useEffect(() => {
const handler = setTimeout(() => { const handler = setTimeout(() => {
setFilterValue(value); setFilterValue(value);
@ -27,12 +26,15 @@ export const MyNames = () => {
const filtered = !lowerFilter const filtered = !lowerFilter
? names ? names
: names.filter((item) => item.name.toLowerCase().includes(lowerFilter)); : names.filter((item) => item.name.toLowerCase().includes(lowerFilter));
return filtered;
}, [names, filterValue]);
const primaryName = useMemo(() => { // Sort to move primaryName to the top if it exists in the list
return names[0]?.name || ''; return [...filtered].sort((a, b) => {
}, [names]); if (a.name === primaryName) return -1;
if (b.name === primaryName) return 1;
return 0;
});
}, [names, filterValue, primaryName]);
return ( return (
<div> <div>
<Box <Box

View File

@ -0,0 +1,42 @@
// PendingTxsProvider.tsx
import { ReactNode, useCallback, useMemo } from 'react';
import { useSetAtom } from 'jotai';
import { primaryNameAtom } from '../global/names';
import { FetchNamesContext } from '../../hooks/useFetchNames';
export const FetchNamesProvider = ({ children }: { children: ReactNode }) => {
const setPrimaryName = useSetAtom(primaryNameAtom);
const fetchPrimaryName = useCallback(
async (address: string) => {
if (!address) return;
try {
const res = await qortalRequest({
action: 'GET_ACCOUNT_NAMES',
address,
limit: 0,
offset: 0,
reverse: false,
});
if (res?.length > 0) {
setPrimaryName(res[0]?.name);
}
} catch (error) {
console.error(error);
}
},
[setPrimaryName]
);
const value = useMemo(
() => ({
fetchPrimaryName,
}),
[fetchPrimaryName]
);
return (
<FetchNamesContext.Provider value={value}>
{children}
</FetchNamesContext.Provider>
);
};

View File

@ -1,20 +1,20 @@
// PendingTxsProvider.tsx // PendingTxsProvider.tsx
import { ReactNode, useEffect, useCallback, useMemo, useRef } from 'react'; import { ReactNode, useEffect, useCallback, useMemo, useRef } from 'react';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { pendingTxsAtom } from '../global/names'; import { pendingTxsAtom, TransactionCategory } from '../global/names';
import { PendingTxsContext } from '../../hooks/useHandlePendingTxs'; import { PendingTxsContext } from '../../hooks/useHandlePendingTxs';
const TX_CHECK_INTERVAL = 80000; const TX_CHECK_INTERVAL = 80000;
export const PendingTxsProvider = ({ children }: { children: ReactNode }) => { export const PendingTxsProvider = ({ children }: { children: ReactNode }) => {
const [pendingTxs, setPendingTxs] = useAtom(pendingTxsAtom); const [pendingTxs, setPendingTxs] = useAtom(pendingTxsAtom);
const hasAvatarRef = useRef({}); const hasAvatarRef = useRef<Record<string, boolean>>({});
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
const categories = Object.keys(pendingTxs); const categories = Object.keys(pendingTxs);
categories.forEach((category) => { categories.forEach((category) => {
const txs = pendingTxs[category]; const txs = pendingTxs[category as TransactionCategory];
if (!txs) return; if (!txs) return;
Object.entries(txs).forEach(async ([signature, tx]) => { Object.entries(txs).forEach(async ([signature, tx]) => {
@ -27,7 +27,9 @@ export const PendingTxsProvider = ({ children }: { children: ReactNode }) => {
if (data?.blockHeight) { if (data?.blockHeight) {
setPendingTxs((prev) => { setPendingTxs((prev) => {
const newCategory = { ...prev[category] }; const newCategory = {
...prev[category as TransactionCategory],
};
delete newCategory[signature]; delete newCategory[signature];
const updated = { const updated = {
@ -36,7 +38,7 @@ export const PendingTxsProvider = ({ children }: { children: ReactNode }) => {
}; };
if (Object.keys(newCategory).length === 0) { if (Object.keys(newCategory).length === 0) {
delete updated[category]; delete updated[category as TransactionCategory];
} }
return updated; return updated;
@ -57,12 +59,12 @@ export const PendingTxsProvider = ({ children }: { children: ReactNode }) => {
const clearPendingTxs = useCallback( const clearPendingTxs = useCallback(
(category: string, fieldName: string, values: string[]) => { (category: string, fieldName: string, values: string[]) => {
setPendingTxs((prev) => { setPendingTxs((prev) => {
const categoryTxs = prev[category]; const categoryTxs = prev[category as TransactionCategory];
if (!categoryTxs) return prev; if (!categoryTxs) return prev;
const filtered = Object.fromEntries( const filtered = Object.fromEntries(
Object.entries(categoryTxs).filter( Object.entries(categoryTxs).filter(
([_, tx]) => !values.includes(tx[fieldName]) ([_, tx]) => !values.includes(tx[fieldName as 'name'])
) )
); );
@ -72,7 +74,7 @@ export const PendingTxsProvider = ({ children }: { children: ReactNode }) => {
}; };
if (Object.keys(filtered).length === 0) { if (Object.keys(filtered).length === 0) {
delete updated[category]; delete updated[category as TransactionCategory];
} }
return updated; return updated;

View File

@ -1,16 +1,113 @@
import { atom } from 'jotai'; import { atom } from 'jotai';
type TransactionCategory = 'REGISTER_NAME'; interface AdditionalFields {
callback: () => void;
status: 'PENDING';
}
interface RegisterNameTransaction {
type: 'REGISTER_NAME';
timestamp: number;
reference: string;
fee: string;
signature: string;
txGroupId: number;
blockHeight: number;
approvalStatus: 'NOT_REQUIRED';
creatorAddress: string;
registrantPublicKey: string;
name: string;
data: string;
}
interface UpdateNameTransaction {
type: 'UPDATE_NAME';
timestamp: number;
reference: string;
fee: string;
signature: string;
txGroupId: number;
blockHeight: number;
approvalStatus: 'NOT_REQUIRED';
creatorAddress: string;
ownerPublicKey: string;
name: string;
newName: string;
newData: string;
}
interface SellNameTransaction {
type: 'SELL_NAME';
timestamp: number;
reference: string;
fee: string;
signature: string;
txGroupId: number;
blockHeight: number;
approvalStatus: 'NOT_REQUIRED';
creatorAddress: string;
ownerPublicKey: string;
name: string;
amount: string;
}
interface CancelSellNameTransaction {
type: 'CANCEL_SELL_NAME';
timestamp: number;
reference: string;
fee: string;
signature: string;
txGroupId: number;
blockHeight: number;
approvalStatus: 'NOT_REQUIRED';
creatorAddress: string;
ownerPublicKey: string;
name: string;
}
interface BuyNameTransaction {
type: 'BUY_NAME';
timestamp: number;
reference: string;
fee: string;
signature: string;
txGroupId: number;
blockHeight: number;
approvalStatus: 'NOT_REQUIRED';
creatorAddress: string;
buyerPublicKey: string;
name: string;
amount: string;
seller: string;
}
export type NameTransactions =
| (RegisterNameTransaction & AdditionalFields)
| (UpdateNameTransaction & AdditionalFields)
| (SellNameTransaction & AdditionalFields)
| (CancelSellNameTransaction & AdditionalFields)
| (BuyNameTransaction & AdditionalFields);
type TransactionMap = { type TransactionMap = {
[signature: string]: any; // replace `any` with your transaction type if known [signature: string]: NameTransactions;
}; };
type PendingTxsState = { export type TransactionCategory = NameTransactions['type'];
export type PendingTxsState = {
[key in TransactionCategory]?: TransactionMap; [key in TransactionCategory]?: TransactionMap;
}; };
export const namesAtom = atom([]);
export const forSaleAtom = atom([]); export interface Names {
name: string;
owner: string;
}
export interface NamesForSale {
name: string;
salePrice: number;
}
export const namesAtom = atom<Names[]>([]);
export const primaryNameAtom = atom('');
export const forSaleAtom = atom<NamesForSale[]>([]);
export const pendingTxsAtom = atom<PendingTxsState>({}); export const pendingTxsAtom = atom<PendingTxsState>({});
export const sortedPendingTxsByCategoryAtom = (category: string) => export const sortedPendingTxsByCategoryAtom = (category: string) =>