diff --git a/frontend/src/utils/README.md b/frontend/src/utils/README.md index df6d2596..5888d309 100644 --- a/frontend/src/utils/README.md +++ b/frontend/src/utils/README.md @@ -1,248 +1,100 @@ -# Utility Modules +# Utility Functions -This directory contains utility functions and classes used throughout the 0xCast application. +## ArrayHelpers -## Error Handling +Generic utility functions for working with arrays. These functions are type-safe and can be used with any array of objects. -### apiErrors.ts +### Methods -Provides structured error classes and utilities for handling API and blockchain errors. +#### `deduplicate(items: T[], keyExtractor: (item: T) => string): T[]` -**Key Exports:** -- `ErrorCode` - Enum of all error types -- `ApiError` - Base error class with retry support -- `ContractError` - Smart contract specific errors with Clarity code mapping -- `ValidationError` - Input validation errors -- `isRetryableError()` - Check if an error can be retried -- `getRetryDelay()` - Get recommended retry delay -- `getUserFriendlyMessage()` - Extract user-facing error message +Removes duplicate items from an array based on a key extraction function. -**Example:** ```typescript -import { ApiError, ErrorCode } from '@/utils/apiErrors'; - -try { - await fetchData(); -} catch (error) { - const apiError = ApiError.fromError(error); - if (apiError.retryable) { - await delay(getRetryDelay(apiError)); - await fetchData(); // Retry - } -} -``` +const items = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + { id: 1, name: 'Alice' } +]; -### contractErrorHandler.ts +const unique = ArrayHelpers.deduplicate(items, (item) => String(item.id)); +``` -Utilities for handling smart contract errors with automatic parsing and logging. +#### `sortBy(items: T[], compareFn: (a: T, b: T) => number): T[]` -**Key Exports:** -- `handleContractCall()` - Wrap contract calls with error handling -- `parseContractError()` - Parse errors into ContractError objects -- `getUserFriendlyContractError()` - Get user-facing error message -- `isTransactionRejection()` - Check if user cancelled transaction -- `isInsufficientFunds()` - Check for insufficient balance -- `extractTxId()` - Extract transaction ID from error +Sorts an array using a custom comparison function. Returns a new array without mutating the original. -**Example:** ```typescript -import { handleContractCall } from '@/utils/contractErrorHandler'; - -const result = await handleContractCall( - 'market-core', - 'create-market', - async () => openContractCall({...}), - { - onSuccess: (data) => console.log('Success:', data), - onError: (error) => toast.error(error.message) - } -); - -if (result.success) { - console.log('Transaction:', result.txId); -} +const items = [{ value: 3 }, { value: 1 }, { value: 2 }]; +const sorted = ArrayHelpers.sortBy(items, (a, b) => a.value - b.value); ``` -## Contract Utilities - -### contractUtils.ts - -Helper functions for working with smart contracts. +#### `sortByDate(items: T[], dateExtractor: (item: T) => Date | string, order?: 'asc' | 'desc'): T[]` -**Key Features:** -- Contract address formatting -- Function argument encoding -- Post-condition builders +Sorts an array by date field. Default order is descending. -## Formatting Utilities +```typescript +const items = [ + { createdAt: '2024-01-01' }, + { createdAt: '2024-03-01' } +]; -### formatters.ts +const sorted = ArrayHelpers.sortByDate(items, (item) => item.createdAt, 'desc'); +``` -Functions for formatting data for display. +#### `sortByPriority(items: T[], priorityExtractor: (item: T) => number, order?: 'asc' | 'desc'): T[]` -**Key Features:** -- Number formatting (currency, percentages) -- Date/time formatting -- Address truncation -- Token amount formatting +Sorts an array by priority value. Default order is ascending. -## Validation Utilities +```typescript +const items = [ + { priority: 3 }, + { priority: 1 } +]; -### validators.ts +const sorted = ArrayHelpers.sortByPriority(items, (item) => item.priority); +``` -Input validation functions. +#### `groupBy(items: T[], keyExtractor: (item: T) => K): Record` -**Key Features:** -- Address validation -- Amount validation -- String sanitization -- Form field validation +Groups array items by a key. -### marketValidation.ts +```typescript +const items = [ + { category: 'fruit', name: 'apple' }, + { category: 'vegetable', name: 'carrot' } +]; -Comprehensive validation utilities for market-related data. +const grouped = ArrayHelpers.groupBy(items, (item) => item.category); +``` -**Key Exports:** -- `validateMarketTitle()` - Validate market title length and characters -- `validateMarketDescription()` - Validate market description -- `validateMarketDuration()` - Validate duration in blocks -- `validatePredictionAmount()` - Validate prediction amounts -- `validateMarketOutcome()` - Validate outcome enum values -- `validateMarketStatus()` - Validate status enum values -- `validateStacksAddress()` - Validate Stacks address format -- `validateMarketEndTime()` - Validate end time is in future -- `validateMarketCreation()` - Validate complete market creation data -- `validatePrediction()` - Validate complete prediction data +#### `filterUnique(items: T[], keyExtractor?: (item: T) => string): T[]` -**Constants:** -- `MIN_TITLE_LENGTH` / `MAX_TITLE_LENGTH` - Title length limits -- `MIN_DESCRIPTION_LENGTH` / `MAX_DESCRIPTION_LENGTH` - Description limits -- `MIN_PREDICTION_AMOUNT` / `MAX_PREDICTION_AMOUNT` - Amount limits -- `MIN_MARKET_DURATION_BLOCKS` / `MAX_MARKET_DURATION_BLOCKS` - Duration limits +Filters array to unique values. For primitive arrays, no key extractor is needed. -**Example:** ```typescript -import { validateMarketCreation } from '@/utils/marketValidation'; - -const data = { - title: 'Will BTC reach $100k?', - description: 'Market resolves YES if Bitcoin reaches $100,000', - durationBlocks: 144 -}; - -const result = validateMarketCreation(data); -if (!result.isValid) { - toast.error(result.error); - return; -} - -// Proceed with market creation -await createMarket(data); +const numbers = [1, 2, 2, 3]; +const unique = ArrayHelpers.filterUnique(numbers); ``` -## WebSocket Utilities - -### websocketUtils.ts - -WebSocket connection management and message handling. - -**Key Features:** -- Auto-reconnection -- Message queuing -- Connection state management -- Event subscriptions - -## Best Practices - -### Error Handling - -1. **Always use structured errors:** - ```typescript - // Good - throw new ValidationError('Invalid amount', 'amount', value); - - // Bad - throw new Error('Invalid amount'); - ``` - -2. **Parse contract errors:** - ```typescript - // Good - const error = parseContractError(rawError, 'market-core', 'predict'); - - // Bad - console.error(rawError); - ``` - -3. **Check error types before retrying:** - ```typescript - if (isRetryableError(error) && !isTransactionRejection(error)) { - // Retry logic - } - ``` - -### Type Safety - -1. **Use TypeScript types:** - ```typescript - // Good - const result: ContractCallResult = await handleContractCall(...); - - // Bad - const result: any = await handleContractCall(...); - ``` - -2. **Validate inputs:** - ```typescript - if (!isValidAddress(address)) { - throw new ValidationError('Invalid address', 'address', address); - } - ``` - -### Performance - -1. **Memoize expensive operations:** - ```typescript - const formatted = useMemo(() => formatLargeNumber(value), [value]); - ``` - -2. **Debounce user input:** - ```typescript - const debouncedSearch = useMemo( - () => debounce(handleSearch, 300), - [] - ); - ``` - -## Testing - -All utility functions should have corresponding unit tests in the `__tests__` directory. - -**Example test structure:** +#### `chunk(items: T[], size: number): T[][]` + +Splits an array into chunks of specified size. + ```typescript -describe('parseContractError', () => { - it('should parse Clarity error codes', () => { - const error = new Error('(err u101)'); - const result = parseContractError(error, 'market-core', 'predict'); - expect(result.errorCode).toBe(101); - expect(result.message).toBe('Market not found.'); - }); -}); +const items = [1, 2, 3, 4, 5]; +const chunks = ArrayHelpers.chunk(items, 2); ``` -## Contributing +#### `partition(items: T[], predicate: (item: T) => boolean): [T[], T[]]` -When adding new utilities: +Partitions an array into two arrays based on a predicate function. -1. Add comprehensive JSDoc documentation -2. Include usage examples in comments -3. Write unit tests -4. Update this README -5. Follow existing naming conventions -6. Export from index.ts if appropriate +```typescript +const numbers = [1, 2, 3, 4, 5]; +const [even, odd] = ArrayHelpers.partition(numbers, (n) => n % 2 === 0); +``` -## Related Documentation +## NotificationHelpers -- [Error Handling Guide](../../docs/error-handling.md) -- [Contract Integration](../../docs/integration-guide.md) -- [API Reference](../../docs/api-reference.md) +Utility functions specific to notification handling. Uses ArrayHelpers internally for deduplication and sorting operations. diff --git a/frontend/src/utils/__tests__/arrayHelpers.test.ts b/frontend/src/utils/__tests__/arrayHelpers.test.ts new file mode 100644 index 00000000..267ba773 --- /dev/null +++ b/frontend/src/utils/__tests__/arrayHelpers.test.ts @@ -0,0 +1,169 @@ +import { describe, it, expect } from 'vitest'; +import { ArrayHelpers } from '../arrayHelpers'; + +describe('ArrayHelpers', () => { + describe('deduplicate', () => { + it('should remove duplicate items based on key extractor', () => { + const items = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + { id: 1, name: 'Alice' }, + { id: 3, name: 'Charlie' }, + ]; + + const result = ArrayHelpers.deduplicate(items, (item) => String(item.id)); + + expect(result).toHaveLength(3); + expect(result).toEqual([ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + { id: 3, name: 'Charlie' }, + ]); + }); + + it('should handle empty arrays', () => { + const result = ArrayHelpers.deduplicate([], (item) => String(item)); + expect(result).toEqual([]); + }); + }); + + describe('sortBy', () => { + it('should sort items using custom compare function', () => { + const items = [{ value: 3 }, { value: 1 }, { value: 2 }]; + + const result = ArrayHelpers.sortBy(items, (a, b) => a.value - b.value); + + expect(result).toEqual([{ value: 1 }, { value: 2 }, { value: 3 }]); + }); + + it('should not mutate original array', () => { + const items = [{ value: 3 }, { value: 1 }]; + const original = [...items]; + + ArrayHelpers.sortBy(items, (a, b) => a.value - b.value); + + expect(items).toEqual(original); + }); + }); + + describe('sortByDate', () => { + it('should sort items by date in descending order by default', () => { + const items = [ + { date: '2024-01-01' }, + { date: '2024-03-01' }, + { date: '2024-02-01' }, + ]; + + const result = ArrayHelpers.sortByDate(items, (item) => item.date); + + expect(result[0].date).toBe('2024-03-01'); + expect(result[2].date).toBe('2024-01-01'); + }); + + it('should sort items by date in ascending order', () => { + const items = [ + { date: '2024-03-01' }, + { date: '2024-01-01' }, + ]; + + const result = ArrayHelpers.sortByDate(items, (item) => item.date, 'asc'); + + expect(result[0].date).toBe('2024-01-01'); + expect(result[1].date).toBe('2024-03-01'); + }); + }); + + describe('sortByPriority', () => { + it('should sort items by priority in ascending order by default', () => { + const items = [ + { priority: 3 }, + { priority: 1 }, + { priority: 2 }, + ]; + + const result = ArrayHelpers.sortByPriority(items, (item) => item.priority); + + expect(result).toEqual([ + { priority: 1 }, + { priority: 2 }, + { priority: 3 }, + ]); + }); + + it('should sort items by priority in descending order', () => { + const items = [ + { priority: 1 }, + { priority: 3 }, + ]; + + const result = ArrayHelpers.sortByPriority(items, (item) => item.priority, 'desc'); + + expect(result[0].priority).toBe(3); + expect(result[1].priority).toBe(1); + }); + }); + + describe('groupBy', () => { + it('should group items by key', () => { + const items = [ + { category: 'fruit', name: 'apple' }, + { category: 'vegetable', name: 'carrot' }, + { category: 'fruit', name: 'banana' }, + ]; + + const result = ArrayHelpers.groupBy(items, (item) => item.category); + + expect(result.fruit).toHaveLength(2); + expect(result.vegetable).toHaveLength(1); + expect(result.fruit[0].name).toBe('apple'); + }); + }); + + describe('filterUnique', () => { + it('should filter unique primitive values', () => { + const items = [1, 2, 2, 3, 1, 4]; + + const result = ArrayHelpers.filterUnique(items); + + expect(result).toEqual([1, 2, 3, 4]); + }); + + it('should filter unique objects using key extractor', () => { + const items = [ + { id: 1 }, + { id: 2 }, + { id: 1 }, + ]; + + const result = ArrayHelpers.filterUnique(items, (item) => String(item.id)); + + expect(result).toHaveLength(2); + }); + }); + + describe('chunk', () => { + it('should split array into chunks of specified size', () => { + const items = [1, 2, 3, 4, 5, 6, 7]; + + const result = ArrayHelpers.chunk(items, 3); + + expect(result).toEqual([[1, 2, 3], [4, 5, 6], [7]]); + }); + + it('should handle empty arrays', () => { + const result = ArrayHelpers.chunk([], 3); + expect(result).toEqual([]); + }); + }); + + describe('partition', () => { + it('should partition array based on predicate', () => { + const items = [1, 2, 3, 4, 5, 6]; + + const [even, odd] = ArrayHelpers.partition(items, (item) => item % 2 === 0); + + expect(even).toEqual([2, 4, 6]); + expect(odd).toEqual([1, 3, 5]); + }); + }); +}); diff --git a/frontend/src/utils/arrayHelpers.ts b/frontend/src/utils/arrayHelpers.ts new file mode 100644 index 00000000..a508c3ba --- /dev/null +++ b/frontend/src/utils/arrayHelpers.ts @@ -0,0 +1,114 @@ +export interface Notification { + type: string; + message: string; + createdAt: string | Date; +} + +export class ArrayHelpers { + static deduplicate( + items: T[], + keyExtractor: (item: T) => string + ): T[] { + if (items.length === 0) { + return []; + } + + const seen = new Set(); + const deduplicated: T[] = []; + + for (const item of items) { + const key = keyExtractor(item); + if (!seen.has(key)) { + seen.add(key); + deduplicated.push(item); + } + } + + return deduplicated; + } + + static sortBy( + items: T[], + compareFn: (a: T, b: T) => number + ): T[] { + return [...items].sort(compareFn); + } + + static sortByDate( + items: T[], + dateExtractor: (item: T) => Date | string, + order: 'asc' | 'desc' = 'desc' + ): T[] { + return this.sortBy(items, (a, b) => { + const dateA = new Date(dateExtractor(a)).getTime(); + const dateB = new Date(dateExtractor(b)).getTime(); + return order === 'desc' ? dateB - dateA : dateA - dateB; + }); + } + + static sortByPriority( + items: T[], + priorityExtractor: (item: T) => number, + order: 'asc' | 'desc' = 'asc' + ): T[] { + return this.sortBy(items, (a, b) => { + const priorityA = priorityExtractor(a); + const priorityB = priorityExtractor(b); + return order === 'asc' ? priorityA - priorityB : priorityB - priorityA; + }); + } + + static groupBy( + items: T[], + keyExtractor: (item: T) => K + ): Record { + return items.reduce((groups, item) => { + const key = keyExtractor(item); + if (!groups[key]) { + groups[key] = []; + } + groups[key].push(item); + return groups; + }, {} as Record); + } + + static filterUnique( + items: T[], + keyExtractor?: (item: T) => string + ): T[] { + if (!keyExtractor) { + return Array.from(new Set(items)); + } + return this.deduplicate(items, keyExtractor); + } + + static chunk(items: T[], size: number): T[][] { + if (size <= 0) { + throw new Error('Chunk size must be greater than 0'); + } + + const chunks: T[][] = []; + for (let i = 0; i < items.length; i += size) { + chunks.push(items.slice(i, i + size)); + } + return chunks; + } + + static partition( + items: T[], + predicate: (item: T) => boolean + ): [T[], T[]] { + const truthy: T[] = []; + const falsy: T[] = []; + + for (const item of items) { + if (predicate(item)) { + truthy.push(item); + } else { + falsy.push(item); + } + } + + return [truthy, falsy]; + } +} diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts new file mode 100644 index 00000000..f8afcbd9 --- /dev/null +++ b/frontend/src/utils/index.ts @@ -0,0 +1,2 @@ +export { ArrayHelpers } from './arrayHelpers'; +export { NotificationHelpers } from './notificationHelpers'; diff --git a/frontend/src/utils/notificationHelpers.ts b/frontend/src/utils/notificationHelpers.ts index 4181f62d..976197ac 100644 --- a/frontend/src/utils/notificationHelpers.ts +++ b/frontend/src/utils/notificationHelpers.ts @@ -1,4 +1,5 @@ import { NotificationType, NotificationChannel } from '@/types/notifications'; +import { ArrayHelpers } from './arrayHelpers'; export class NotificationHelpers { static getNotificationTitle(type: NotificationType, data?: Record): string { @@ -200,37 +201,29 @@ export class NotificationHelpers { } static deduplicateNotifications(notifications: any[]): any[] { - const seen = new Set(); - const deduplicated: any[] = []; - - for (const notification of notifications) { - const key = `${notification.type}_${notification.message}_${notification.createdAt}`; - if (!seen.has(key)) { - seen.add(key); - deduplicated.push(notification); - } - } - - return deduplicated; + return ArrayHelpers.deduplicate( + notifications, + (notification) => `${notification.type}_${notification.message}_${notification.createdAt}` + ); } static sortNotifications(notifications: any[], sortBy: 'date' | 'priority' = 'date'): any[] { - const sorted = [...notifications]; - if (sortBy === 'date') { - sorted.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - } else if (sortBy === 'priority') { - const priorityOrder = { high: 0, medium: 1, low: 2 }; - sorted.sort((a, b) => { - const priorityA = priorityOrder[this.getPriorityLabel(a.type)] || 2; - const priorityB = priorityOrder[this.getPriorityLabel(b.type)] || 2; - if (priorityA !== priorityB) { - return priorityA - priorityB; - } - return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); - }); + return ArrayHelpers.sortByDate( + notifications, + (notification) => notification.createdAt, + 'desc' + ); } - return sorted; + const priorityOrder = { high: 0, medium: 1, low: 2 }; + return ArrayHelpers.sortBy(notifications, (a, b) => { + const priorityA = priorityOrder[this.getPriorityLabel(a.type)] || 2; + const priorityB = priorityOrder[this.getPriorityLabel(b.type)] || 2; + if (priorityA !== priorityB) { + return priorityA - priorityB; + } + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); + }); } }