mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:51:13 -05:00
Move to Phase 4-5
This commit is contained in:
124
docs/PHASE_4_5_OPTIMIZATION.md
Normal file
124
docs/PHASE_4_5_OPTIMIZATION.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# Phase 4-5: localStorage Validation & React Optimizations
|
||||||
|
|
||||||
|
## Phase 4: localStorage Validation ✅ COMPLETE
|
||||||
|
|
||||||
|
### Implementation Summary
|
||||||
|
Created a comprehensive localStorage wrapper (`src/lib/localStorage.ts`) that provides:
|
||||||
|
|
||||||
|
#### Core Features
|
||||||
|
- **Error Handling**: All operations wrapped in try-catch with proper logging
|
||||||
|
- **Availability Detection**: Checks for private browsing mode / unavailable storage
|
||||||
|
- **Quota Management**: Handles `QuotaExceededError` gracefully
|
||||||
|
- **Type Safety**: Generic JSON methods with TypeScript support
|
||||||
|
- **Corruption Recovery**: Automatically removes corrupted JSON data
|
||||||
|
|
||||||
|
#### API Methods
|
||||||
|
```typescript
|
||||||
|
// Basic operations
|
||||||
|
getItem(key: string): string | null
|
||||||
|
setItem(key: string, value: string): boolean
|
||||||
|
removeItem(key: string): boolean
|
||||||
|
clear(): boolean
|
||||||
|
|
||||||
|
// JSON operations (with automatic parse/stringify)
|
||||||
|
getJSON<T>(key: string, defaultValue: T): T
|
||||||
|
setJSON<T>(key: string, value: T): boolean
|
||||||
|
|
||||||
|
// Batch operations
|
||||||
|
getItems(keys: string[]): Record<string, string | null>
|
||||||
|
setItems(items: Record<string, string>): boolean
|
||||||
|
removeItems(keys: string[]): boolean
|
||||||
|
|
||||||
|
// Utility
|
||||||
|
isLocalStorageAvailable(): boolean
|
||||||
|
hasItem(key: string): boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
### Files Migrated (8 files)
|
||||||
|
1. ✅ `src/components/theme/ThemeProvider.tsx`
|
||||||
|
2. ✅ `src/components/moderation/ReportsQueue.tsx` (already had try-catch)
|
||||||
|
3. ✅ `src/hooks/moderation/useModerationFilters.ts`
|
||||||
|
4. ✅ `src/hooks/moderation/usePagination.ts`
|
||||||
|
5. ✅ `src/hooks/useLocationAutoDetect.ts`
|
||||||
|
6. ✅ `src/hooks/useSearch.tsx`
|
||||||
|
7. ✅ `src/hooks/useUnitPreferences.ts`
|
||||||
|
8. ⚠️ `src/lib/authStorage.ts` (special case - custom implementation needed for auth)
|
||||||
|
|
||||||
|
### Benefits
|
||||||
|
- **Eliminated console.error**: All localStorage errors now use logger
|
||||||
|
- **Consistent Error Handling**: Same behavior across entire app
|
||||||
|
- **Type Safety**: Generic JSON methods prevent type errors
|
||||||
|
- **Better UX**: Graceful degradation when localStorage unavailable
|
||||||
|
- **Simplified Code**: Reduced boilerplate from ~10-15 lines to 1-2 lines per operation
|
||||||
|
|
||||||
|
### Before/After Examples
|
||||||
|
|
||||||
|
#### Before (Verbose, Inconsistent)
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('key');
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(saved);
|
||||||
|
return parsed;
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('Failed to parse', parseError);
|
||||||
|
localStorage.removeItem('key');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to access localStorage', error);
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### After (Clean, Consistent)
|
||||||
|
```typescript
|
||||||
|
return storage.getJSON('key', defaultValue);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: React Optimizations
|
||||||
|
|
||||||
|
### Status: READY TO BEGIN
|
||||||
|
|
||||||
|
### Optimization Categories
|
||||||
|
|
||||||
|
#### 1. Component Memoization
|
||||||
|
- Identify pure components that re-render unnecessarily
|
||||||
|
- Add `React.memo` where appropriate
|
||||||
|
- Focus on list items and frequently re-rendered components
|
||||||
|
|
||||||
|
#### 2. Callback Optimization
|
||||||
|
- Review `useCallback` usage for event handlers passed to children
|
||||||
|
- Ensure dependencies are correct
|
||||||
|
- Focus on components with frequent re-renders
|
||||||
|
|
||||||
|
#### 3. Expensive Computation Memoization
|
||||||
|
- Identify expensive computations in render
|
||||||
|
- Add `useMemo` for filtering, sorting, transforming data
|
||||||
|
- Focus on list views and data processing
|
||||||
|
|
||||||
|
#### 4. Component Splitting
|
||||||
|
- Review large components (>300 lines)
|
||||||
|
- Split into smaller, focused components
|
||||||
|
- Reduce prop drilling
|
||||||
|
|
||||||
|
#### 5. Lazy Loading
|
||||||
|
- Implement code splitting for routes
|
||||||
|
- Lazy load heavy components (editors, galleries)
|
||||||
|
- Improve initial load time
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
1. Search for large components (>300 lines)
|
||||||
|
2. Identify missing React.memo in list components
|
||||||
|
3. Review expensive computations without useMemo
|
||||||
|
4. Implement lazy loading for heavy components
|
||||||
|
5. Measure performance improvements
|
||||||
|
|
||||||
|
### Estimated Impact
|
||||||
|
- **Initial Load**: 10-20% improvement with lazy loading
|
||||||
|
- **Re-renders**: 30-50% reduction with proper memoization
|
||||||
|
- **Scroll Performance**: Significant improvement in lists
|
||||||
|
- **Memory Usage**: Reduced with proper cleanup
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { createContext, useContext, useEffect, useState } from "react"
|
import React, { createContext, useContext, useEffect, useState } from "react"
|
||||||
|
import * as storage from "@/lib/localStorage"
|
||||||
|
|
||||||
type Theme = "dark" | "light" | "system"
|
type Theme = "dark" | "light" | "system"
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ export function ThemeProvider({
|
|||||||
...props
|
...props
|
||||||
}: ThemeProviderProps) {
|
}: ThemeProviderProps) {
|
||||||
const [theme, setTheme] = useState<Theme>(
|
const [theme, setTheme] = useState<Theme>(
|
||||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
() => (storage.getItem(storageKey) as Theme) || defaultTheme
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -51,7 +52,7 @@ export function ThemeProvider({
|
|||||||
const value = {
|
const value = {
|
||||||
theme,
|
theme,
|
||||||
setTheme: (theme: Theme) => {
|
setTheme: (theme: Theme) => {
|
||||||
localStorage.setItem(storageKey, theme)
|
storage.setItem(storageKey, theme)
|
||||||
setTheme(theme)
|
setTheme(theme)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useDebounce } from '@/hooks/useDebounce';
|
|||||||
import { logger } from '@/lib/logger';
|
import { logger } from '@/lib/logger';
|
||||||
import { MODERATION_CONSTANTS } from '@/lib/moderation/constants';
|
import { MODERATION_CONSTANTS } from '@/lib/moderation/constants';
|
||||||
import type { EntityFilter, StatusFilter, QueueTab, SortConfig, SortField } from '@/types/moderation';
|
import type { EntityFilter, StatusFilter, QueueTab, SortConfig, SortField } from '@/types/moderation';
|
||||||
|
import * as storage from '@/lib/localStorage';
|
||||||
|
|
||||||
export interface ModerationFiltersConfig {
|
export interface ModerationFiltersConfig {
|
||||||
/** Initial entity filter */
|
/** Initial entity filter */
|
||||||
@@ -184,29 +185,18 @@ export function useModerationFilters(
|
|||||||
// Persist filters to localStorage
|
// Persist filters to localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (persist) {
|
if (persist) {
|
||||||
try {
|
storage.setJSON(storageKey, {
|
||||||
localStorage.setItem(
|
entityFilter,
|
||||||
storageKey,
|
statusFilter,
|
||||||
JSON.stringify({
|
activeTab,
|
||||||
entityFilter,
|
});
|
||||||
statusFilter,
|
|
||||||
activeTab,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error('Failed to persist filters:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [entityFilter, statusFilter, activeTab, persist, storageKey]);
|
}, [entityFilter, statusFilter, activeTab, persist, storageKey]);
|
||||||
|
|
||||||
// Persist sort to localStorage
|
// Persist sort to localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (persist) {
|
if (persist) {
|
||||||
try {
|
storage.setJSON(`${storageKey}_sort`, sortConfig);
|
||||||
localStorage.setItem(`${storageKey}_sort`, JSON.stringify(sortConfig));
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error('Failed to persist sort:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [sortConfig, persist, storageKey]);
|
}, [sortConfig, persist, storageKey]);
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { useState, useCallback, useEffect, useMemo } from 'react';
|
import { useState, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { MODERATION_CONSTANTS } from '@/lib/moderation/constants';
|
import { MODERATION_CONSTANTS } from '@/lib/moderation/constants';
|
||||||
|
import * as storage from '@/lib/localStorage';
|
||||||
|
|
||||||
export interface PaginationConfig {
|
export interface PaginationConfig {
|
||||||
/** Initial page number (1-indexed) */
|
/** Initial page number (1-indexed) */
|
||||||
@@ -149,17 +150,10 @@ export function usePagination(config: PaginationConfig = {}): PaginationState {
|
|||||||
// Persist state
|
// Persist state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (persist) {
|
if (persist) {
|
||||||
try {
|
storage.setJSON(storageKey, {
|
||||||
localStorage.setItem(
|
currentPage,
|
||||||
storageKey,
|
pageSize,
|
||||||
JSON.stringify({
|
});
|
||||||
currentPage,
|
|
||||||
pageSize,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error('Failed to persist pagination state:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [currentPage, pageSize, persist, storageKey]);
|
}, [currentPage, pageSize, persist, storageKey]);
|
||||||
|
|
||||||
|
|||||||
@@ -2,20 +2,7 @@ import { useEffect } from 'react';
|
|||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { useUnitPreferences } from '@/hooks/useUnitPreferences';
|
import { useUnitPreferences } from '@/hooks/useUnitPreferences';
|
||||||
import { logger } from '@/lib/logger';
|
import { logger } from '@/lib/logger';
|
||||||
|
import * as storage from '@/lib/localStorage';
|
||||||
function isLocalStorageAvailable(): boolean {
|
|
||||||
try {
|
|
||||||
if (typeof window === 'undefined' || typeof localStorage === 'undefined') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const testKey = '__localStorage_test__';
|
|
||||||
localStorage.setItem(testKey, 'test');
|
|
||||||
localStorage.removeItem(testKey);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useLocationAutoDetect() {
|
export function useLocationAutoDetect() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -26,25 +13,21 @@ export function useLocationAutoDetect() {
|
|||||||
if (loading) return;
|
if (loading) return;
|
||||||
|
|
||||||
// Check if localStorage is available
|
// Check if localStorage is available
|
||||||
if (!isLocalStorageAvailable()) {
|
if (!storage.isLocalStorageAvailable()) {
|
||||||
logger.warn('localStorage is not available, skipping location auto-detection');
|
logger.warn('localStorage is not available, skipping location auto-detection');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we've already attempted detection
|
// Check if we've already attempted detection
|
||||||
const hasAttemptedDetection = localStorage.getItem('location_detection_attempted');
|
const hasAttemptedDetection = storage.getItem('location_detection_attempted');
|
||||||
|
|
||||||
// Auto-detect if we haven't attempted it yet and auto_detect is enabled
|
// Auto-detect if we haven't attempted it yet and auto_detect is enabled
|
||||||
if (preferences.auto_detect && !hasAttemptedDetection) {
|
if (preferences.auto_detect && !hasAttemptedDetection) {
|
||||||
autoDetectPreferences().then(() => {
|
autoDetectPreferences().then(() => {
|
||||||
if (isLocalStorageAvailable()) {
|
storage.setItem('location_detection_attempted', 'true');
|
||||||
localStorage.setItem('location_detection_attempted', 'true');
|
|
||||||
}
|
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error('❌ Failed to auto-detect location:', error);
|
logger.error('Failed to auto-detect location', { error });
|
||||||
if (isLocalStorageAvailable()) {
|
storage.setItem('location_detection_attempted', 'true');
|
||||||
localStorage.setItem('location_detection_attempted', 'true');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [user, loading, preferences.auto_detect]);
|
}, [user, loading, preferences.auto_detect]);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useMemo, useCallback } from 'react';
|
|||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
import { Park, Ride, Company } from '@/types/database';
|
import { Park, Ride, Company } from '@/types/database';
|
||||||
import { logger } from '@/lib/logger';
|
import { logger } from '@/lib/logger';
|
||||||
|
import * as storage from '@/lib/localStorage';
|
||||||
|
|
||||||
export interface SearchResult {
|
export interface SearchResult {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -59,27 +60,9 @@ export function useSearch(options: UseSearchOptions = {}) {
|
|||||||
|
|
||||||
// Load recent searches from localStorage
|
// Load recent searches from localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
const searches = storage.getJSON<string[]>('thrillwiki_recent_searches', []);
|
||||||
const stored = localStorage.getItem('thrillwiki_recent_searches');
|
if (Array.isArray(searches)) {
|
||||||
if (stored) {
|
setRecentSearches(searches);
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(stored);
|
|
||||||
if (Array.isArray(parsed)) {
|
|
||||||
setRecentSearches(parsed);
|
|
||||||
} else {
|
|
||||||
// Invalid format, clear it
|
|
||||||
logger.warn('Recent searches data is not an array, clearing');
|
|
||||||
localStorage.removeItem('thrillwiki_recent_searches');
|
|
||||||
}
|
|
||||||
} catch (parseError: unknown) {
|
|
||||||
// JSON parse failed, data is corrupted
|
|
||||||
console.error('Failed to parse recent searches from localStorage:', parseError);
|
|
||||||
localStorage.removeItem('thrillwiki_recent_searches');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
// localStorage access failed
|
|
||||||
console.error('Error accessing localStorage:', error);
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -206,13 +189,13 @@ export function useSearch(options: UseSearchOptions = {}) {
|
|||||||
|
|
||||||
const updated = [searchQuery, ...recentSearches.filter(s => s !== searchQuery)].slice(0, 5);
|
const updated = [searchQuery, ...recentSearches.filter(s => s !== searchQuery)].slice(0, 5);
|
||||||
setRecentSearches(updated);
|
setRecentSearches(updated);
|
||||||
localStorage.setItem('thrillwiki_recent_searches', JSON.stringify(updated));
|
storage.setJSON('thrillwiki_recent_searches', updated);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clear recent searches
|
// Clear recent searches
|
||||||
const clearRecentSearches = () => {
|
const clearRecentSearches = () => {
|
||||||
setRecentSearches([]);
|
setRecentSearches([]);
|
||||||
localStorage.removeItem('thrillwiki_recent_searches');
|
storage.removeItem('thrillwiki_recent_searches');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get suggestions (recent searches when no query)
|
// Get suggestions (recent searches when no query)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { supabase } from '@/integrations/supabase/client';
|
|||||||
import { logger } from '@/lib/logger';
|
import { logger } from '@/lib/logger';
|
||||||
import { UnitPreferences, getMeasurementSystemFromCountry } from '@/lib/units';
|
import { UnitPreferences, getMeasurementSystemFromCountry } from '@/lib/units';
|
||||||
import type { Json } from '@/integrations/supabase/types';
|
import type { Json } from '@/integrations/supabase/types';
|
||||||
|
import * as storage from '@/lib/localStorage';
|
||||||
|
|
||||||
// Type guard for unit preferences
|
// Type guard for unit preferences
|
||||||
function isValidUnitPreferences(obj: unknown): obj is UnitPreferences {
|
function isValidUnitPreferences(obj: unknown): obj is UnitPreferences {
|
||||||
@@ -147,7 +148,7 @@ export function useUnitPreferences() {
|
|||||||
action: 'update_unit_preferences'
|
action: 'update_unit_preferences'
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem('unit_preferences', JSON.stringify(updated));
|
storage.setJSON('unit_preferences', updated);
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('Error saving unit preferences', {
|
logger.error('Error saving unit preferences', {
|
||||||
|
|||||||
176
src/lib/localStorage.ts
Normal file
176
src/lib/localStorage.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
/**
|
||||||
|
* Safe localStorage wrapper with proper error handling and validation
|
||||||
|
*
|
||||||
|
* Handles:
|
||||||
|
* - Private browsing mode (localStorage unavailable)
|
||||||
|
* - Storage quota exceeded
|
||||||
|
* - JSON parse errors
|
||||||
|
* - Cross-origin restrictions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logger } from './logger';
|
||||||
|
|
||||||
|
export class LocalStorageError extends Error {
|
||||||
|
constructor(message: string, public readonly cause?: Error) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'LocalStorageError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if localStorage is available
|
||||||
|
*/
|
||||||
|
export function isLocalStorageAvailable(): boolean {
|
||||||
|
try {
|
||||||
|
const testKey = '__localStorage_test__';
|
||||||
|
localStorage.setItem(testKey, 'test');
|
||||||
|
localStorage.removeItem(testKey);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely get an item from localStorage
|
||||||
|
*/
|
||||||
|
export function getItem(key: string): string | null {
|
||||||
|
try {
|
||||||
|
if (!isLocalStorageAvailable()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return localStorage.getItem(key);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to get localStorage item: ${key}`, { error });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely set an item in localStorage
|
||||||
|
*/
|
||||||
|
export function setItem(key: string, value: string): boolean {
|
||||||
|
try {
|
||||||
|
if (!isLocalStorageAvailable()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
localStorage.setItem(key, value);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.name === 'QuotaExceededError') {
|
||||||
|
logger.warn('localStorage quota exceeded', { key });
|
||||||
|
} else {
|
||||||
|
logger.warn(`Failed to set localStorage item: ${key}`, { error });
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely remove an item from localStorage
|
||||||
|
*/
|
||||||
|
export function removeItem(key: string): boolean {
|
||||||
|
try {
|
||||||
|
if (!isLocalStorageAvailable()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to remove localStorage item: ${key}`, { error });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely clear all localStorage
|
||||||
|
*/
|
||||||
|
export function clear(): boolean {
|
||||||
|
try {
|
||||||
|
if (!isLocalStorageAvailable()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
localStorage.clear();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Failed to clear localStorage', { error });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get and parse a JSON object from localStorage
|
||||||
|
*/
|
||||||
|
export function getJSON<T>(key: string, defaultValue: T): T {
|
||||||
|
try {
|
||||||
|
const item = getItem(key);
|
||||||
|
if (!item) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = JSON.parse(item);
|
||||||
|
return parsed as T;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to parse localStorage JSON for key: ${key}`, { error });
|
||||||
|
// Remove corrupted data
|
||||||
|
removeItem(key);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stringify and set a JSON object in localStorage
|
||||||
|
*/
|
||||||
|
export function setJSON<T>(key: string, value: T): boolean {
|
||||||
|
try {
|
||||||
|
const serialized = JSON.stringify(value);
|
||||||
|
return setItem(key, serialized);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to stringify localStorage JSON for key: ${key}`, { error });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a key exists in localStorage
|
||||||
|
*/
|
||||||
|
export function hasItem(key: string): boolean {
|
||||||
|
return getItem(key) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get multiple items at once
|
||||||
|
*/
|
||||||
|
export function getItems(keys: string[]): Record<string, string | null> {
|
||||||
|
const result: Record<string, string | null> = {};
|
||||||
|
for (const key of keys) {
|
||||||
|
result[key] = getItem(key);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set multiple items at once
|
||||||
|
*/
|
||||||
|
export function setItems(items: Record<string, string>): boolean {
|
||||||
|
let allSuccessful = true;
|
||||||
|
for (const [key, value] of Object.entries(items)) {
|
||||||
|
if (!setItem(key, value)) {
|
||||||
|
allSuccessful = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allSuccessful;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove multiple items at once
|
||||||
|
*/
|
||||||
|
export function removeItems(keys: string[]): boolean {
|
||||||
|
let allSuccessful = true;
|
||||||
|
for (const key of keys) {
|
||||||
|
if (!removeItem(key)) {
|
||||||
|
allSuccessful = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allSuccessful;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user