From 42e99391aea5d33672bcb3df35074d525565499e Mon Sep 17 00:00:00 2001 From: The Joel Date: Sun, 28 Jun 2026 14:49:54 +0100 Subject: [PATCH] feat: add truncateHandle helper and apply to compact contexts --- src/components/common/CreatorCard.tsx | 13 +++++-- .../__tests__/handleDisplay.utils.test.ts | 38 ++++++++++++++++++- src/utils/handleDisplay.utils.ts | 19 +++++++++- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/components/common/CreatorCard.tsx b/src/components/common/CreatorCard.tsx index cc9edae..f5068e2 100644 --- a/src/components/common/CreatorCard.tsx +++ b/src/components/common/CreatorCard.tsx @@ -24,7 +24,10 @@ import toast from 'react-hot-toast'; import showToast from '@/utils/toast.util'; import { formatCompactNumber } from '@/utils/numberFormat.utils'; import { formatCreatorKeyPriceDisplay } from '@/utils/keyPriceDisplay.utils'; -import { formatCreatorHandle } from '@/utils/handleDisplay.utils'; +import { + formatCreatorHandle, + truncateHandle, +} from '@/utils/handleDisplay.utils'; import { normalizeCreatorDisplayName } from '@/utils/creatorDisplayName.utils'; import { getCreatorPriceChartAccessibilityCopy } from '@/utils/creatorPriceChartAccessibility.utils'; import { formatJoinDate } from '@/utils/formatJoinDate'; @@ -78,6 +81,8 @@ const CreatorCard: React.FC = ({ const displayInstructorHandle = formatCreatorHandle(creator.instructorId) || '@creator'; const displaySocialHandle = formatCreatorHandle(creator.socialHandle); + const truncatedInstructorHandle = truncateHandle(displayInstructorHandle); + const truncatedSocialHandle = truncateHandle(displaySocialHandle); const displayCreatorName = normalizeCreatorDisplayName(creator.title) || 'Unnamed creator'; const priceChartAccessibility = getCreatorPriceChartAccessibilityCopy({ @@ -300,7 +305,7 @@ const CreatorCard: React.FC = ({ creatorShareSupply={creator.creatorShareSupply} isVerified={creator.isVerified} > - {displayInstructorHandle} + {truncatedInstructorHandle}

@@ -324,7 +329,7 @@ const CreatorCard: React.FC = ({ creatorShareSupply={creator.creatorShareSupply} isVerified={creator.isVerified} > - {displaySocialHandle} + {truncatedSocialHandle} ) : ( @@ -410,7 +415,7 @@ const CreatorCard: React.FC = ({ } value={ creator.socialHandle - ? displaySocialHandle + ? truncatedSocialHandle : 'No public handle' } valueTitle={ diff --git a/src/utils/__tests__/handleDisplay.utils.test.ts b/src/utils/__tests__/handleDisplay.utils.test.ts index e93e0e1..11e7829 100644 --- a/src/utils/__tests__/handleDisplay.utils.test.ts +++ b/src/utils/__tests__/handleDisplay.utils.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { formatCreatorHandle } from '../handleDisplay.utils'; +import { formatCreatorHandle, truncateHandle } from '../handleDisplay.utils'; describe('formatCreatorHandle', () => { it('lowercases mixed-case handles and prepends @', () => { @@ -32,7 +32,9 @@ describe('formatCreatorHandle', () => { }); it('is idempotent: formatting an already-formatted handle is a no-op', () => { - expect(formatCreatorHandle(formatCreatorHandle('ARivers'))).toBe('@arivers'); + expect(formatCreatorHandle(formatCreatorHandle('ARivers'))).toBe( + '@arivers' + ); }); it('does not modify the underlying string the caller passes in', () => { @@ -44,3 +46,35 @@ describe('formatCreatorHandle', () => { expect(raw).toBe('ARivers'); }); }); + +describe('truncateHandle', () => { + it('returns short handle unchanged (under max length)', () => { + expect(truncateHandle('@short', 20)).toBe('@short'); + expect(truncateHandle('abc', 5)).toBe('abc'); + }); + + it('returns exact max handle unchanged', () => { + expect(truncateHandle('12345678901234567890', 20)).toBe( + '12345678901234567890' + ); + expect(truncateHandle('abcde', 5)).toBe('abcde'); + }); + + it('truncates one over max handle with ellipsis', () => { + expect(truncateHandle('123456789012345678901', 20)).toBe( + '12345678901234567890...' + ); + expect(truncateHandle('abcdef', 5)).toBe('abcde...'); + }); + + it('uses default max of 20 characters when maxLength is not specified', () => { + // Exactly 20 chars should not be truncated + expect(truncateHandle('12345678901234567890')).toBe( + '12345678901234567890' + ); + // 21 chars (one over max) should be truncated + expect(truncateHandle('123456789012345678901')).toBe( + '12345678901234567890...' + ); + }); +}); diff --git a/src/utils/handleDisplay.utils.ts b/src/utils/handleDisplay.utils.ts index 4d5ebef..7989e7a 100644 --- a/src/utils/handleDisplay.utils.ts +++ b/src/utils/handleDisplay.utils.ts @@ -19,8 +19,25 @@ export const formatCreatorHandle = (raw: string | null | undefined): string => { if (raw == null) return ''; const trimmed = raw.trim(); if (trimmed === '') return ''; - const withoutLeadingAt = trimmed.startsWith('@') ? trimmed.slice(1) : trimmed; + const withoutLeadingAt = trimmed.startsWith('@') + ? trimmed.slice(1) + : trimmed; const normalised = withoutLeadingAt.trim().toLowerCase(); if (normalised === '') return ''; return `@${normalised}`; }; + +/** + * Truncates a creator handle to a maximum length, appending an ellipsis + * only when truncation occurs. + */ +export const truncateHandle = ( + handle: string, + maxLength: number = 20 +): string => { + if (!handle) return ''; + if (handle.length <= maxLength) { + return handle; + } + return `${handle.slice(0, maxLength)}...`; +};