diff --git a/src/app/store/messagingStore.ts b/src/app/store/messagingStore.ts index 3cc2e47a..8b970e7f 100644 --- a/src/app/store/messagingStore.ts +++ b/src/app/store/messagingStore.ts @@ -1,3 +1,4 @@ +import { DEFAULT_SOCKET_URL } from '@/constants/app.constants'; import { create } from 'zustand'; import io, { Socket } from 'socket.io-client'; @@ -614,7 +615,7 @@ export const useMessagingStore = create((set, get) => ({ get().loadConversations(); try { - const socket = io(process.env.NEXT_PUBLIC_WEBSOCKET_URL || 'http://localhost:3001', { + const socket = io(process.env.NEXT_PUBLIC_WEBSOCKET_URL || DEFAULT_SOCKET_URL, { autoConnect: false, // Don't auto-connect for demo }); diff --git a/src/components/errors/ErrorBoundarySystem.tsx b/src/components/errors/ErrorBoundarySystem.tsx index ec0e26b1..a000308a 100644 --- a/src/components/errors/ErrorBoundarySystem.tsx +++ b/src/components/errors/ErrorBoundarySystem.tsx @@ -57,19 +57,6 @@ export class ErrorBoundarySystem extends Component ({ - errorInfo, - errorCount: (prevState.errorCount ?? 0) + 1, - })); - - errorReportingService.addBreadcrumb('errorBoundary', { - isolationId: this.props.isolationId, - isolationLevel: this.props.isolationLevel, - errorMessage: error.message, - componentStack: errorInfo.componentStack, - }); - if (this.props.onError) { this.props.onError(error, errorInfo); } diff --git a/src/constants/app.constants.ts b/src/constants/app.constants.ts new file mode 100644 index 00000000..2cd48129 --- /dev/null +++ b/src/constants/app.constants.ts @@ -0,0 +1,44 @@ +/** + * Application-wide constants + * Extracting magic numbers and strings for better maintainability + */ + +// Timeouts (in milliseconds) +export const DEFAULT_TOAST_DURATION = 5000; +export const DEFAULT_IDLE_TIMEOUT_MS = 2000; +export const RECONNECT_DELAY_MS = 1000; +export const MAX_TREND_POINTS = 200; +export const MAX_RETRIES = 3; + +// API Timeouts +export const API_TIMEOUT_DEFAULT = 10000; +export const API_TIMEOUT_UPLOAD = 60000; +export const API_TIMEOUT_DOWNLOAD = 60000; +export const API_TIMEOUT_SEARCH = 15000; +export const API_CACHE_TTL_DEFAULT = 300000; // 5 minutes + +// API URLs & Endpoints +export const DEFAULT_SOCKET_URL = 'http://localhost:3001'; + +// Web3 Config +export const DEFAULT_STARKNET_NETWORK = 'goerli-alpha'; +export const STARKNET_NETWORKS = { + MAINNET: { + rpcUrl: 'https://starknet-mainnet.public.blastapi.io', + explorerUrl: 'https://starkscan.co', + }, + TESTNET: { + rpcUrl: 'https://starknet-testnet.public.blastapi.io', + explorerUrl: 'https://testnet.starkscan.co', + }, + SEPOLIA: { + rpcUrl: 'https://starknet-sepolia.public.blastapi.io', + explorerUrl: 'https://sepolia.starkscan.co', + }, +} as const; + +// Storage Keys +export const STORAGE_KEYS = { + PERF_TRENDS: 'teachlink:perf:trends', + AUTH_TOKEN: 'token', +}; diff --git a/src/context/ToastContext.tsx b/src/context/ToastContext.tsx index 354a62fa..1ef6d673 100644 --- a/src/context/ToastContext.tsx +++ b/src/context/ToastContext.tsx @@ -1,5 +1,6 @@ 'use client'; +import { DEFAULT_TOAST_DURATION } from '@/constants/app.constants'; import React, { createContext, useContext, useState, useCallback, useMemo, ReactNode } from 'react'; import { Toast, ToastType } from '@/components/ui/Toast'; @@ -29,7 +30,7 @@ export function ToastProvider({ children }: { children: ReactNode }) { }, []); const addToast = useCallback( - (message: string, type: ToastType = 'info', duration = 5000) => { + (message: string, type: ToastType = 'info', duration = DEFAULT_TOAST_DURATION) => { const id = Math.random().toString(36).substr(2, 9); setToasts((prev) => [...prev, { id, type, message, duration }]); diff --git a/src/lib/api.ts b/src/lib/api.ts index 8b54a5ba..57c618b6 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,12 +1,19 @@ +import { + API_TIMEOUT_DEFAULT, + MAX_RETRIES, + RECONNECT_DELAY_MS, + STORAGE_KEYS, + API_CACHE_TTL_DEFAULT, +} from '@/constants/app.constants'; import { ApiError, parseApiError } from '@/utils/error-handler'; import { ErrorType, ErrorInfo } from '@/utils/errorUtils'; export type { ErrorInfo }; -const DEFAULT_TIMEOUT_MS = 10_000; -const MAX_RETRIES = 3; -const RETRY_DELAY_MS = 1000; -const DEFAULT_TTL_MS = 300_000; // 5 minutes +const DEFAULT_TIMEOUT_MS = API_TIMEOUT_DEFAULT; +const API_MAX_RETRIES = MAX_RETRIES; +const RETRY_DELAY_MS = RECONNECT_DELAY_MS; +const DEFAULT_TTL_MS = API_CACHE_TTL_DEFAULT; /** * Cache entry structure @@ -113,7 +120,7 @@ class ApiClientImpl { this.config = { baseURL: config.baseURL || process.env.NEXT_PUBLIC_API_URL || '', timeout: config.timeout || DEFAULT_TIMEOUT_MS, - maxRetries: config.maxRetries || MAX_RETRIES, + maxRetries: config.maxRetries || API_MAX_RETRIES, retryDelay: config.retryDelay || RETRY_DELAY_MS, defaultTTL: config.defaultTTL || DEFAULT_TTL_MS, }; @@ -176,7 +183,7 @@ class ApiClientImpl { */ private getToken(): string | null { if (typeof window === 'undefined') return null; - return localStorage.getItem('token'); + return localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); } /** @@ -196,7 +203,7 @@ class ApiClientImpl { private async requestWithRetry(config: RequestConfig, attempt = 1): Promise { const token = this.getToken(); const url = this.config.baseURL ? `${this.config.baseURL}${config.url}` : config.url; - + // Include token in cache key to prevent cross-user cache leakage (security best practice) const cacheKey = `${url}:${token || 'anonymous'}`; @@ -324,7 +331,11 @@ class ApiClientImpl { /** * POST request – automatically invalidates the cache entry for this URL on success. */ - async post(url: string, body?: unknown, options?: Omit): Promise { + async post( + url: string, + body?: unknown, + options?: Omit, + ): Promise { return this.requestWithRetry({ ...options, url, @@ -336,7 +347,11 @@ class ApiClientImpl { /** * PATCH request – automatically invalidates the cache entry for this URL on success. */ - async patch(url: string, body?: unknown, options?: Omit): Promise { + async patch( + url: string, + body?: unknown, + options?: Omit, + ): Promise { return this.requestWithRetry({ ...options, url, @@ -348,7 +363,11 @@ class ApiClientImpl { /** * PUT request – automatically invalidates the cache entry for this URL on success. */ - async put(url: string, body?: unknown, options?: Omit): Promise { + async put( + url: string, + body?: unknown, + options?: Omit, + ): Promise { return this.requestWithRetry({ ...options, url, diff --git a/src/lib/apiInterceptors.ts b/src/lib/apiInterceptors.ts index 2037b6c2..01bf3a58 100644 --- a/src/lib/apiInterceptors.ts +++ b/src/lib/apiInterceptors.ts @@ -1,3 +1,9 @@ +import { + API_TIMEOUT_UPLOAD, + API_TIMEOUT_DOWNLOAD, + API_TIMEOUT_SEARCH, + STORAGE_KEYS, +} from '@/constants/app.constants'; import { apiClient, RequestInterceptor, ResponseInterceptor, ErrorInterceptor } from './api'; import { RequestConfig } from './api'; @@ -45,7 +51,7 @@ export const authRefreshInterceptor: ErrorInterceptor = async (error: Error) => if (error.message && error.message.includes('401')) { // Clear invalid token if (typeof window !== 'undefined') { - localStorage.removeItem('token'); + localStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN); // Optionally redirect to login window.location.href = '/login'; } @@ -59,9 +65,9 @@ export const authRefreshInterceptor: ErrorInterceptor = async (error: Error) => export const timeoutInterceptor: RequestInterceptor = async (config: RequestConfig) => { // You can customize timeout per endpoint const urlPatterns: Array<{ pattern: string | RegExp; timeout: number }> = [ - { pattern: /\/upload/, timeout: 60000 }, // 60s for uploads - { pattern: /\/download/, timeout: 60000 }, // 60s for downloads - { pattern: /\/search/, timeout: 15000 }, // 15s for search + { pattern: /\/upload/, timeout: API_TIMEOUT_UPLOAD }, + { pattern: /\/download/, timeout: API_TIMEOUT_DOWNLOAD }, + { pattern: /\/search/, timeout: API_TIMEOUT_SEARCH }, ]; const url = config.url; diff --git a/src/utils/performanceUtils.ts b/src/utils/performanceUtils.ts index bdfd20d4..b4bda1f8 100644 --- a/src/utils/performanceUtils.ts +++ b/src/utils/performanceUtils.ts @@ -2,6 +2,7 @@ * Performance utilities: Core Web Vitals (web-vitals), trends, suggestions, and helpers. */ +import { STORAGE_KEYS, MAX_TREND_POINTS, DEFAULT_IDLE_TIMEOUT_MS } from '@/constants/app.constants'; import { onCLS, onFCP, onINP, onLCP, onTTFB, type Metric } from 'web-vitals'; export interface PerformanceMetric { @@ -37,8 +38,7 @@ export interface OptimizationSuggestion { metric?: string; } -const TREND_STORAGE_KEY = 'teachlink:perf:trends'; -const MAX_TREND_POINTS = 200; +const TREND_STORAGE_KEY = STORAGE_KEYS.PERF_TRENDS; const vitalListeners = new Set<(metric: PerformanceMetric) => void>(); let vitalsStarted = false; @@ -312,7 +312,7 @@ export function measurePerformancePhase( } /** Run work during browser idle time when available. */ -export function runWhenIdle(callback: () => void, timeoutMs = 2000): void { +export function runWhenIdle(callback: () => void, timeoutMs = DEFAULT_IDLE_TIMEOUT_MS): void { if (typeof window === 'undefined') { callback(); return; diff --git a/src/utils/web3/envValidation.ts b/src/utils/web3/envValidation.ts index debe352e..95592edf 100644 --- a/src/utils/web3/envValidation.ts +++ b/src/utils/web3/envValidation.ts @@ -1,4 +1,6 @@ +import { DEFAULT_STARKNET_NETWORK } from '@/constants/app.constants'; import { z } from 'zod'; + /** * Web3 Environment Validation * Validates required environment variables for Starknet integration @@ -6,8 +8,8 @@ import { z } from 'zod'; const envSchema = z.object({ NEXT_PUBLIC_STARKNET_NETWORK: z - .enum(['mainnet-alpha', 'goerli-alpha', 'sepolia-alpha']) - .default('goerli-alpha'), + .enum(['mainnet-alpha', 'goerli-alpha', 'sepolia-alpha', 'testnet', 'mainnet', 'sepolia']) + .default(DEFAULT_STARKNET_NETWORK as any), NEXT_PUBLIC_STARKNET_RPC_URL: z.string().url().optional(), NEXT_PUBLIC_API_URL: z.string().url().optional(), NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), @@ -33,14 +35,26 @@ const NETWORKS = { rpcUrl: 'https://starknet-mainnet.public.blastapi.io', explorerUrl: 'https://starkscan.co', }, + mainnet: { + rpcUrl: 'https://starknet-mainnet.public.blastapi.io', + explorerUrl: 'https://starkscan.co', + }, 'goerli-alpha': { rpcUrl: 'https://starknet-testnet.public.blastapi.io', explorerUrl: 'https://testnet.starkscan.co', }, + testnet: { + rpcUrl: 'https://starknet-testnet.public.blastapi.io', + explorerUrl: 'https://testnet.starkscan.co', + }, 'sepolia-alpha': { rpcUrl: 'https://starknet-sepolia.public.blastapi.io', explorerUrl: 'https://sepolia.starkscan.co', }, + sepolia: { + rpcUrl: 'https://starknet-sepolia.public.blastapi.io', + explorerUrl: 'https://sepolia.starkscan.co', + }, } as const; type NetworkType = keyof typeof NETWORKS; @@ -84,12 +98,12 @@ export function validateWeb3Env(): EnvValidationResult { errors.push(validation.error || 'Environment validation failed'); } - const network = process.env.NEXT_PUBLIC_STARKNET_NETWORK || 'goerli-alpha'; + const network = process.env.NEXT_PUBLIC_STARKNET_NETWORK || DEFAULT_STARKNET_NETWORK; const customRpcUrl = process.env.NEXT_PUBLIC_STARKNET_RPC_URL; // Validate network if (!Object.keys(NETWORKS).includes(network)) { - warnings.push(`Unknown network "${network}", defaulting to goerli-alpha`); + warnings.push(`Unknown network "${network}", defaulting to ${DEFAULT_STARKNET_NETWORK}`); } // Check for custom RPC in production @@ -97,7 +111,8 @@ export function validateWeb3Env(): EnvValidationResult { warnings.push('Consider setting NEXT_PUBLIC_STARKNET_RPC_URL for production'); } - const networkConfig = NETWORKS[network as NetworkType] || NETWORKS['goerli-alpha']; + const networkConfig = + NETWORKS[network as NetworkType] || NETWORKS[DEFAULT_STARKNET_NETWORK as NetworkType]; return { isValid: errors.length === 0, @@ -148,5 +163,5 @@ export function validateStarknetEnv(): { valid: boolean; missing: string[] } { } export function getStarknetNetwork(): string { - return process.env.NEXT_PUBLIC_STARKNET_NETWORK ?? 'goerli-alpha'; + return process.env.NEXT_PUBLIC_STARKNET_NETWORK ?? DEFAULT_STARKNET_NETWORK; }