Skip to content

Commit fc59d36

Browse files
authored
Merge pull request xch-dev#583 from dkackman/labeled-items
2 parents 6ad5118 + 1bd193e commit fc59d36

File tree

14 files changed

+460
-414
lines changed

14 files changed

+460
-414
lines changed

crates/sage-api/src/records/pending_transaction.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ use crate::Amount;
77
pub struct PendingTransactionRecord {
88
pub transaction_id: String,
99
pub fee: Amount,
10-
pub submitted_at: Option<String>,
10+
pub submitted_at: Option<u64>,
1111
}

crates/sage-database/src/tables/mempool_items.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub struct MempoolItem {
1111
pub hash: Bytes32,
1212
pub aggregated_signature: Signature,
1313
pub fee: u64,
14-
pub submitted_timestamp: i64,
14+
pub submitted_timestamp: Option<u64>,
1515
}
1616

1717
impl Database {
@@ -188,7 +188,7 @@ async fn mempool_items_to_submit(
188188
hash: row.hash.convert()?,
189189
aggregated_signature: row.aggregated_signature.convert()?,
190190
fee: row.fee.convert()?,
191-
submitted_timestamp: row.submitted_timestamp.unwrap_or(0),
191+
submitted_timestamp: row.submitted_timestamp.map(|ts| ts as u64),
192192
})
193193
})
194194
.collect()
@@ -328,7 +328,7 @@ async fn mempool_items(conn: impl SqliteExecutor<'_>) -> Result<Vec<MempoolItem>
328328
hash: row.hash.convert()?,
329329
aggregated_signature: row.aggregated_signature.convert()?,
330330
fee: row.fee.convert()?,
331-
submitted_timestamp: row.submitted_timestamp.unwrap_or(0),
331+
submitted_timestamp: row.submitted_timestamp.map(|ts| ts as u64),
332332
})
333333
})
334334
.collect()

crates/sage/src/endpoints/data.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ impl Sage {
494494
Result::Ok(PendingTransactionRecord {
495495
transaction_id: hex::encode(tx.hash),
496496
fee: Amount::u64(tx.fee),
497-
submitted_at: Some(tx.submitted_timestamp.to_string()),
497+
submitted_at: tx.submitted_timestamp,
498498
})
499499
})
500500
.collect::<Result<Vec<_>>>()?;

src/bindings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ export type OptionAsset = { asset_id: string | null; amount: Amount }
523523
export type OptionRecord = { launcher_id: string; name: string | null; visible: boolean; coin_id: string; address: string; amount: Amount; underlying_asset: Asset; underlying_amount: Amount; underlying_coin_id: string; strike_asset: Asset; strike_amount: Amount; expiration_seconds: number; created_height: number | null; created_timestamp: number | null }
524524
export type OptionSortMode = "name" | "created_height" | "expiration_seconds"
525525
export type PeerRecord = { ip_addr: string; port: number; peak_height: number; user_managed: boolean }
526-
export type PendingTransactionRecord = { transaction_id: string; fee: Amount; submitted_at: string | null }
526+
export type PendingTransactionRecord = { transaction_id: string; fee: Amount; submitted_at: number | null }
527527
export type PerformDatabaseMaintenance = { force_vacuum: boolean }
528528
export type PerformDatabaseMaintenanceResponse = { vacuum_duration_ms: number; analyze_duration_ms: number; wal_checkpoint_duration_ms: number; total_duration_ms: number; pages_vacuumed: number; wal_pages_checkpointed: number }
529529
export type RedownloadNft = { nft_id: string }

src/components/AddressItem.tsx

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,48 @@
11
import { CopyBox } from '@/components/CopyBox';
22
import { t } from '@lingui/core/macro';
3+
import { useId } from 'react';
34
import { toast } from 'react-toastify';
45

56
export interface AddressItemProps {
6-
label: string | null;
7+
label: string;
78
address: string;
9+
className?: string;
810
}
911

