Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/src/components/Leaderboard.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState, useCallback } from 'react';
import { CONTRACT_ADDRESS, CONTRACT_NAME } from '../config/contracts';
import { formatSTX } from '../lib/utils';
import { formatSTX, formatAddress } from '../lib/utils';
import CopyButton from './ui/copy-button';

const API_BASE = 'https://api.hiro.so';
Expand Down Expand Up @@ -72,7 +72,7 @@ export default function Leaderboard() {
return b.totalReceived - a.totalReceived;
}).slice(0, 20);

const truncateAddress = (addr) => `${addr.slice(0, 8)}...${addr.slice(-6)}`;
const truncateAddress = (addr) => formatAddress(addr, 8, 6);

if (loading) {
return (
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/components/NotificationBell.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useRef, useEffect } from 'react';
import { formatSTX } from '../lib/utils';
import { formatSTX, formatAddress } from '../lib/utils';

export default function NotificationBell({ notifications, unreadCount, onMarkRead, loading }) {
const [open, setOpen] = useState(false);
Expand All @@ -22,8 +22,7 @@ export default function NotificationBell({ notifications, unreadCount, onMarkRea
}
};

const truncateAddr = (addr) =>
addr ? `${addr.slice(0, 6)}...${addr.slice(-4)}` : '';
const truncateAddr = (addr) => formatAddress(addr, 6, 4);

return (
<div className="relative" ref={dropdownRef}>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/RecentTips.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { openContractCall } from '@stacks/connect';
import { uintCV, stringUtf8CV, PostConditionMode, Pc } from '@stacks/transactions';
import { CONTRACT_ADDRESS, CONTRACT_NAME } from '../config/contracts';
import { formatSTX, toMicroSTX } from '../lib/utils';
import { formatSTX, toMicroSTX, formatAddress } from '../lib/utils';
import { network, appDetails, userSession } from '../utils/stacks';
import { useTipContext } from '../context/TipContext';
import CopyButton from './ui/copy-button';
Expand All @@ -26,7 +26,7 @@
const [sortBy, setSortBy] = useState('newest');
const [showFilters, setShowFilters] = useState(false);
const [offset, setOffset] = useState(0);
const [totalResults, setTotalResults] = useState(0);

Check failure on line 29 in frontend/src/components/RecentTips.jsx

View workflow job for this annotation

GitHub Actions / Frontend Lint

'totalResults' is assigned a value but never used. Allowed unused vars must match /^[A-Z_]/u

const fetchRecentTips = useCallback(async () => {
try {
Expand Down Expand Up @@ -102,7 +102,7 @@

const truncateAddress = (address) => {
const addrStr = typeof address === 'string' ? address : (address.value || '');
return `${addrStr.slice(0, 8)}...${addrStr.slice(-6)}`;
return formatAddress(addrStr, 8, 6);
};

const fullAddress = (address) => {
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/TipHistory.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEffect, useState, useCallback } from 'react';
import { fetchCallReadOnlyFunction, cvToJSON, principalCV } from '@stacks/transactions';
import { network } from '../utils/stacks';
import { CONTRACT_ADDRESS, CONTRACT_NAME } from '../config/contracts';
import { formatSTX } from '../lib/utils';
import { formatSTX, formatAddress } from '../lib/utils';
import { useTipContext } from '../context/TipContext';
import CopyButton from './ui/copy-button';
import ShareTip from './ShareTip';
Expand Down Expand Up @@ -123,7 +123,7 @@ export default function TipHistory({ userAddress }) {
}
};

const truncateAddr = (addr) => `${addr.slice(0, 8)}...${addr.slice(-6)}`;
const truncateAddr = (addr) => formatAddress(addr, 8, 6);

const filteredTips = tips.filter(t => {
if (tab === 'sent' && t.direction !== 'sent') return false;
Expand Down Expand Up @@ -196,7 +196,7 @@ export default function TipHistory({ userAddress }) {
{stats['tips-sent'].value}
</p>
<p className="text-sm font-medium text-gray-500">
Total Volume: <span className="text-gray-700 font-bold">{(stats['total-sent'].value / 1000000).toFixed(2)} STX</span>
Total Volume: <span className="text-gray-700 font-bold">{formatSTX(stats['total-sent'].value, 2)} STX</span>
</p>
</div>

Expand All @@ -213,7 +213,7 @@ export default function TipHistory({ userAddress }) {
{stats['tips-received'].value}
</p>
<p className="text-sm font-medium text-gray-500">
Total Earned: <span className="text-gray-700 font-bold">{(stats['total-received'].value / 1000000).toFixed(2)} STX</span>
Total Earned: <span className="text-gray-700 font-bold">{formatSTX(stats['total-received'].value, 2)} STX</span>
</p>
</div>
</div>
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,25 @@ export function formatSTX(microStx, decimals = 6) {
export function toMicroSTX(stx) {
return Math.floor(parseFloat(stx) * MICRO_STX);
}

/**
* Truncate a Stacks address for display.
* @param {string} address - Full address
* @param {number} [startChars=6] - Characters to show from start
* @param {number} [endChars=4] - Characters to show from end
* @returns {string} Truncated address
*/
export function formatAddress(address, startChars = 6, endChars = 4) {
if (!address || address.length <= startChars + endChars + 3) return address || '';
return `${address.slice(0, startChars)}...${address.slice(-endChars)}`;
}

/**
* Locale-aware number formatting.
* @param {number|string} n - Number to format
* @param {object} [options] - Intl.NumberFormat options
* @returns {string} Formatted number
*/
export function formatNumber(n, options = {}) {
return Number(n).toLocaleString(undefined, options);
}
47 changes: 46 additions & 1 deletion frontend/src/test/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { formatSTX, toMicroSTX, cn } from '../lib/utils';
import { formatSTX, toMicroSTX, cn, formatAddress, formatNumber } from '../lib/utils';

describe('formatSTX', () => {
it('converts micro-STX to STX string', () => {
Expand Down Expand Up @@ -53,9 +53,54 @@
});

it('handles conditional classes', () => {
const result = cn('base', false && 'hidden', 'visible');

Check failure on line 56 in frontend/src/test/utils.test.js

View workflow job for this annotation

GitHub Actions / Frontend Lint

Unexpected constant truthiness on the left-hand side of a `&&` expression
expect(result).toContain('base');
expect(result).toContain('visible');
expect(result).not.toContain('hidden');
});
});

describe('formatAddress', () => {
it('truncates a standard Stacks address', () => {
const addr = 'SP31PKQVQZVZCK3FM3NH67CGD6G1FMR17VQVS2W5T';
expect(formatAddress(addr)).toBe('SP31PK...2W5T');
});

it('uses custom start and end lengths', () => {
const addr = 'SP31PKQVQZVZCK3FM3NH67CGD6G1FMR17VQVS2W5T';
expect(formatAddress(addr, 8, 6)).toBe('SP31PKQV...VS2W5T');
});

it('returns short addresses unmodified', () => {
expect(formatAddress('SP123')).toBe('SP123');
});

it('handles empty or null input', () => {
expect(formatAddress('')).toBe('');
expect(formatAddress(null)).toBe('');
expect(formatAddress(undefined)).toBe('');
});
});

describe('formatNumber', () => {
it('formats a number with locale separators', () => {
const result = formatNumber(1234567);
expect(result).toContain('1');
expect(result.length).toBeGreaterThan(3);
});

it('formats with decimal options', () => {
const result = formatNumber(1234.5, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
expect(result).toContain('34');
expect(result).toContain('50');
});

it('handles zero', () => {
expect(formatNumber(0)).toBe('0');
});

it('handles string input', () => {
const result = formatNumber('999');
expect(result).toContain('999');
});
});
Loading