mirror of
https://github.com/Qortal/names.git
synced 2025-06-14 18:21:21 +00:00
updates
This commit is contained in:
parent
effcbb6628
commit
39ca19b079
20
connect-qapp.sh
Executable file
20
connect-qapp.sh
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# CONFIGURATION
|
||||||
|
REMOTE_USER=phil
|
||||||
|
REMOTE_HOST=devnet-nodes.qortal.link
|
||||||
|
QAPP_PORT=22393
|
||||||
|
CORE_API_PORT=22391
|
||||||
|
EDITOR_PORT=5174
|
||||||
|
|
||||||
|
# Start reverse tunnel in background
|
||||||
|
echo "Starting SSH tunnel to $REMOTE_HOST..."
|
||||||
|
|
||||||
|
ssh -p22221 -o ServerAliveInterval=30 \
|
||||||
|
-L $QAPP_PORT:127.0.0.1:$QAPP_PORT \
|
||||||
|
-L $CORE_API_PORT:127.0.0.1:$CORE_API_PORT \
|
||||||
|
-R $EDITOR_PORT:127.0.0.1:$EDITOR_PORT \
|
||||||
|
$REMOTE_USER@$REMOTE_HOST
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
|||||||
import { Routes } from "./Routes";
|
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';
|
||||||
|
|
||||||
export const AppWrapper = () => {
|
export const AppWrapper = () => {
|
||||||
return (
|
return (
|
||||||
@ -14,10 +15,12 @@ export const AppWrapper = () => {
|
|||||||
authenticateOnMount: true,
|
authenticateOnMount: true,
|
||||||
},
|
},
|
||||||
publicSalt: publicSalt,
|
publicSalt: publicSalt,
|
||||||
appName: 'names'
|
appName: 'names',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Routes />
|
<PendingTxsProvider>
|
||||||
|
<Routes />
|
||||||
|
</PendingTxsProvider>
|
||||||
</GlobalProvider>
|
</GlobalProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,10 +5,6 @@ import {
|
|||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemIcon,
|
|
||||||
ListItemText,
|
|
||||||
styled,
|
styled,
|
||||||
TextField,
|
TextField,
|
||||||
Typography,
|
Typography,
|
||||||
@ -23,10 +19,11 @@ import {
|
|||||||
useGlobal,
|
useGlobal,
|
||||||
} from 'qapp-core';
|
} from 'qapp-core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked';
|
|
||||||
import { BarSpinner } from '../common/Spinners/BarSpinner/BarSpinner';
|
import { BarSpinner } from '../common/Spinners/BarSpinner/BarSpinner';
|
||||||
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 { useSetAtom } from 'jotai';
|
||||||
|
import { namesAtom, pendingTxsAtom } from '../state/global/names';
|
||||||
export enum Availability {
|
export enum Availability {
|
||||||
NULL = 'null',
|
NULL = 'null',
|
||||||
LOADING = 'loading',
|
LOADING = 'loading',
|
||||||
@ -45,21 +42,48 @@ const Label = styled('label')`
|
|||||||
const RegisterName = () => {
|
const RegisterName = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const balance = useGlobal().auth.balance;
|
const balance = useGlobal().auth.balance;
|
||||||
|
const setNames = useSetAtom(namesAtom);
|
||||||
|
|
||||||
|
const address = useGlobal().auth.address;
|
||||||
const [nameValue, setNameValue] = useState('');
|
const [nameValue, setNameValue] = useState('');
|
||||||
const [isNameAvailable, setIsNameAvailable] = useState<Availability>(
|
const [isNameAvailable, setIsNameAvailable] = useState<Availability>(
|
||||||
Availability.NULL
|
Availability.NULL
|
||||||
);
|
);
|
||||||
|
const setPendingTxs = useSetAtom(pendingTxsAtom);
|
||||||
|
|
||||||
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(null);
|
||||||
const registerNameFunc = async () => {
|
const registerNameFunc = async () => {
|
||||||
|
if (!address) return;
|
||||||
const loadId = showLoading('Registering name...please wait');
|
const loadId = showLoading('Registering name...please wait');
|
||||||
try {
|
try {
|
||||||
setIsLoadingRegisterName(true);
|
setIsLoadingRegisterName(true);
|
||||||
await qortalRequest({
|
const res = await qortalRequest({
|
||||||
action: 'REGISTER_NAME',
|
action: 'REGISTER_NAME',
|
||||||
name: nameValue,
|
name: nameValue,
|
||||||
});
|
});
|
||||||
|
setPendingTxs((prev) => {
|
||||||
|
return {
|
||||||
|
...prev, // preserve existing categories
|
||||||
|
['REGISTER_NAME']: {
|
||||||
|
...(prev['REGISTER_NAME'] || {}), // preserve existing transactions in this category
|
||||||
|
[res.signature]: {
|
||||||
|
...res,
|
||||||
|
status: 'PENDING',
|
||||||
|
callback: () => {
|
||||||
|
setNames((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
name: res.name,
|
||||||
|
owner: res.creatorAddress,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
}, // add or overwrite this transaction
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
showSuccess('Successfully registered a name');
|
showSuccess('Successfully registered a name');
|
||||||
setNameValue('');
|
setNameValue('');
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
@ -8,12 +8,23 @@ import {
|
|||||||
Paper,
|
Paper,
|
||||||
Button,
|
Button,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom, useSetAtom } from 'jotai';
|
||||||
import { forwardRef, useMemo } from 'react';
|
import { forwardRef, useMemo } from 'react';
|
||||||
import { TableVirtuoso, TableComponents } from 'react-virtuoso';
|
import { TableVirtuoso, TableComponents } from 'react-virtuoso';
|
||||||
import { forSaleAtom, namesAtom } from '../../state/global/names';
|
import {
|
||||||
|
forSaleAtom,
|
||||||
|
namesAtom,
|
||||||
|
pendingTxsAtom,
|
||||||
|
} 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 {
|
||||||
|
dismissToast,
|
||||||
|
showError,
|
||||||
|
showLoading,
|
||||||
|
showSuccess,
|
||||||
|
useGlobal,
|
||||||
|
} from 'qapp-core';
|
||||||
|
|
||||||
interface NameData {
|
interface NameData {
|
||||||
name: string;
|
name: string;
|
||||||
@ -78,21 +89,53 @@ function fixedHeaderContent(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function rowContent(_index: number, row: NameData) {
|
function rowContent(
|
||||||
const handleUpdate = () => {
|
_index: number,
|
||||||
console.log('Update:', row.name);
|
row: NameData,
|
||||||
// Your logic here
|
setPendingTxs,
|
||||||
};
|
setNames,
|
||||||
|
setNamesForSale,
|
||||||
|
address
|
||||||
|
) {
|
||||||
const handleBuy = async (name: string) => {
|
const handleBuy = async (name: string) => {
|
||||||
|
const loadId = showLoading('Attempting to purchase name...please wait');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('hello');
|
const res = await qortalRequest({
|
||||||
await qortalRequest({
|
|
||||||
action: 'BUY_NAME',
|
action: 'BUY_NAME',
|
||||||
nameForSale: name,
|
nameForSale: name,
|
||||||
});
|
});
|
||||||
|
showSuccess('Purchased name');
|
||||||
|
setPendingTxs((prev) => {
|
||||||
|
return {
|
||||||
|
...prev, // preserve existing categories
|
||||||
|
['BUY_NAME']: {
|
||||||
|
...(prev['BUY_NAME'] || {}), // preserve existing transactions in this category
|
||||||
|
[res.signature]: {
|
||||||
|
...res,
|
||||||
|
status: 'PENDING',
|
||||||
|
callback: () => {
|
||||||
|
setNamesForSale((prev) =>
|
||||||
|
prev.filter((item) => item?.name !== res.name)
|
||||||
|
);
|
||||||
|
setNames((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
name: res.name,
|
||||||
|
owner: res.creatorAddress,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
}, // add or overwrite this transaction
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
showError(error?.message || 'Unable to purchase name');
|
||||||
|
|
||||||
console.log('error', error);
|
console.log('error', error);
|
||||||
|
} finally {
|
||||||
|
dismissToast(loadId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -119,6 +162,11 @@ export const ForSaleTable = ({
|
|||||||
sortBy,
|
sortBy,
|
||||||
handleSort,
|
handleSort,
|
||||||
}) => {
|
}) => {
|
||||||
|
const address = useGlobal().auth.address;
|
||||||
|
const setNames = useSetAtom(namesAtom);
|
||||||
|
const setNamesForSale = useSetAtom(forSaleAtom);
|
||||||
|
const setPendingTxs = useSetAtom(pendingTxsAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
sx={{
|
sx={{
|
||||||
@ -132,7 +180,16 @@ export const ForSaleTable = ({
|
|||||||
fixedHeaderContent={() =>
|
fixedHeaderContent={() =>
|
||||||
fixedHeaderContent(sortBy, sortDirection, handleSort)
|
fixedHeaderContent(sortBy, sortDirection, handleSort)
|
||||||
}
|
}
|
||||||
itemContent={rowContent}
|
itemContent={(index, row) =>
|
||||||
|
rowContent(
|
||||||
|
index,
|
||||||
|
row,
|
||||||
|
setPendingTxs,
|
||||||
|
setNames,
|
||||||
|
setNamesForSale,
|
||||||
|
address
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
@ -20,10 +20,17 @@ import {
|
|||||||
CircularProgress,
|
CircularProgress,
|
||||||
Avatar,
|
Avatar,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom, useAtomValue, 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 { forSaleAtom, namesAtom } from '../../state/global/names';
|
import {
|
||||||
|
forceRefreshAtom,
|
||||||
|
forSaleAtom,
|
||||||
|
namesAtom,
|
||||||
|
pendingTxsAtom,
|
||||||
|
refreshAtom,
|
||||||
|
sortedPendingTxsByCategoryAtom,
|
||||||
|
} from '../../state/global/names';
|
||||||
import PersonIcon from '@mui/icons-material/Person';
|
import PersonIcon from '@mui/icons-material/Person';
|
||||||
import { useModal } from '../../hooks/useModal';
|
import { useModal } from '../../hooks/useModal';
|
||||||
import {
|
import {
|
||||||
@ -40,6 +47,7 @@ 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';
|
||||||
interface NameData {
|
interface NameData {
|
||||||
name: string;
|
name: string;
|
||||||
isSelling?: boolean;
|
isSelling?: boolean;
|
||||||
@ -80,10 +88,17 @@ function fixedHeaderContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ManageAvatar = ({ name, modalFunctionsAvatar }) => {
|
const ManageAvatar = ({ name, modalFunctionsAvatar }) => {
|
||||||
const [hasAvatar, setHasAvatar] = useState<boolean | null>(null);
|
const { setHasAvatar, getHasAvatar } = usePendingTxs();
|
||||||
|
const [refresh] = useAtom(refreshAtom); // just to subscribe
|
||||||
|
const [hasAvatarState, setHasAvatarState] = useState<boolean | null>(null);
|
||||||
|
|
||||||
const checkIfAvatarExists = useCallback(async (name) => {
|
const checkIfAvatarExists = useCallback(async (name) => {
|
||||||
try {
|
try {
|
||||||
|
const res = getHasAvatar(name);
|
||||||
|
if (res !== null) {
|
||||||
|
setHasAvatarState(res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const identifier = `qortal_avatar`;
|
const identifier = `qortal_avatar`;
|
||||||
const url = `/arbitrary/resources/searchsimple?mode=ALL&service=THUMBNAIL&identifier=${identifier}&limit=1&name=${name}&includemetadata=false&prefix=true`;
|
const url = `/arbitrary/resources/searchsimple?mode=ALL&service=THUMBNAIL&identifier=${identifier}&limit=1&name=${name}&includemetadata=false&prefix=true`;
|
||||||
const response = await getNameQueue.enqueue(() =>
|
const response = await getNameQueue.enqueue(() =>
|
||||||
@ -97,9 +112,10 @@ const ManageAvatar = ({ name, modalFunctionsAvatar }) => {
|
|||||||
|
|
||||||
const responseData = await response.json();
|
const responseData = await response.json();
|
||||||
if (responseData?.length > 0) {
|
if (responseData?.length > 0) {
|
||||||
setHasAvatar(true);
|
setHasAvatarState(true);
|
||||||
|
setHasAvatar(name, true);
|
||||||
} else {
|
} else {
|
||||||
setHasAvatar(false);
|
setHasAvatarState(false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@ -108,17 +124,19 @@ const ManageAvatar = ({ name, modalFunctionsAvatar }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
checkIfAvatarExists(name);
|
checkIfAvatarExists(name);
|
||||||
}, [name, checkIfAvatarExists]);
|
}, [name, checkIfAvatarExists, refresh]);
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
disabled={hasAvatar === null}
|
disabled={hasAvatarState === null}
|
||||||
onClick={() => modalFunctionsAvatar.show({ name, hasAvatar })}
|
onClick={() =>
|
||||||
|
modalFunctionsAvatar.show({ name, hasAvatar: hasAvatarState })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{hasAvatar === null ? (
|
{hasAvatarState === null ? (
|
||||||
<CircularProgress size={10} />
|
<CircularProgress size={10} />
|
||||||
) : hasAvatar ? (
|
) : hasAvatarState ? (
|
||||||
'Change avatar'
|
'Change avatar'
|
||||||
) : (
|
) : (
|
||||||
'Set avatar'
|
'Set avatar'
|
||||||
@ -134,51 +152,133 @@ function rowContent(
|
|||||||
modalFunctions?: any,
|
modalFunctions?: any,
|
||||||
modalFunctionsUpdateName?: any,
|
modalFunctionsUpdateName?: any,
|
||||||
modalFunctionsAvatar?: any,
|
modalFunctionsAvatar?: any,
|
||||||
modalFunctionsSellName?: any
|
modalFunctionsSellName?: any,
|
||||||
|
setPendingTxs?: any,
|
||||||
|
setNames?: any,
|
||||||
|
setNamesForSale?: any
|
||||||
) {
|
) {
|
||||||
const handleUpdate = async (name: string) => {
|
const handleUpdate = async (name: string) => {
|
||||||
|
const loadId = showLoading('Updating name...please wait');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await modalFunctionsUpdateName.show();
|
const response = await modalFunctionsUpdateName.show();
|
||||||
console.log('Update:', row.name);
|
const res = await qortalRequest({
|
||||||
console.log('hello', response);
|
|
||||||
await qortalRequest({
|
|
||||||
action: 'UPDATE_NAME',
|
action: 'UPDATE_NAME',
|
||||||
newName: response,
|
newName: response,
|
||||||
oldName: name,
|
oldName: name,
|
||||||
});
|
});
|
||||||
|
showSuccess('Successfully updated name');
|
||||||
|
setPendingTxs((prev) => {
|
||||||
|
return {
|
||||||
|
...prev, // preserve existing categories
|
||||||
|
['UPDATE_NAME']: {
|
||||||
|
...(prev['UPDATE_NAME'] || {}), // preserve existing transactions in this category
|
||||||
|
[res.signature]: {
|
||||||
|
...res,
|
||||||
|
status: 'PENDING',
|
||||||
|
callback: () => {
|
||||||
|
setNames((prev) => {
|
||||||
|
const copyArray = [...prev];
|
||||||
|
const findIndex = copyArray.findIndex(
|
||||||
|
(item) => item.name === res.name
|
||||||
|
);
|
||||||
|
if (findIndex === -1) return copyArray;
|
||||||
|
copyArray[findIndex] = {
|
||||||
|
name: res.newName,
|
||||||
|
owner: res.creatorAddress,
|
||||||
|
};
|
||||||
|
return copyArray;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}, // add or overwrite this transaction
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
showError(error?.message || 'Unable to update name');
|
||||||
console.log('error', error);
|
console.log('error', error);
|
||||||
|
} finally {
|
||||||
|
dismissToast(loadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Your logic here
|
// Your logic here
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSell = async (name: string) => {
|
const handleSell = async (name: string) => {
|
||||||
|
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);
|
||||||
console.log('hello');
|
const res = await qortalRequest({
|
||||||
await qortalRequest({
|
|
||||||
action: 'SELL_NAME',
|
action: 'SELL_NAME',
|
||||||
nameForSale: name,
|
nameForSale: name,
|
||||||
salePrice: price,
|
salePrice: price,
|
||||||
});
|
});
|
||||||
|
showSuccess('Placed name for sale');
|
||||||
|
setPendingTxs((prev) => {
|
||||||
|
return {
|
||||||
|
...prev, // preserve existing categories
|
||||||
|
['SELL_NAME']: {
|
||||||
|
...(prev['SELL_NAME'] || {}), // preserve existing transactions in this category
|
||||||
|
[res.signature]: {
|
||||||
|
...res,
|
||||||
|
status: 'PENDING',
|
||||||
|
callback: () => {
|
||||||
|
setNamesForSale((prev) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: res.name,
|
||||||
|
salePrice: res.amount,
|
||||||
|
},
|
||||||
|
...prev,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}, // add or overwrite this transaction
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
showError(error?.message || 'Unable to place name for sale');
|
||||||
console.log('error', error);
|
console.log('error', error);
|
||||||
|
} finally {
|
||||||
|
dismissToast(loadId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = async (name: string) => {
|
const handleCancel = async (name: string) => {
|
||||||
|
const loadId = showLoading('Removing name from market...please wait');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('hello', name);
|
const res = await qortalRequest({
|
||||||
await qortalRequest({
|
|
||||||
action: 'CANCEL_SELL_NAME',
|
action: 'CANCEL_SELL_NAME',
|
||||||
nameForSale: name,
|
nameForSale: name,
|
||||||
});
|
});
|
||||||
|
setPendingTxs((prev) => {
|
||||||
|
return {
|
||||||
|
...prev, // preserve existing categories
|
||||||
|
['CANCEL_SELL_NAME']: {
|
||||||
|
...(prev['CANCEL_SELL_NAME'] || {}), // preserve existing transactions in this category
|
||||||
|
[res.signature]: {
|
||||||
|
...res,
|
||||||
|
status: 'PENDING',
|
||||||
|
callback: () => {
|
||||||
|
setNamesForSale((prev) =>
|
||||||
|
prev.filter((item) => item?.name !== res.name)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}, // add or overwrite this transaction
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
showSuccess('Removed name from market');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
showError(error?.message || 'Unable to remove name from market');
|
||||||
console.log('error', error);
|
console.log('error', error);
|
||||||
|
} finally {
|
||||||
|
dismissToast(loadId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -250,14 +350,19 @@ function rowContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const NameTable = ({ names, primaryName }) => {
|
export const NameTable = ({ names, primaryName }) => {
|
||||||
// const [names, setNames] = useAtom(namesAtom);
|
const setNames = useSetAtom(namesAtom);
|
||||||
const [namesForSale] = useAtom(forSaleAtom);
|
const [namesForSale, setNamesForSale] = useAtom(forSaleAtom);
|
||||||
const modalFunctions = useModal();
|
const modalFunctions = useModal();
|
||||||
const modalFunctionsUpdateName = useModal();
|
const modalFunctionsUpdateName = useModal();
|
||||||
const modalFunctionsAvatar = useModal();
|
const modalFunctionsAvatar = useModal();
|
||||||
const modalFunctionsSellName = useModal();
|
const modalFunctionsSellName = useModal();
|
||||||
|
const categoryAtom = useMemo(
|
||||||
|
() => sortedPendingTxsByCategoryAtom('REGISTER_NAME'),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const txs = useAtomValue(categoryAtom);
|
||||||
|
const setPendingTxs = useSetAtom(pendingTxsAtom);
|
||||||
|
|
||||||
console.log('names', names);
|
|
||||||
const namesToDisplay = useMemo(() => {
|
const namesToDisplay = useMemo(() => {
|
||||||
const namesForSaleString = namesForSale.map((item) => item.name);
|
const namesForSaleString = namesForSale.map((item) => item.name);
|
||||||
return names.map((name) => {
|
return names.map((name) => {
|
||||||
@ -287,7 +392,10 @@ export const NameTable = ({ names, primaryName }) => {
|
|||||||
modalFunctions,
|
modalFunctions,
|
||||||
modalFunctionsUpdateName,
|
modalFunctionsUpdateName,
|
||||||
modalFunctionsAvatar,
|
modalFunctionsAvatar,
|
||||||
modalFunctionsSellName
|
modalFunctionsSellName,
|
||||||
|
setPendingTxs,
|
||||||
|
setNames,
|
||||||
|
setNamesForSale
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -339,11 +447,12 @@ export const NameTable = ({ names, primaryName }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AvatarModal = ({ modalFunctionsAvatar }) => {
|
const AvatarModal = ({ modalFunctionsAvatar }) => {
|
||||||
|
const { setHasAvatar } = usePendingTxs();
|
||||||
|
const forceRefresh = useSetAtom(forceRefreshAtom);
|
||||||
|
|
||||||
const [arbitraryFee, setArbitraryFee] = useState('');
|
const [arbitraryFee, setArbitraryFee] = useState('');
|
||||||
const [pickedAvatar, setPickedAvatar] = useState<any>(null);
|
const [pickedAvatar, setPickedAvatar] = useState<any>(null);
|
||||||
const [isLoadingPublish, setIsLoadingPublish] = useState(false);
|
const [isLoadingPublish, setIsLoadingPublish] = useState(false);
|
||||||
const theme = useTheme();
|
|
||||||
console.log('pickedAvatar', pickedAvatar);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getArbitraryName = async () => {
|
const getArbitraryName = async () => {
|
||||||
try {
|
try {
|
||||||
@ -369,6 +478,9 @@ const AvatarModal = ({ modalFunctionsAvatar }) => {
|
|||||||
identifier: 'qortal_avatar',
|
identifier: 'qortal_avatar',
|
||||||
name: modalFunctionsAvatar.data.name,
|
name: modalFunctionsAvatar.data.name,
|
||||||
});
|
});
|
||||||
|
setHasAvatar(modalFunctionsAvatar.data.name, true);
|
||||||
|
forceRefresh();
|
||||||
|
|
||||||
showSuccess('Successfully published avatar');
|
showSuccess('Successfully published avatar');
|
||||||
modalFunctionsAvatar.onOk();
|
modalFunctionsAvatar.onOk();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
87
src/components/Tables/PendingTxsTable.tsx
Normal file
87
src/components/Tables/PendingTxsTable.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Paper,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
import { TableVirtuoso, TableComponents } from 'react-virtuoso';
|
||||||
|
import { allSortedPendingTxsAtom } from '../../state/global/names';
|
||||||
|
import { Spacer } from 'qapp-core';
|
||||||
|
|
||||||
|
interface NameData {
|
||||||
|
name: string;
|
||||||
|
isSelling?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VirtuosoTableComponents: TableComponents<NameData> = {
|
||||||
|
Scroller: forwardRef<HTMLDivElement>((props, ref) => (
|
||||||
|
<TableContainer component={Paper} {...props} ref={ref} />
|
||||||
|
)),
|
||||||
|
Table: (props) => (
|
||||||
|
<Table
|
||||||
|
{...props}
|
||||||
|
sx={{ borderCollapse: 'separate', tableLayout: 'fixed' }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
TableHead: forwardRef<HTMLTableSectionElement>((props, ref) => (
|
||||||
|
<TableHead {...props} ref={ref} />
|
||||||
|
)),
|
||||||
|
TableRow,
|
||||||
|
TableBody: forwardRef<HTMLTableSectionElement>((props, ref) => (
|
||||||
|
<TableBody {...props} ref={ref} />
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
|
||||||
|
function fixedHeaderContent() {
|
||||||
|
return (
|
||||||
|
<TableRow sx={{ backgroundColor: 'background.paper' }}>
|
||||||
|
<TableCell>Tx type</TableCell>
|
||||||
|
<TableCell>Info</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowContent(_index: number, row: NameData) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TableCell>{row.type}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{row.type === 'REGISTER_NAME' && `Name: ${row.name}`}
|
||||||
|
{row.type === 'UPDATE_NAME' && `New name: ${row.newName}`}
|
||||||
|
{row.type === 'SELL_NAME' && `name: ${row.name}`}
|
||||||
|
{row.type === 'CANCEL_SELL_NAME' && `name: ${row.name}`}
|
||||||
|
{row.type === 'BUY_NAME' && `name: ${row.name}`}
|
||||||
|
</TableCell>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PendingTxsTable = () => {
|
||||||
|
const allTxs = useAtomValue(allSortedPendingTxsAtom);
|
||||||
|
if (allTxs?.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Spacer height="20px" />
|
||||||
|
<Typography variant="h3">Pending transactions</Typography>
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
height: '250px', // Header + footer height
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableVirtuoso
|
||||||
|
data={allTxs}
|
||||||
|
components={VirtuosoTableComponents}
|
||||||
|
fixedHeaderContent={fixedHeaderContent}
|
||||||
|
itemContent={rowContent}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -2,11 +2,13 @@ import { useSetAtom } from 'jotai';
|
|||||||
import { forSaleAtom, namesAtom } from '../state/global/names';
|
import { forSaleAtom, 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';
|
||||||
|
|
||||||
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 getNamesForSale = useCallback(async () => {
|
const getNamesForSale = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@ -28,6 +30,11 @@ export const useHandleNameData = () => {
|
|||||||
offset: 0,
|
offset: 0,
|
||||||
reverse: false,
|
reverse: false,
|
||||||
});
|
});
|
||||||
|
clearPendingTxs(
|
||||||
|
'REGISTER_NAMES',
|
||||||
|
'name',
|
||||||
|
res?.map((item) => item.name)
|
||||||
|
);
|
||||||
setNames(res);
|
setNames(res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -43,8 +50,8 @@ export const useHandleNameData = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getMyNames();
|
getMyNames();
|
||||||
const interval = setInterval(getMyNames, 120_000); // every 2 minutes
|
// const interval = setInterval(getMyNames, 120_000); // every 2 minutes
|
||||||
return () => clearInterval(interval);
|
// return () => clearInterval(interval);
|
||||||
}, [getMyNames]);
|
}, [getMyNames]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
24
src/hooks/useHandlePendingTxs.tsx
Normal file
24
src/hooks/useHandlePendingTxs.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// PendingTxsContext.tsx
|
||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
type PendingTxsContextType = {
|
||||||
|
clearPendingTxs: (
|
||||||
|
category: string,
|
||||||
|
fieldName: string,
|
||||||
|
values: string[]
|
||||||
|
) => void;
|
||||||
|
getHasAvatar: (name: string) => boolean | null;
|
||||||
|
setHasAvatar: (name: string, hasAvatar: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PendingTxsContext = createContext<
|
||||||
|
PendingTxsContextType | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
export const usePendingTxs = () => {
|
||||||
|
const context = useContext(PendingTxsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('usePendingTxs must be used within a PendingTxsProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
@ -1,7 +1,7 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from 'react';
|
||||||
import { To, useNavigate } from "react-router-dom";
|
import { To, useNavigate } from 'react-router-dom';
|
||||||
import { EnumTheme, themeAtom } from "../state/global/system";
|
import { EnumTheme, themeAtom } from '../state/global/system';
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from 'jotai';
|
||||||
|
|
||||||
interface CustomWindow extends Window {
|
interface CustomWindow extends Window {
|
||||||
_qdnTheme: string;
|
_qdnTheme: string;
|
||||||
@ -9,41 +9,41 @@ interface CustomWindow extends Window {
|
|||||||
const customWindow = window as unknown as CustomWindow;
|
const customWindow = window as unknown as CustomWindow;
|
||||||
|
|
||||||
export const useIframe = () => {
|
export const useIframe = () => {
|
||||||
const setTheme = useSetAtom(themeAtom);
|
const setTheme = useSetAtom(themeAtom);
|
||||||
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('customWindow', customWindow?._qdnTheme)
|
const themeColorDefault = customWindow?._qdnTheme;
|
||||||
const themeColorDefault = customWindow?._qdnTheme
|
if (themeColorDefault === 'dark') {
|
||||||
if(themeColorDefault === 'dark'){
|
setTheme(EnumTheme.DARK);
|
||||||
setTheme(EnumTheme.DARK)
|
} else if (themeColorDefault === 'light') {
|
||||||
} else if(themeColorDefault === 'light'){
|
setTheme(EnumTheme.LIGHT);
|
||||||
setTheme(EnumTheme.LIGHT)
|
}
|
||||||
}
|
function handleNavigation(event: {
|
||||||
function handleNavigation(event: { data: { action: string; path: To; theme: 'dark' | 'light' }; }) {
|
data: { action: string; path: To; theme: 'dark' | 'light' };
|
||||||
if (event.data?.action === "NAVIGATE_TO_PATH" && event.data.path) {
|
}) {
|
||||||
|
if (event.data?.action === 'NAVIGATE_TO_PATH' && event.data.path) {
|
||||||
navigate(event.data.path); // Navigate directly to the specified path
|
navigate(event.data.path); // Navigate directly to the specified path
|
||||||
|
|
||||||
// Send a response back to the parent window after navigation is handled
|
// Send a response back to the parent window after navigation is handled
|
||||||
window.parent.postMessage(
|
window.parent.postMessage(
|
||||||
{ action: "NAVIGATION_SUCCESS", path: event.data.path },
|
{ action: 'NAVIGATION_SUCCESS', path: event.data.path },
|
||||||
"*"
|
'*'
|
||||||
);
|
);
|
||||||
} else if (event.data?.action === "THEME_CHANGED" && event.data.theme) {
|
} else if (event.data?.action === 'THEME_CHANGED' && event.data.theme) {
|
||||||
const themeColor = event.data.theme
|
const themeColor = event.data.theme;
|
||||||
if(themeColor === 'dark'){
|
if (themeColor === 'dark') {
|
||||||
setTheme(EnumTheme.DARK)
|
setTheme(EnumTheme.DARK);
|
||||||
} else if(themeColor === 'light'){
|
} else if (themeColor === 'light') {
|
||||||
setTheme(EnumTheme.LIGHT)
|
setTheme(EnumTheme.LIGHT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("message", handleNavigation);
|
window.addEventListener('message', handleNavigation);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("message", handleNavigation);
|
window.removeEventListener('message', handleNavigation);
|
||||||
};
|
};
|
||||||
}, [navigate, setTheme]);
|
}, [navigate, setTheme]);
|
||||||
return { navigate };
|
return { navigate };
|
||||||
|
@ -65,9 +65,6 @@ export const Market = () => {
|
|||||||
[sortBy]
|
[sortBy]
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('sortDirection', sortDirection);
|
|
||||||
console.log('sortBy', sortBy);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Box
|
<Box
|
||||||
|
@ -24,7 +24,6 @@ export const MyNames = () => {
|
|||||||
|
|
||||||
const filteredNames = useMemo(() => {
|
const filteredNames = useMemo(() => {
|
||||||
const lowerFilter = filterValue.trim().toLowerCase();
|
const lowerFilter = filterValue.trim().toLowerCase();
|
||||||
console.log('lowerFilter', lowerFilter, names);
|
|
||||||
const filtered = !lowerFilter
|
const filtered = !lowerFilter
|
||||||
? names
|
? names
|
||||||
: names.filter((item) => item.name.toLowerCase().includes(lowerFilter));
|
: names.filter((item) => item.name.toLowerCase().includes(lowerFilter));
|
||||||
|
108
src/state/contexts/PendingTxsProvider.tsx
Normal file
108
src/state/contexts/PendingTxsProvider.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// PendingTxsProvider.tsx
|
||||||
|
import { ReactNode, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
|
import { pendingTxsAtom } from '../global/names';
|
||||||
|
import { PendingTxsContext } from '../../hooks/useHandlePendingTxs';
|
||||||
|
|
||||||
|
const TX_CHECK_INTERVAL = 80000;
|
||||||
|
|
||||||
|
export const PendingTxsProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [pendingTxs, setPendingTxs] = useAtom(pendingTxsAtom);
|
||||||
|
const hasAvatarRef = useRef({});
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
const categories = Object.keys(pendingTxs);
|
||||||
|
|
||||||
|
categories.forEach((category) => {
|
||||||
|
const txs = pendingTxs[category];
|
||||||
|
if (!txs) return;
|
||||||
|
|
||||||
|
Object.entries(txs).forEach(async ([signature, tx]) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/transactions/signature/${signature}`
|
||||||
|
);
|
||||||
|
if (!response.ok) throw new Error(`Fetch failed for ${signature}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data?.blockHeight) {
|
||||||
|
setPendingTxs((prev) => {
|
||||||
|
const newCategory = { ...prev[category] };
|
||||||
|
delete newCategory[signature];
|
||||||
|
|
||||||
|
const updated = {
|
||||||
|
...prev,
|
||||||
|
[category]: newCategory,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Object.keys(newCategory).length === 0) {
|
||||||
|
delete updated[category];
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
|
||||||
|
tx.callback?.();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to check tx ${signature}`, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, TX_CHECK_INTERVAL);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [pendingTxs, setPendingTxs]);
|
||||||
|
|
||||||
|
const clearPendingTxs = useCallback(
|
||||||
|
(category: string, fieldName: string, values: string[]) => {
|
||||||
|
setPendingTxs((prev) => {
|
||||||
|
const categoryTxs = prev[category];
|
||||||
|
if (!categoryTxs) return prev;
|
||||||
|
|
||||||
|
const filtered = Object.fromEntries(
|
||||||
|
Object.entries(categoryTxs).filter(
|
||||||
|
([_, tx]) => !values.includes(tx[fieldName])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const updated = {
|
||||||
|
...prev,
|
||||||
|
[category]: filtered,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Object.keys(filtered).length === 0) {
|
||||||
|
delete updated[category];
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setPendingTxs]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getHasAvatar = useCallback((name: string) => {
|
||||||
|
return hasAvatarRef.current[name] || null;
|
||||||
|
}, []);
|
||||||
|
const setHasAvatar = useCallback((name: string, hasAvatar: boolean) => {
|
||||||
|
hasAvatarRef.current = {
|
||||||
|
...hasAvatarRef.current,
|
||||||
|
[name]: hasAvatar,
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
clearPendingTxs,
|
||||||
|
getHasAvatar,
|
||||||
|
setHasAvatar,
|
||||||
|
}),
|
||||||
|
[clearPendingTxs, getHasAvatar, setHasAvatar]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PendingTxsContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</PendingTxsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,39 @@
|
|||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
|
type TransactionCategory = 'REGISTER_NAME';
|
||||||
|
|
||||||
|
type TransactionMap = {
|
||||||
|
[signature: string]: any; // replace `any` with your transaction type if known
|
||||||
|
};
|
||||||
|
|
||||||
|
type PendingTxsState = {
|
||||||
|
[key in TransactionCategory]?: TransactionMap;
|
||||||
|
};
|
||||||
export const namesAtom = atom([]);
|
export const namesAtom = atom([]);
|
||||||
export const forSaleAtom = atom([]);
|
export const forSaleAtom = atom([]);
|
||||||
|
export const pendingTxsAtom = atom<PendingTxsState>({});
|
||||||
|
|
||||||
|
export const sortedPendingTxsByCategoryAtom = (category: string) =>
|
||||||
|
atom((get) => {
|
||||||
|
const txsByCategory = get(pendingTxsAtom)[category as TransactionCategory];
|
||||||
|
if (!txsByCategory) return [];
|
||||||
|
|
||||||
|
return Object.values(txsByCategory).sort(
|
||||||
|
(a, b) => b.timestamp - a.timestamp
|
||||||
|
); // Newest to oldest
|
||||||
|
});
|
||||||
|
|
||||||
|
export const allSortedPendingTxsAtom = atom((get) => {
|
||||||
|
const allTxsByCategory = get(pendingTxsAtom);
|
||||||
|
|
||||||
|
const allTxs = Object.values(allTxsByCategory)
|
||||||
|
.flatMap((categoryMap) => Object.values(categoryMap))
|
||||||
|
.filter((tx) => typeof tx.timestamp === 'number');
|
||||||
|
|
||||||
|
return allTxs.sort((a, b) => b.timestamp - a.timestamp);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const refreshAtom = atom(0);
|
||||||
|
export const forceRefreshAtom = atom(null, (get, set) => {
|
||||||
|
set(refreshAtom, get(refreshAtom) + 1);
|
||||||
|
});
|
||||||
|
@ -3,6 +3,7 @@ import { useIframe } from '../hooks/useIframeListener';
|
|||||||
import { AppBar, Toolbar, Button, Box, useTheme } from '@mui/material';
|
import { AppBar, Toolbar, Button, Box, useTheme } from '@mui/material';
|
||||||
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
|
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
|
||||||
import StorefrontIcon from '@mui/icons-material/Storefront';
|
import StorefrontIcon from '@mui/icons-material/Storefront';
|
||||||
|
import { PendingTxsTable } from '../components/Tables/PendingTxsTable';
|
||||||
|
|
||||||
const Layout = () => {
|
const Layout = () => {
|
||||||
useIframe();
|
useIframe();
|
||||||
@ -54,6 +55,7 @@ const Layout = () => {
|
|||||||
|
|
||||||
<Box component="main">
|
<Box component="main">
|
||||||
<main>
|
<main>
|
||||||
|
<PendingTxsTable />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</Box>
|
</Box>
|
||||||
|
20
stop-qapp.sh
Executable file
20
stop-qapp.sh
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
PID_FILE=".qapp-tunnel.pid"
|
||||||
|
|
||||||
|
if [ ! -f "$PID_FILE" ]; then
|
||||||
|
echo "Tunnel PID file not found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TUNNEL_PID=$(cat "$PID_FILE")
|
||||||
|
|
||||||
|
if ps -p $TUNNEL_PID > /dev/null; then
|
||||||
|
echo "Stopping SSH tunnel with PID $TUNNEL_PID..."
|
||||||
|
kill $TUNNEL_PID
|
||||||
|
rm "$PID_FILE"
|
||||||
|
echo "Tunnel stopped."
|
||||||
|
else
|
||||||
|
echo "No active tunnel with PID $TUNNEL_PID. Removing stale PID file."
|
||||||
|
rm "$PID_FILE"
|
||||||
|
fi
|
Loading…
x
Reference in New Issue
Block a user