mirror of
https://github.com/Qortal/names.git
synced 2025-06-18 12:11:21 +00:00
fixed ts , added update and sell conditions
This commit is contained in:
parent
39ca19b079
commit
d1233cc1ad
9
package-lock.json
generated
9
package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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 (
|
||||||
|
@ -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>
|
||||||
|
20
src/hooks/useFetchNames.tsx
Normal file
20
src/hooks/useFetchNames.tsx
Normal 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;
|
||||||
|
};
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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
9
src/interfaces/index.ts
Normal 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';
|
@ -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'));
|
||||||
|
@ -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
|
||||||
|
42
src/state/contexts/FetchNamesProvider.tsx
Normal file
42
src/state/contexts/FetchNamesProvider.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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;
|
||||||
|
@ -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) =>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user