From c25700d7cd72d6b7b5b3693a570fa11aea66d290 Mon Sep 17 00:00:00 2001 From: Brian Kirkpatrick Date: Wed, 4 Mar 2026 18:55:48 -0800 Subject: [PATCH] added some error handling --- TASKS.md | 45 +++++++++++----------- components/app-root.js | 2 + components/error-toast.js | 79 +++++++++++++++++++++++++++++++++++++++ store/index.js | 57 ++++++++++++++++++++++++---- 4 files changed, 154 insertions(+), 29 deletions(-) create mode 100644 components/error-toast.js diff --git a/TASKS.md b/TASKS.md index edd6904..3ca75af 100644 --- a/TASKS.md +++ b/TASKS.md @@ -2,34 +2,35 @@ ## 1. Critical Issues -- [ ] **1.1 Fix missing `del` import in store/index.js** - - Line 3 imports `get, set` from idb-keyval but `del` is used on line 13 - - Add `del` to the import statement - - This will cause a ReferenceError when removeItem is called +- [x] **1.1 Fix missing `del` import in store/index.js** ✅ COMPLETE + - Line 3 now imports `get, set, del` from idb-keyval + - Used in removeItem function on line 13 + - No more ReferenceError -- [ ] **1.2 Fix hydration flag initialization** - - The `onRehydrateStorage` callback uses `store.setState()` before `store` is fully initialized - - Creates a circular reference issue preventing app from loading - - **Options:** - - A) Initialize `_hydrated: true` by default (simplest) - - B) Use Zustand's persist middleware built-in hydration detection - - C) Move hydration callback outside store creation +- [x] **1.2 Fix hydration flag initialization** ✅ COMPLETE + - Implemented Option A: Initialize `_hydrated: true` by default + - Removed problematic `onRehydrateStorage` callback + - App now loads immediately without hanging on "loading..." + - Persistence happens in background automatically -- [ ] **1.3 Consolidate duplicate storage implementation** - - Storage is defined in both `store/index.js` AND `store/middleware/persistence.js` - - The middleware file exports `idbStorage` but it's not being used - - **Decision needed:** Use one or the other, not both +- [x] **1.3 Consolidate duplicate storage implementation** ✅ COMPLETE + - Single storage implementation in `store/index.js` + - Uses idb-keyval directly with createJSONStorage + - Clean, simple approach without extra middleware files ## 2. Architectural Improvements -- [ ] **2.1 Add error handling** - - Add try/catch blocks around IndexedDB operations - - Implement fallback if IndexedDB fails or is unavailable - - Add user feedback for errors (toast notifications?) +- [x] **2.1 Add error handling** ✅ COMPLETE + - Added try/catch blocks around all IndexedDB operations (getItem, setItem, removeItem) + - Implemented fallback: returns null on getItem error, silently fails on setItem/removeItem + - Added error state to store with auto-clear after 3 seconds + - Added validation in addItem and removeItem actions + - Created error-toast component with slide-in animation + - Toast shows error messages and allows manual dismissal -- [ ] **2.2 Add partialize option to persist middleware** - - Currently saves ALL state including `_hydrated` flag and `route` - - Should use `partialize: (state) => ({ user: state.user, items: state.items })` +- [x] **2.2 Add partialize option to persist middleware** ✅ COMPLETE + - Added partialize to exclude `_hydrated` flag from persistence + - Only persists: user, items, and route - Prevents unnecessary data in IndexedDB - [ ] **2.3 Add basic CSS reset to index.html** diff --git a/components/app-root.js b/components/app-root.js index 2f3147f..29454ec 100644 --- a/components/app-root.js +++ b/components/app-root.js @@ -4,6 +4,7 @@ import { StoreController } from "../controllers/store-controller.js"; import "./nav-bar.js"; import "./page-home.js"; import "./page-items.js"; +import "./error-toast.js"; class AppRoot extends LitElement { #hydrated = new StoreController(this, store, s => s._hydrated); @@ -31,6 +32,7 @@ class AppRoot extends LitElement { return html`
${this.#renderRoute()}
+ `; } diff --git a/components/error-toast.js b/components/error-toast.js new file mode 100644 index 0000000..0e90eda --- /dev/null +++ b/components/error-toast.js @@ -0,0 +1,79 @@ +// components/error-toast.js +import { LitElement, html, css } from 'lit' +import { store } from '../store/index.js' +import { StoreController } from '../controllers/store-controller.js' + +class ErrorToast extends LitElement { + #error = new StoreController(this, store, s => s.error) + + static styles = css` + :host { + position: fixed; + top: 1rem; + right: 1rem; + z-index: 1000; + } + + .toast { + background: #ef4444; + color: white; + padding: 1rem 1.5rem; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + display: flex; + align-items: center; + gap: 1rem; + min-width: 300px; + animation: slideIn 0.3s ease-out; + } + + @keyframes slideIn { + from { + transform: translateX(400px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + + .message { + flex: 1; + font-size: 0.9rem; + } + + .close { + background: none; + border: none; + color: white; + cursor: pointer; + font-size: 1.2rem; + padding: 0; + opacity: 0.8; + } + + .close:hover { + opacity: 1; + } + ` + + render() { + if (!this.#error.value) { + return html`` + } + + return html` +
+ ${this.#error.value} + +
+ ` + } +} + +customElements.define('error-toast', ErrorToast) diff --git a/store/index.js b/store/index.js index 43b1d46..feaea96 100644 --- a/store/index.js +++ b/store/index.js @@ -2,17 +2,32 @@ import { createStore } from 'zustand/vanilla' import { persist, createJSONStorage } from 'zustand/middleware' import { get, set, del } from 'idb-keyval' -// Create IndexedDB storage adapter +// Create IndexedDB storage adapter with error handling const storage = createJSONStorage(() => ({ getItem: async (name) => { - const value = await get(name) - return value ?? null + try { + const value = await get(name) + return value ?? null + } catch (error) { + console.error('IndexedDB getItem error:', error) + return null // Fallback to null if IndexedDB fails + } }, setItem: async (name, value) => { - await set(name, value) + try { + await set(name, value) + } catch (error) { + console.error('IndexedDB setItem error:', error) + // Silently fail - app continues to work without persistence + } }, removeItem: async (name) => { - await del(name) + try { + await del(name) + } catch (error) { + console.error('IndexedDB removeItem error:', error) + // Silently fail + } }, })) @@ -23,12 +38,40 @@ export const store = createStore( user: null, items: [], route: 'home', + error: null, // For error notifications // Actions setUser: (user) => set({ user }), - addItem: (item) => set(s => ({ items: [...s.items, item] })), - removeItem: (id) => set(s => ({ items: s.items.filter(i => i.id !== id) })), + + addItem: (item) => { + try { + if (!item || !item.name || !item.name.trim()) { + throw new Error('Item name is required') + } + set(s => ({ items: [...s.items, item], error: null })) + } catch (error) { + console.error('addItem error:', error) + set({ error: error.message }) + setTimeout(() => set({ error: null }), 3000) // Clear after 3s + } + }, + + removeItem: (id) => { + try { + if (!id) { + throw new Error('Item ID is required') + } + set(s => ({ items: s.items.filter(i => i.id !== id), error: null })) + } catch (error) { + console.error('removeItem error:', error) + set({ error: error.message }) + setTimeout(() => set({ error: null }), 3000) + } + }, + navigate: (route) => set({ route }), + + clearError: () => set({ error: null }), }), { name: 'app-store',