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
2 changes: 1 addition & 1 deletion src/components/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export type {
// Keep local components that are not yet migrated
export { AmountDisplay, AmountWithFiat, formatAmount } from './amount-display'
export { AnimatedNumber, AnimatedAmount } from './animated-number'
export { TimeDisplay, formatDate, formatDateTime, formatTime, toDate } from './time-display'
export { TimeDisplay, formatDate, formatDateTime, formatTime, getLocale, toDate } from './time-display'
export { FormField } from './form-field'
export { ErrorBoundary } from './error-boundary'
export { CopyableText } from './copyable-text'
Expand Down
19 changes: 14 additions & 5 deletions src/components/common/time-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';

type TimeFormat = 'relative' | 'date' | 'datetime' | 'time';
type TimeFormat = 'relative' | 'date' | 'datetime' | 'time' | 'monthDayTime';

interface TimeDisplayProps {
value: Date | string | number;
Expand All @@ -17,7 +17,7 @@ function toDate(value: Date | string | number): Date {
}

// 获取当前语言的 locale
function getLocale(lang: string): string {
export function getLocale(lang: string): string {
const localeMap: Record<string, string> = {
'zh-CN': 'zh-CN',
en: 'en-US',
Expand All @@ -40,7 +40,7 @@ export function TimeDisplay({ value, format = 'relative', className }: TimeDispl

switch (format) {
case 'relative':
formatted = formatRelativeTime(date, t);
formatted = formatRelativeTime(date, t, locale);
break;
case 'date':
formatted = date.toLocaleDateString(locale, {
Expand All @@ -65,6 +65,15 @@ export function TimeDisplay({ value, format = 'relative', className }: TimeDispl
minute: '2-digit',
});
break;
case 'monthDayTime':
formatted = date.toLocaleString(locale, {
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
break;
}

const titleText = date.toLocaleString(locale, {
Expand All @@ -84,7 +93,7 @@ export function TimeDisplay({ value, format = 'relative', className }: TimeDispl
}

// 相对时间格式化(支持 i18n)
function formatRelativeTime(date: Date, t: TFunction): string {
function formatRelativeTime(date: Date, t: TFunction, locale: string): string {
const now = new Date();
const diff = now.getTime() - date.getTime();
const absDiff = Math.abs(diff);
Expand All @@ -108,7 +117,7 @@ function formatRelativeTime(date: Date, t: TFunction): string {
}

// 超过一周显示日期
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
return date.toLocaleDateString(locale, { month: 'short', day: 'numeric' });
}

// 导出工具函数(用于非组件场景)
Expand Down
17 changes: 14 additions & 3 deletions src/components/transaction/transaction-item.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,25 @@ function renderWithProvider(ui: React.ReactElement) {
return render(<TestI18nProvider>{ui}</TestI18nProvider>)
}

const LOCALE = 'zh-CN'
const FIXED_TIME = new Date('2024-01-15T14:30:45')
const formatMonthDayTime = (date: Date) =>
date.toLocaleString(LOCALE, {
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})

const mockTransaction: TransactionInfo = {
id: '1',
type: 'send',
status: 'confirmed',
amount: Amount.fromFormatted('100', 6, 'USDT'),
symbol: 'USDT',
address: '0x1234567890abcdef1234567890abcdef12345678',
timestamp: new Date(),
timestamp: FIXED_TIME,
}

describe('TransactionItem', () => {
Expand Down Expand Up @@ -98,8 +109,8 @@ describe('TransactionItem', () => {
expect(screen.getByText(t('transaction:type.exchange'))).toBeInTheDocument()
})

it('formats recent timestamp', () => {
it('formats timestamp with month/day and seconds', () => {
renderWithProvider(<TransactionItem transaction={mockTransaction} />)
expect(screen.getByText(t('time.justNow'))).toBeInTheDocument()
expect(screen.getByText(formatMonthDayTime(FIXED_TIME))).toBeInTheDocument()
})
})
6 changes: 5 additions & 1 deletion src/components/transaction/transaction-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,11 @@ export function TransactionItem({
size="sm"
className={cn('@xs:text-base', typeMeta.color)}
/>
<TimeDisplay value={transaction.timestamp} className="text-muted-foreground block text-xs" />
<TimeDisplay
value={transaction.timestamp}
format="monthDayTime"
className="text-muted-foreground block text-xs"
/>
</div>
)}
</div>
Expand Down
9 changes: 6 additions & 3 deletions src/components/transaction/transaction-list.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ function renderWithProvider(ui: React.ReactElement) {
}

// Use fixed date to avoid timezone/midnight flakiness
// Set to noon on a specific date so "today" and "yesterday" are stable
// Set to noon on a specific date so local date grouping is stable
const FIXED_NOW = new Date('2025-06-15T12:00:00Z').getTime()
const LOCALE = 'zh-CN'
const formatGroupDate = (date: Date) =>
date.toLocaleDateString(LOCALE, { year: 'numeric', month: 'long', day: 'numeric' })

const mockTransactions: TransactionInfo[] = [
{
Expand Down Expand Up @@ -55,8 +58,8 @@ describe('TransactionList', () => {

it('groups transactions by date', () => {
renderWithProvider(<TransactionList transactions={mockTransactions} />)
expect(screen.getByText('今天')).toBeInTheDocument()
expect(screen.getByText('昨天')).toBeInTheDocument()
expect(screen.getByText(formatGroupDate(mockTransactions[0]!.timestamp as Date))).toBeInTheDocument()
expect(screen.getByText(formatGroupDate(mockTransactions[1]!.timestamp as Date))).toBeInTheDocument()
})

it('shows loading skeleton', () => {
Expand Down
27 changes: 12 additions & 15 deletions src/components/transaction/transaction-list.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
import { TransactionItem, type TransactionInfo } from './transaction-item';
import { EmptyState, SkeletonList } from '@biochain/key-ui';
import { getLocale } from '../common';

interface TransactionListProps {
transactions: TransactionInfo[];
Expand All @@ -15,25 +17,17 @@ interface TransactionListProps {
testId?: string | undefined;
}

function groupByDate(transactions: TransactionInfo[]): Map<string, TransactionInfo[]> {
function groupByDate(transactions: TransactionInfo[], locale: string): Map<string, TransactionInfo[]> {
const groups = new Map<string, TransactionInfo[]>();
const now = new Date();
const today = now.toDateString();
const yesterday = new Date(now.getTime() - 86400000).toDateString();

transactions.forEach((tx) => {
// timestamp 可能是 number (毫秒), string, 或 Date 对象
const date = tx.timestamp instanceof Date ? tx.timestamp : new Date(tx.timestamp);
const dateStr = date.toDateString();

let key: string;
if (dateStr === today) {
key = '今天'; // i18n-ignore: date grouping
} else if (dateStr === yesterday) {
key = '昨天'; // i18n-ignore: date grouping
} else {
key = date.toLocaleDateString('zh-CN', { month: 'long', day: 'numeric' });
}
const key = date.toLocaleDateString(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
});

if (!groups.has(key)) {
groups.set(key, []);
Expand All @@ -55,6 +49,9 @@ export function TransactionList({
showChainIcon = false,
testId,
}: TransactionListProps) {
const { i18n } = useTranslation();
const locale = getLocale(i18n.language);

if (loading) {
return <SkeletonList count={5} {...(className && { className })} />;
}
Expand All @@ -80,7 +77,7 @@ export function TransactionList({
);
}

const grouped = groupByDate(transactions);
const grouped = groupByDate(transactions, locale);

return (
<div {...(testId && { 'data-testid': testId })} className={cn('space-y-4', className)}>
Expand Down