10-
export function AddressItem({ label, address }: AddressItemProps) {
12+
export function AddressItem({
13+
label,
14+
address,
15+
className = '',
16+
}: AddressItemProps) {
17+
const labelId = useId();
18+
const contentId = useId();
19+
20+
// Don't render if address is empty
21+
if (!address || address.trim() === '') {
22+
return null;
23+
}
24+
1125
return (
12-
<div>
13-
<h6 className='text-sm font-semibold text-muted-foreground mb-2'>
26+
<div
27+
className={className}
28+
role='region'
29+
aria-labelledby={labelId}
30+
aria-label={t`${label} address section`}
31+
>
32+
<label
33+
id={labelId}
34+
htmlFor={contentId}
35+
className='text-sm font-medium text-muted-foreground block mb-1'
36+
>
1437
{label}
15-
</h6>
38+
</label>
1639
<CopyBox
17-
title={label ?? ''}
40+
id={contentId}
41+
title={t`Copy ${label}: ${address}`}
1842
value={address}
1943
onCopy={() => toast.success(t`${label} copied to clipboard`)}
44+
aria-label={t`${label}: ${address} (click to copy)`}
45+
aria-describedby={labelId}
2046
/>
2147
</div>
2248
);

src/components/CopyBox.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,27 @@ interface CopyBoxProps {
88
truncate?: boolean;
99
inputRef?: React.RefObject<HTMLInputElement>;
1010
onCopy?: () => void;
11+
id?: string;
12+
'aria-label'?: string;
13+
'aria-describedby'?: string;
1114
}
1215

1316
export function CopyBox(props: CopyBoxProps) {
1417
const truncate = props.truncate ?? true;
18+
const inputId =
19+
props.id || `copy-box-input-${Math.random().toString(36).substring(2, 9)}`;
20+
1521
return (
1622
<div className={`flex rounded-md shadow-sm max-w-x ${props.className}`}>
1723
<input
24+
id={inputId}
1825
ref={props.inputRef}
1926
title={props.title}
2027
type='text'
2128
value={props.displayValue ?? props.value}
2229
readOnly
30+
aria-label={props['aria-label'] || props.title}
31+
aria-describedby={props['aria-describedby']}
2332
className={`block w-full text-sm rounded-none rounded-l-md border-0 py-1.5 px-2 ${
2433
truncate ? 'truncate' : ''
2534
} text-muted-foreground bg-white text-neutral-950 dark:bg-neutral-900 dark:text-neutral-50 font-mono tracking-tight ring-1 ring-inset ring-neutral-200 dark:ring-neutral-800 sm:leading-6`}
@@ -28,6 +37,7 @@ export function CopyBox(props: CopyBoxProps) {
2837
value={props.value}
2938
onCopy={props.onCopy}
3039
className='relative rounded-none -ml-px inline-flex items-center gap-x-1.5 rounded-r-md px-3 py-2 text-sm font-semibold ring-1 ring-inset ring-neutral-200 dark:ring-neutral-800 hover:bg-gray-50 bg-white text-neutral-950 dark:bg-neutral-900 dark:text-neutral-50 '
40+
aria-label={`Copy ${props.value}`}
3141
/>
3242
</div>
3343
);

src/components/CopyButton.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ interface CopyButtonProps {
77
value: string;
88
className?: string;
99
onCopy?: () => void;
10+
'aria-label'?: string;
1011
}
1112

12-
export function CopyButton({ value, className, onCopy }: CopyButtonProps) {
13+
export function CopyButton({
14+
value,
15+
className,
16+
onCopy,
17+
'aria-label': ariaLabel,
18+
}: CopyButtonProps) {
1319
const [copied, setCopied] = useState(false);
1420

1521
const copyAddress = () => {
@@ -26,11 +32,19 @@ export function CopyButton({ value, className, onCopy }: CopyButtonProps) {
2632
variant='ghost'
2733
onClick={copyAddress}
2834
className={className}
35+
aria-label={ariaLabel || (copied ? 'Copied!' : `Copy ${value}`)}
36+
title={copied ? 'Copied!' : `Copy ${value}`}
2937
>
3038
{copied ? (
31-
<CopyCheckIcon className='h-5 w-5 text-emerald-500' />
39+
<CopyCheckIcon
40+
className='h-5 w-5 text-emerald-500'
41+
aria-hidden='true'
42+
/>
3243
) : (
33-
<CopyIcon className='h-5 w-5 text-muted-foreground' />
44+
<CopyIcon
45+
className='h-5 w-5 text-muted-foreground'
46+
aria-hidden='true'
47+
/>
3448
)}
3549
</Button>
3650
);

src/components/LabeledItem.tsx

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { isValidUrl } from '@/lib/utils';
2+
import { t } from '@lingui/core/macro';
3+
import { openUrl } from '@tauri-apps/plugin-opener';
4+
import { useId } from 'react';
5+
6+
export interface LabeledItemProps {
7+
label: string;
8+
className?: string;
9+
content: string | null | undefined;
10+
onClick?: () => void;
11+
children?: React.ReactNode;
12+
}
13+
14+
export function LabeledItem({
15+
label,
16+
className = '',
17+
content,
18+
onClick,
19+
children,
20+
}: LabeledItemProps) {
21+
const labelId = useId();
22+
const contentId = useId();
23+
24+
// Don't render if both content and children are null or empty
25+
if ((!content || content.trim() === '') && !children) {
26+
return null;
27+
}
28+
29+
const handleKeyDown = (event: React.KeyboardEvent, action: () => void) => {
30+
if (event.key === 'Enter' || event.key === ' ') {
31+
event.preventDefault();
32+
action();
33+
}
34+
};
35+
36+
return (
37+
<div
38+
role='region'
39+
aria-labelledby={labelId}
40+
aria-label={t`${label} section`}
41+
>
42+
<label
43+
id={labelId}
44+
htmlFor={contentId}
45+
className='text-sm font-medium text-muted-foreground block mb-1'
46+
>
47+
{label}
48+
</label>
49+
50+
{content && isValidUrl(content) ? (
51+
<button
52+
type='button'
53+
id={contentId}
54+
onClick={() => openUrl(content)}
55+
onKeyDown={(e) => handleKeyDown(e, () => openUrl(content))}
56+
className='text-sm break-all text-blue-700 dark:text-blue-300 cursor-pointer hover:underline focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-sm text-left w-full p-0 border-0 bg-transparent'
57+
title={t`Open external link: ${content}`}
58+
aria-label={t`${label}: ${content} (opens in external application)`}
59+
aria-describedby={labelId}
60+
>
61+
{content}
62+
</button>
63+
) : onClick ? (
64+
<button
65+
type='button'
66+
id={contentId}
67+
onClick={onClick}
68+
onKeyDown={(e) => handleKeyDown(e, onClick)}
69+
className='text-sm break-all text-blue-700 dark:text-blue-300 cursor-pointer hover:underline focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-sm text-left w-full p-0 border-0 bg-transparent'
70+
title={t`Navigate to: ${content}`}
71+
aria-label={t`${label}: ${content} (navigate within app)`}
72+
aria-describedby={labelId}
73+
>
74+
{content}
75+
</button>
76+
) : (
77+
<div
78+
id={contentId}
79+
className={`text-sm break-all ${className}`}
80+
aria-describedby={labelId}
81+
role='text'
82+
>
83+
{content}
84+
</div>
85+
)}
86+
87+
{children}
88+
</div>
89+
);
90+
}

0 commit comments

Comments
 (0)