This commit is contained in:
PhilReact 2025-05-08 15:46:41 +03:00
parent effcbb6628
commit 39ca19b079
15 changed files with 572 additions and 77 deletions

20
connect-qapp.sh Executable file
View 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

View File

@ -1,6 +1,7 @@
import { Routes } from "./Routes";
import { GlobalProvider } from "qapp-core";
import { publicSalt } from "./qapp-config.ts";
import { Routes } from './Routes';
import { GlobalProvider } from 'qapp-core';
import { publicSalt } from './qapp-config.ts';
import { PendingTxsProvider } from './state/contexts/PendingTxsProvider.tsx';
export const AppWrapper = () => {
return (
@ -14,10 +15,12 @@ export const AppWrapper = () => {
authenticateOnMount: true,
},
publicSalt: publicSalt,
appName: 'names'
appName: 'names',
}}
>
<PendingTxsProvider>
<Routes />
</PendingTxsProvider>
</GlobalProvider>
);
};

View File

@ -5,10 +5,6 @@ import {
DialogActions,
DialogContent,
DialogTitle,
List,
ListItem,
ListItemIcon,
ListItemText,
styled,
TextField,
Typography,
@ -23,10 +19,11 @@ import {
useGlobal,
} from 'qapp-core';
import { useEffect, useState } from 'react';
import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked';
import { BarSpinner } from '../common/Spinners/BarSpinner/BarSpinner';
import CheckIcon from '@mui/icons-material/Check';
import ErrorIcon from '@mui/icons-material/Error';
import { useSetAtom } from 'jotai';
import { namesAtom, pendingTxsAtom } from '../state/global/names';
export enum Availability {
NULL = 'null',
LOADING = 'loading',
@ -45,21 +42,48 @@ const Label = styled('label')`
const RegisterName = () => {
const [isOpen, setIsOpen] = useState(false);
const balance = useGlobal().auth.balance;
const setNames = useSetAtom(namesAtom);
const address = useGlobal().auth.address;
const [nameValue, setNameValue] = useState('');
const [isNameAvailable, setIsNameAvailable] = useState<Availability>(
Availability.NULL
);
const setPendingTxs = useSetAtom(pendingTxsAtom);
const [isLoadingRegisterName, setIsLoadingRegisterName] = useState(false);
const theme = useTheme();
const [nameFee, setNameFee] = useState(null);
const registerNameFunc = async () => {
if (!address) return;
const loadId = showLoading('Registering name...please wait');
try {
setIsLoadingRegisterName(true);
await qortalRequest({
const res = await qortalRequest({
action: 'REGISTER_NAME',
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');
setNameValue('');
setIsOpen(false);

View File

@ -8,12 +8,23 @@ import {
Paper,
Button,
} from '@mui/material';
import { useAtom } from 'jotai';
import { useAtom, useSetAtom } from 'jotai';
import { forwardRef, useMemo } from 'react';
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 ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import {
dismissToast,
showError,
showLoading,
showSuccess,
useGlobal,
} from 'qapp-core';
interface NameData {
name: string;
@ -78,21 +89,53 @@ function fixedHeaderContent(
);
}
function rowContent(_index: number, row: NameData) {
const handleUpdate = () => {
console.log('Update:', row.name);
// Your logic here
};
function rowContent(
_index: number,
row: NameData,
setPendingTxs,
setNames,
setNamesForSale,
address
) {
const handleBuy = async (name: string) => {
const loadId = showLoading('Attempting to purchase name...please wait');
try {
console.log('hello');
await qortalRequest({
const res = await qortalRequest({
action: 'BUY_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) {
showError(error?.message || 'Unable to purchase name');
console.log('error', error);
} finally {
dismissToast(loadId);
}
};
@ -119,6 +162,11 @@ export const ForSaleTable = ({
sortBy,
handleSort,
}) => {
const address = useGlobal().auth.address;
const setNames = useSetAtom(namesAtom);
const setNamesForSale = useSetAtom(forSaleAtom);
const setPendingTxs = useSetAtom(pendingTxsAtom);
return (
<Paper
sx={{
@ -132,7 +180,16 @@ export const ForSaleTable = ({
fixedHeaderContent={() =>
fixedHeaderContent(sortBy, sortDirection, handleSort)
}
itemContent={rowContent}
itemContent={(index, row) =>
rowContent(
index,
row,
setPendingTxs,
setNames,
setNamesForSale,
address
)
}
/>
</Paper>
);

View File

@ -20,10 +20,17 @@ import {
CircularProgress,
Avatar,
} from '@mui/material';
import { useAtom } from 'jotai';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
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 { useModal } from '../../hooks/useModal';
import {
@ -40,6 +47,7 @@ import { Availability } from '../RegisterName';
import CheckIcon from '@mui/icons-material/Check';
import ErrorIcon from '@mui/icons-material/Error';
import { BarSpinner } from '../../common/Spinners/BarSpinner/BarSpinner';
import { usePendingTxs } from '../../hooks/useHandlePendingTxs';
interface NameData {
name: string;
isSelling?: boolean;
@ -80,10 +88,17 @@ function fixedHeaderContent() {
}
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) => {
try {
const res = getHasAvatar(name);
if (res !== null) {
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 response = await getNameQueue.enqueue(() =>
@ -97,9 +112,10 @@ const ManageAvatar = ({ name, modalFunctionsAvatar }) => {
const responseData = await response.json();
if (responseData?.length > 0) {
setHasAvatar(true);
setHasAvatarState(true);
setHasAvatar(name, true);
} else {
setHasAvatar(false);
setHasAvatarState(false);
}
} catch (error) {
console.log(error);
@ -108,17 +124,19 @@ const ManageAvatar = ({ name, modalFunctionsAvatar }) => {
useEffect(() => {
if (!name) return;
checkIfAvatarExists(name);
}, [name, checkIfAvatarExists]);
}, [name, checkIfAvatarExists, refresh]);
return (
<Button
variant="outlined"
size="small"
disabled={hasAvatar === null}
onClick={() => modalFunctionsAvatar.show({ name, hasAvatar })}
disabled={hasAvatarState === null}
onClick={() =>
modalFunctionsAvatar.show({ name, hasAvatar: hasAvatarState })
}
>
{hasAvatar === null ? (
{hasAvatarState === null ? (
<CircularProgress size={10} />
) : hasAvatar ? (
) : hasAvatarState ? (
'Change avatar'
) : (
'Set avatar'
@ -134,51 +152,133 @@ function rowContent(
modalFunctions?: any,
modalFunctionsUpdateName?: any,
modalFunctionsAvatar?: any,
modalFunctionsSellName?: any
modalFunctionsSellName?: any,
setPendingTxs?: any,
setNames?: any,
setNamesForSale?: any
) {
const handleUpdate = async (name: string) => {
const loadId = showLoading('Updating name...please wait');
try {
const response = await modalFunctionsUpdateName.show();
console.log('Update:', row.name);
console.log('hello', response);
await qortalRequest({
const res = await qortalRequest({
action: 'UPDATE_NAME',
newName: response,
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) {
showError(error?.message || 'Unable to update name');
console.log('error', error);
} finally {
dismissToast(loadId);
}
// Your logic here
};
const handleSell = async (name: string) => {
const loadId = showLoading('Placing name for sale...please wait');
try {
if (name === primaryName) {
await modalFunctions.show({ name });
}
const price = await modalFunctionsSellName.show(name);
console.log('hello');
await qortalRequest({
const res = await qortalRequest({
action: 'SELL_NAME',
nameForSale: name,
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) {
showError(error?.message || 'Unable to place name for sale');
console.log('error', error);
} finally {
dismissToast(loadId);
}
};
const handleCancel = async (name: string) => {
const loadId = showLoading('Removing name from market...please wait');
try {
console.log('hello', name);
await qortalRequest({
const res = await qortalRequest({
action: 'CANCEL_SELL_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) {
showError(error?.message || 'Unable to remove name from market');
console.log('error', error);
} finally {
dismissToast(loadId);
}
};
@ -250,14 +350,19 @@ function rowContent(
}
export const NameTable = ({ names, primaryName }) => {
// const [names, setNames] = useAtom(namesAtom);
const [namesForSale] = useAtom(forSaleAtom);
const setNames = useSetAtom(namesAtom);
const [namesForSale, setNamesForSale] = useAtom(forSaleAtom);
const modalFunctions = useModal();
const modalFunctionsUpdateName = useModal();
const modalFunctionsAvatar = 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 namesForSaleString = namesForSale.map((item) => item.name);
return names.map((name) => {
@ -287,7 +392,10 @@ export const NameTable = ({ names, primaryName }) => {
modalFunctions,
modalFunctionsUpdateName,
modalFunctionsAvatar,
modalFunctionsSellName
modalFunctionsSellName,
setPendingTxs,
setNames,
setNamesForSale
)
}
/>
@ -339,11 +447,12 @@ export const NameTable = ({ names, primaryName }) => {
};
const AvatarModal = ({ modalFunctionsAvatar }) => {
const { setHasAvatar } = usePendingTxs();
const forceRefresh = useSetAtom(forceRefreshAtom);
const [arbitraryFee, setArbitraryFee] = useState('');
const [pickedAvatar, setPickedAvatar] = useState<any>(null);
const [isLoadingPublish, setIsLoadingPublish] = useState(false);
const theme = useTheme();
console.log('pickedAvatar', pickedAvatar);
useEffect(() => {
const getArbitraryName = async () => {
try {
@ -369,6 +478,9 @@ const AvatarModal = ({ modalFunctionsAvatar }) => {
identifier: 'qortal_avatar',
name: modalFunctionsAvatar.data.name,
});
setHasAvatar(modalFunctionsAvatar.data.name, true);
forceRefresh();
showSuccess('Successfully published avatar');
modalFunctionsAvatar.onOk();
} catch (error) {

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

View File

@ -2,11 +2,13 @@ import { useSetAtom } from 'jotai';
import { forSaleAtom, namesAtom } from '../state/global/names';
import { useCallback, useEffect } from 'react';
import { useGlobal } from 'qapp-core';
import { usePendingTxs } from './useHandlePendingTxs';
export const useHandleNameData = () => {
const setNamesForSale = useSetAtom(forSaleAtom);
const setNames = useSetAtom(namesAtom);
const address = useGlobal().auth.address;
const { clearPendingTxs } = usePendingTxs();
const getNamesForSale = useCallback(async () => {
try {
@ -28,6 +30,11 @@ export const useHandleNameData = () => {
offset: 0,
reverse: false,
});
clearPendingTxs(
'REGISTER_NAMES',
'name',
res?.map((item) => item.name)
);
setNames(res);
} catch (error) {
console.error(error);
@ -43,8 +50,8 @@ export const useHandleNameData = () => {
useEffect(() => {
getMyNames();
const interval = setInterval(getMyNames, 120_000); // every 2 minutes
return () => clearInterval(interval);
// const interval = setInterval(getMyNames, 120_000); // every 2 minutes
// return () => clearInterval(interval);
}, [getMyNames]);
return null;

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

View File

@ -1,7 +1,7 @@
import { useEffect } from "react";
import { To, useNavigate } from "react-router-dom";
import { EnumTheme, themeAtom } from "../state/global/system";
import { useSetAtom } from "jotai";
import { useEffect } from 'react';
import { To, useNavigate } from 'react-router-dom';
import { EnumTheme, themeAtom } from '../state/global/system';
import { useSetAtom } from 'jotai';
interface CustomWindow extends Window {
_qdnTheme: string;
@ -11,39 +11,39 @@ const customWindow = window as unknown as CustomWindow;
export const useIframe = () => {
const setTheme = useSetAtom(themeAtom);
const navigate = useNavigate();
useEffect(() => {
console.log('customWindow', customWindow?._qdnTheme)
const themeColorDefault = customWindow?._qdnTheme
const themeColorDefault = customWindow?._qdnTheme;
if (themeColorDefault === 'dark') {
setTheme(EnumTheme.DARK)
setTheme(EnumTheme.DARK);
} else if (themeColorDefault === 'light') {
setTheme(EnumTheme.LIGHT)
setTheme(EnumTheme.LIGHT);
}
function handleNavigation(event: { data: { action: string; path: To; theme: 'dark' | 'light' }; }) {
if (event.data?.action === "NAVIGATE_TO_PATH" && event.data.path) {
function handleNavigation(event: {
data: { action: string; path: To; theme: 'dark' | 'light' };
}) {
if (event.data?.action === 'NAVIGATE_TO_PATH' && event.data.path) {
navigate(event.data.path); // Navigate directly to the specified path
// Send a response back to the parent window after navigation is handled
window.parent.postMessage(
{ action: "NAVIGATION_SUCCESS", path: event.data.path },
"*"
{ action: 'NAVIGATION_SUCCESS', path: event.data.path },
'*'
);
} else if (event.data?.action === "THEME_CHANGED" && event.data.theme) {
const themeColor = event.data.theme
} else if (event.data?.action === 'THEME_CHANGED' && event.data.theme) {
const themeColor = event.data.theme;
if (themeColor === 'dark') {
setTheme(EnumTheme.DARK)
setTheme(EnumTheme.DARK);
} else if (themeColor === 'light') {
setTheme(EnumTheme.LIGHT)
setTheme(EnumTheme.LIGHT);
}
}
}
window.addEventListener("message", handleNavigation);
window.addEventListener('message', handleNavigation);
return () => {
window.removeEventListener("message", handleNavigation);
window.removeEventListener('message', handleNavigation);
};
}, [navigate, setTheme]);
return { navigate };

View File

@ -65,9 +65,6 @@ export const Market = () => {
[sortBy]
);
console.log('sortDirection', sortDirection);
console.log('sortBy', sortBy);
return (
<div>
<Box

View File

@ -24,7 +24,6 @@ export const MyNames = () => {
const filteredNames = useMemo(() => {
const lowerFilter = filterValue.trim().toLowerCase();
console.log('lowerFilter', lowerFilter, names);
const filtered = !lowerFilter
? names
: names.filter((item) => item.name.toLowerCase().includes(lowerFilter));

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

View File

@ -1,4 +1,39 @@
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 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);
});

View File

@ -3,6 +3,7 @@ import { useIframe } from '../hooks/useIframeListener';
import { AppBar, Toolbar, Button, Box, useTheme } from '@mui/material';
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
import StorefrontIcon from '@mui/icons-material/Storefront';
import { PendingTxsTable } from '../components/Tables/PendingTxsTable';
const Layout = () => {
useIframe();
@ -54,6 +55,7 @@ const Layout = () => {
<Box component="main">
<main>
<PendingTxsTable />
<Outlet />
</main>
</Box>

20
stop-qapp.sh Executable file
View 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