This commit is contained in:
PhilReact 2025-06-04 16:09:58 +03:00
parent e76a417f31
commit 01bc736d6b
3 changed files with 200 additions and 32 deletions

View File

@ -8,8 +8,8 @@ import {
Paper,
Button,
} from '@mui/material';
import { useSetAtom } from 'jotai';
import { forwardRef } from 'react';
import { useAtom, useSetAtom } from 'jotai';
import { forwardRef, useMemo } from 'react';
import { TableVirtuoso, TableComponents } from 'react-virtuoso';
import {
forSaleAtom,
@ -105,7 +105,9 @@ function rowContent(
setNames: SetNames,
setNamesForSale: SetNamesForSale,
isPrimaryNameForSale: boolean,
t: TFunction
t: TFunction,
nameStrings: string[],
pendingBuyNameStrings: string[]
) {
const handleBuy = async (name: string) => {
const loadId = showLoading(
@ -163,13 +165,17 @@ function rowContent(
}
};
const isNameOwned = nameStrings.includes(row.name);
const isNameBuying = pendingBuyNameStrings.includes(row.name);
return (
<>
<TableCell>{row.name}</TableCell>
<TableCell>{row.salePrice}</TableCell>
<TableCell>
<Button
disabled={isPrimaryNameForSale}
disabled={isPrimaryNameForSale || isNameOwned || isNameBuying}
variant="contained"
size="small"
onClick={() => handleBuy(row.name)}
@ -198,10 +204,20 @@ export const ForSaleTable = ({
handleSort,
isPrimaryNameForSale,
}: ForSaleTable) => {
const setNames = useSetAtom(namesAtom);
const [names, setNames] = useAtom(namesAtom);
const setNamesForSale = useSetAtom(forSaleAtom);
const setPendingTxs = useSetAtom(pendingTxsAtom);
const [pendingTxs, setPendingTxs] = useAtom(pendingTxsAtom);
const { t } = useTranslation();
const nameStrings = useMemo(() => {
return names?.map((item) => item.name);
}, [names]);
const pendingBuyNameStrings = useMemo(() => {
const buyNameTxs = pendingTxs['BUY_NAME'];
if (!buyNameTxs) return [];
return Object.values(buyNameTxs).map((tx) => tx.name);
}, [pendingTxs]);
return (
<Paper
sx={{
@ -223,7 +239,9 @@ export const ForSaleTable = ({
setNames,
setNamesForSale,
isPrimaryNameForSale,
t
t,
nameStrings,
pendingBuyNameStrings
)
}
/>

View File

@ -63,6 +63,7 @@ import { TFunction } from 'i18next';
interface NameData {
name: string;
isSelling?: boolean;
forceUpdateState?: number;
}
const getNameQueue = new RequestQueueWithPromise(2);
@ -212,15 +213,17 @@ function rowContent(
);
return;
}
const loadId = showLoading(
t('core:update_name.responses.loading', {
postProcess: 'capitalizeFirstChar',
})
);
let loadId = null;
try {
const response = await modalFunctionsUpdateName.show(undefined);
loadId = showLoading(
t('core:update_name.responses.loading', {
postProcess: 'capitalizeFirstChar',
})
);
if (typeof response !== 'string') throw new Error('Invalid name');
const res = await qortalRequest({
action: 'UPDATE_NAME',
newName: response,
@ -264,13 +267,15 @@ function rowContent(
return;
}
showError(
t('core:update_name.responses.loading', {
t('core:update_name.responses.error', {
postProcess: 'capitalizeFirstChar',
})
);
console.log('error', error);
} finally {
dismissToast(loadId);
if (loadId) {
dismissToast(loadId);
}
}
// Your logic here
@ -285,16 +290,17 @@ function rowContent(
);
return;
}
const loadId = showLoading(
t('core:sell_name.responses.loading', {
postProcess: 'capitalizeFirstChar',
})
);
let loadId = null;
try {
if (name === primaryName) {
await modalFunctions.show({ name });
}
const price = await modalFunctionsSellName.show(name);
loadId = showLoading(
t('core:sell_name.responses.loading', {
postProcess: 'capitalizeFirstChar',
})
);
if (typeof price !== 'string' && typeof price !== 'number')
throw new Error(
t('core:sell_name.responses.error3', {
@ -347,13 +353,15 @@ function rowContent(
);
console.log('error', error);
} finally {
dismissToast(loadId);
if (loadId) {
dismissToast(loadId);
}
}
};
const handleCancel = async (name: string) => {
const loadId = showLoading(
t('core:cancel_name.responses.error2', {
t('core:cancel_name.responses.loading', {
postProcess: 'capitalizeFirstChar',
})
);
@ -381,7 +389,7 @@ function rowContent(
};
});
showSuccess(
t('core:cancel_name.responses.error2', {
t('core:cancel_name.responses.success', {
postProcess: 'capitalizeFirstChar',
})
);
@ -391,12 +399,14 @@ function rowContent(
return;
}
showError(
t('core:cancel_name.responses.error2', {
t('core:cancel_name.responses.error', {
postProcess: 'capitalizeFirstChar',
})
);
} finally {
dismissToast(loadId);
if (loadId) {
dismissToast(loadId);
}
}
};
@ -408,8 +418,20 @@ function rowContent(
display: 'flex',
gap: '5px',
alignItems: 'center',
wordBreak: 'break-word',
}}
>
<Avatar
sx={{
height: '30px',
width: '30px',
objectFit: 'contain',
}}
src={`/arbitrary/THUMBNAIL/${row.name}/qortal_avatar?forceUpdateState=${row?.forceUpdateState}`}
alt={row.name}
>
{row.name?.charAt(0)}
</Avatar>
{primaryName === row.name && (
<Tooltip
title={t('core:tooltips.primary_name', {
@ -467,6 +489,7 @@ function rowContent(
color="error"
size="small"
onClick={() => handleCancel(row.name)}
variant="contained"
disabled={isNameCurrentlyDoingATx}
>
{t('core:actions.cancel_sell', {
@ -495,6 +518,7 @@ export const NameTable = ({ names, primaryName }: NameTableProps) => {
const [namesForSale, setNamesForSale] = useAtom(forSaleAtom);
const [pendingTxs] = useAtom(pendingTxsAtom);
const { t } = useTranslation(['core']);
const [forceUpdateState, forceUpdate] = useState(0);
const modalFunctions = useModal<{ name: string }>();
const modalFunctionsUpdateName = useModal();
@ -504,15 +528,20 @@ export const NameTable = ({ names, primaryName }: NameTableProps) => {
const setPendingTxs = useSetAtom(pendingTxsAtom);
const triggerRerender = useCallback(() => {
forceUpdate((n) => n + 1);
}, []);
const namesToDisplay = useMemo(() => {
const namesForSaleString = namesForSale.map((item) => item.name);
return names.map((name) => {
return {
name: name.name,
isSelling: namesForSaleString.includes(name.name),
forceUpdateState,
};
});
}, [names, namesForSale]);
}, [names, namesForSale, forceUpdateState]);
return (
<Paper
@ -596,7 +625,11 @@ export const NameTable = ({ names, primaryName }: NameTableProps) => {
<UpdateNameModal modalFunctionsUpdateName={modalFunctionsUpdateName} />
)}
{modalFunctionsAvatar?.isShow && (
<AvatarModal modalFunctionsAvatar={modalFunctionsAvatar} />
<AvatarModal
modalFunctionsAvatar={modalFunctionsAvatar}
triggerRerender={triggerRerender}
forceUpdateState={forceUpdateState}
/>
)}
{modalFunctionsSellName?.isShow && (
<SellNameModal modalFunctionsSellName={modalFunctionsSellName} />
@ -612,8 +645,14 @@ interface PickedAvatar {
interface AvatarModalProps {
modalFunctionsAvatar: ModalFunctionsAvatar;
triggerRerender: () => void;
forceUpdateState?: number;
}
const AvatarModal = ({ modalFunctionsAvatar }: AvatarModalProps) => {
const AvatarModal = ({
modalFunctionsAvatar,
triggerRerender,
forceUpdateState,
}: AvatarModalProps) => {
const { t } = useTranslation();
const { setHasAvatar } = usePendingTxs();
const forceRefresh = useSetAtom(forceRefreshAtom);
@ -651,6 +690,7 @@ const AvatarModal = ({ modalFunctionsAvatar }: AvatarModalProps) => {
})
);
modalFunctionsAvatar.onOk(undefined);
triggerRerender();
} catch (error) {
if (error instanceof Error) {
showError(error?.message);
@ -699,10 +739,10 @@ const AvatarModal = ({ modalFunctionsAvatar }: AvatarModalProps) => {
height: '138px',
width: '138px',
}}
src={`/arbitrary/THUMBNAIL/${modalFunctionsAvatar.data.name}/qortal_avatar?async=true`}
src={`/arbitrary/THUMBNAIL/${modalFunctionsAvatar.data.name}/qortal_avatar?forceUpdateState=${forceUpdateState}`}
alt={modalFunctionsAvatar.data.name}
>
<CircularProgress />
{modalFunctionsAvatar?.data?.name?.charAt(0)}
</Avatar>
)}
{pickedAvatar?.base64 && (
@ -1021,7 +1061,7 @@ interface SellNameModalProps {
const SellNameModal = ({ modalFunctionsSellName }: SellNameModalProps) => {
const { t } = useTranslation();
const [price, setPrice] = useState(0);
const [price, setPrice] = useState<number | string>(0);
return (
<Dialog
@ -1044,10 +1084,24 @@ const SellNameModal = ({ modalFunctionsSellName }: SellNameModalProps) => {
<TextField
autoComplete="off"
autoFocus
onChange={(e) => setPrice(+e.target.value)}
onChange={(e) => {
const raw = e.target.value;
// Allow empty input
if (raw === '') {
setPrice('');
return;
}
// Remove leading zeros and convert to number
const numericValue = +raw;
if (!isNaN(numericValue)) {
setPrice(numericValue);
}
}}
value={price}
type="number"
placeholder={t('core:new_name.choose_price', {
placeholder={t('core:sell_name.choose_price', {
postProcess: 'capitalizeFirstChar',
})}
/>

View File

@ -0,0 +1,96 @@
{
"header": {
"my_names": "我的名称",
"market": "待售名称"
},
"inputs": {
"filter_names": "筛选名称"
},
"actions": {
"new_name": "新名称",
"register_name": "注册名称",
"close": "关闭",
"update_avatar": "更新头像",
"set_avatar": "设置头像",
"update": "更新",
"sell": "出售",
"cancel_sell": "取消出售",
"cancel": "取消",
"continue": "继续",
"publish": "发布",
"buy": "购买",
"choose_image": "选择图片"
},
"new_name": {
"choose_name": "选择一个名称",
"balance_message": "你的余额为 {{balance}} QORT。注册名称需要支付 {{nameFee}} QORT。",
"name_available": "{{name}} 可用",
"name_unavailable": "{{name}} 不可用",
"checking_name": "正在检查名称是否已存在",
"responses": {
"success": "名称注册成功",
"error": "无法注册名称",
"loading": "正在注册名称...请稍候"
}
},
"tables": {
"name": "名称",
"actions": "操作"
},
"update_name": {
"responses": {
"loading": "正在更新名称...请稍候",
"success": "名称更新成功",
"error": "无法更新名称"
},
"title": "更新名称时请注意",
"choose_name": "选择新名称",
"balance_info": "你的余额为 {{balance}} QORT。注册名称需要支付 {{nameFee}} QORT。",
"name_available": "{{name}} 可用",
"name_unavailable": "{{name}} 不可用"
},
"sell_name": {
"responses": {
"loading": "正在上架名称出售...请稍候",
"success": "名称已上架出售",
"error1": "拥有其他名称时无法出售主名称",
"error2": "无法上架名称出售",
"error3": "无效的价格"
},
"title": "出售名称",
"choose_price": "设定出售价格"
},
"cancel_name": {
"responses": {
"loading": "正在从市场移除名称...请稍候",
"success": "名称已从市场移除",
"error": "无法从市场移除名称"
}
},
"tooltips": {
"primary_name": "这是你的主名称(身份)"
},
"warnings": {
"warning": "警告",
"primary_name_sell_caution": "出售主名称时请谨慎",
"primary_name_sell": "{{name}} 是你的主名称。如果你是某个私密群组的管理员,出售该名称将移除你在该群组的密钥。请确保其他管理员在出售前重新加密最新密钥。请谨慎操作!",
"update_name1": "如果你更新了名称,你将失去与原名称关联的资源。换句话说,你将失去 QDN 上该名称下内容的所有权。请谨慎操作!"
},
"avatar": {
"responses": {
"loading": "正在发布头像...请稍候",
"success": "头像发布成功",
"error1": "缺少数据",
"error2": "无法发布头像"
},
"title": "发布头像"
},
"market": {
"sale_price": "售价",
"responses": {
"loading": "正在尝试购买名称...请稍候",
"success": "名称购买成功",
"error": "无法购买名称"
}
}
}