|
24 | 24 | } |
25 | 25 |
|
26 | 26 | const STORAGE_KEY = 'total-recall-notes'; |
| 27 | + const STATE_KEY = 'total-recall-state'; |
27 | 28 | const DEFAULT_NOTES = [ |
28 | 29 | "Why don't scientists trust atoms? Because they make up everything.", |
29 | 30 | "I told my computer I needed a break, and it said 'No problem — I'll go to sleep.'", |
30 | 31 | 'Why did the scarecrow get a promotion? He was outstanding in his field.' |
31 | 32 | ].join('\n\n'); |
32 | 33 |
|
33 | 34 | let entries = []; |
34 | | - let deck = []; |
| 35 | + let deckOrder = []; |
35 | 36 | let index = 0; |
36 | 37 |
|
37 | 38 | function setStatus(message) { |
|
47 | 48 | } |
48 | 49 | } |
49 | 50 |
|
| 51 | + function safeSetItem(key, value) { |
| 52 | + try { |
| 53 | + localStorage.setItem(key, value); |
| 54 | + } catch (error) { |
| 55 | + console.error('Failed to write to localStorage', error); // eslint-disable-line no-console |
| 56 | + } |
| 57 | + } |
| 58 | + |
| 59 | + function safeRemoveItem(key) { |
| 60 | + try { |
| 61 | + localStorage.removeItem(key); |
| 62 | + } catch (error) { |
| 63 | + console.error('Failed to remove item from localStorage', error); // eslint-disable-line no-console |
| 64 | + } |
| 65 | + } |
| 66 | + |
50 | 67 | function parseNotes(raw) { |
51 | 68 | if (typeof raw !== 'string') { |
52 | 69 | return []; |
|
67 | 84 | return copy; |
68 | 85 | } |
69 | 86 |
|
70 | | - function resetDeck() { |
71 | | - if (!entries.length) { |
72 | | - deck = []; |
73 | | - index = 0; |
| 87 | + function createSequentialDeck() { |
| 88 | + return entries.map((_, entryIndex) => entryIndex); |
| 89 | + } |
| 90 | + |
| 91 | + function clampIndex(value, length) { |
| 92 | + if (length <= 0) { |
| 93 | + return 0; |
| 94 | + } |
| 95 | + |
| 96 | + const number = Number(value); |
| 97 | + if (!Number.isFinite(number)) { |
| 98 | + return 0; |
| 99 | + } |
| 100 | + |
| 101 | + const integer = Math.trunc(number); |
| 102 | + if (integer < 0) { |
| 103 | + return 0; |
| 104 | + } |
| 105 | + |
| 106 | + if (integer >= length) { |
| 107 | + return length - 1; |
| 108 | + } |
| 109 | + |
| 110 | + return integer; |
| 111 | + } |
| 112 | + |
| 113 | + function sanitizeStoredDeck(order) { |
| 114 | + if (!Array.isArray(order) || order.length !== entries.length) { |
| 115 | + return null; |
| 116 | + } |
| 117 | + |
| 118 | + const seen = new Set(); |
| 119 | + const sanitized = []; |
| 120 | + |
| 121 | + for (let i = 0; i < order.length; i += 1) { |
| 122 | + const value = Number(order[i]); |
| 123 | + if (!Number.isInteger(value) || value < 0 || value >= entries.length || seen.has(value)) { |
| 124 | + return null; |
| 125 | + } |
| 126 | + seen.add(value); |
| 127 | + sanitized.push(value); |
| 128 | + } |
| 129 | + |
| 130 | + return sanitized; |
| 131 | + } |
| 132 | + |
| 133 | + function loadState() { |
| 134 | + const raw = safeGetItem(STATE_KEY); |
| 135 | + if (typeof raw !== 'string' || raw.length === 0) { |
| 136 | + return null; |
| 137 | + } |
| 138 | + |
| 139 | + try { |
| 140 | + return JSON.parse(raw); |
| 141 | + } catch (error) { |
| 142 | + console.error('Failed to parse saved deck state', error); // eslint-disable-line no-console |
| 143 | + return null; |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + function saveState() { |
| 148 | + if (!entries.length || deckOrder.length !== entries.length) { |
| 149 | + safeRemoveItem(STATE_KEY); |
74 | 150 | return; |
75 | 151 | } |
76 | 152 |
|
77 | | - deck = shuffle(entries); |
78 | | - index = 0; |
| 153 | + const payload = JSON.stringify({ deck: deckOrder, index }); |
| 154 | + safeSetItem(STATE_KEY, payload); |
79 | 155 | } |
80 | 156 |
|
81 | 157 | function ensureDeck() { |
82 | | - if (!deck.length) { |
83 | | - resetDeck(); |
| 158 | + if (!entries.length) { |
| 159 | + deckOrder = []; |
| 160 | + index = 0; |
| 161 | + return; |
| 162 | + } |
| 163 | + |
| 164 | + const expectedLength = entries.length; |
| 165 | + if (deckOrder.length !== expectedLength) { |
| 166 | + deckOrder = createSequentialDeck(); |
| 167 | + index = 0; |
| 168 | + saveState(); |
| 169 | + return; |
| 170 | + } |
| 171 | + |
| 172 | + if (index < 0 || index >= deckOrder.length) { |
| 173 | + index = clampIndex(index, deckOrder.length); |
| 174 | + saveState(); |
84 | 175 | } |
85 | 176 | } |
86 | 177 |
|
|
100 | 191 | } |
101 | 192 |
|
102 | 193 | ensureDeck(); |
103 | | - const current = deck[index]; |
| 194 | + |
| 195 | + if (!deckOrder.length) { |
| 196 | + renderEmptyState(); |
| 197 | + return; |
| 198 | + } |
| 199 | + |
| 200 | + const currentEntryIndex = deckOrder[index]; |
| 201 | + const current = entries[currentEntryIndex]; |
104 | 202 |
|
105 | 203 | entryEl.textContent = current; |
106 | 204 | entryEl.classList.remove('is-empty'); |
107 | | - counterEl.textContent = `${index + 1} of ${deck.length}`; |
108 | | - const disableNav = deck.length <= 1; |
| 205 | + counterEl.textContent = `${index + 1} of ${deckOrder.length}`; |
| 206 | + const disableNav = deckOrder.length <= 1; |
109 | 207 | prevButton.disabled = disableNav; |
110 | 208 | nextButton.disabled = disableNav; |
111 | | - shuffleButton.disabled = deck.length <= 1; |
| 209 | + shuffleButton.disabled = deckOrder.length <= 1; |
112 | 210 | } |
113 | 211 |
|
114 | 212 | function showNext() { |
115 | 213 | if (!entries.length) { |
116 | 214 | return; |
117 | 215 | } |
118 | 216 | ensureDeck(); |
119 | | - index = (index + 1) % deck.length; |
| 217 | + if (!deckOrder.length) { |
| 218 | + return; |
| 219 | + } |
| 220 | + index = (index + 1) % deckOrder.length; |
| 221 | + saveState(); |
120 | 222 | render(); |
121 | 223 | } |
122 | 224 |
|
|
125 | 227 | return; |
126 | 228 | } |
127 | 229 | ensureDeck(); |
128 | | - index = (index - 1 + deck.length) % deck.length; |
| 230 | + if (!deckOrder.length) { |
| 231 | + return; |
| 232 | + } |
| 233 | + index = (index - 1 + deckOrder.length) % deckOrder.length; |
| 234 | + saveState(); |
129 | 235 | render(); |
130 | 236 | } |
131 | 237 |
|
132 | 238 | function reshuffleDeck() { |
133 | 239 | if (entries.length <= 1) { |
134 | 240 | return; |
135 | 241 | } |
136 | | - deck = shuffle(entries); |
| 242 | + deckOrder = shuffle(createSequentialDeck()); |
137 | 243 | index = 0; |
| 244 | + saveState(); |
138 | 245 | render(); |
139 | 246 | setStatus('Deck reshuffled.'); |
140 | 247 | } |
141 | 248 |
|
142 | | - function updateEntriesFrom(raw) { |
| 249 | + function updateEntriesFrom(raw, storedState = null) { |
143 | 250 | entries = parseNotes(raw); |
144 | | - resetDeck(); |
| 251 | + |
| 252 | + if (!entries.length) { |
| 253 | + deckOrder = []; |
| 254 | + index = 0; |
| 255 | + render(); |
| 256 | + safeRemoveItem(STATE_KEY); |
| 257 | + return; |
| 258 | + } |
| 259 | + |
| 260 | + let nextDeck = createSequentialDeck(); |
| 261 | + let nextIndex = 0; |
| 262 | + |
| 263 | + if (storedState && typeof storedState === 'object') { |
| 264 | + const sanitizedDeck = sanitizeStoredDeck(storedState.deck); |
| 265 | + if (sanitizedDeck) { |
| 266 | + nextDeck = sanitizedDeck; |
| 267 | + nextIndex = clampIndex(storedState.index, nextDeck.length); |
| 268 | + } |
| 269 | + } |
| 270 | + |
| 271 | + deckOrder = nextDeck; |
| 272 | + index = nextIndex; |
145 | 273 | render(); |
| 274 | + saveState(); |
146 | 275 | } |
147 | 276 |
|
148 | 277 | function handleSave() { |
149 | 278 | const raw = notesInput.value; |
150 | | - try { |
151 | | - localStorage.setItem(STORAGE_KEY, raw); |
152 | | - } catch (error) { |
153 | | - console.error('Failed to save notes to localStorage', error); // eslint-disable-line no-console |
154 | | - } |
| 279 | + safeSetItem(STORAGE_KEY, raw); |
155 | 280 | updateEntriesFrom(raw); |
156 | 281 | const count = entries.length; |
157 | 282 | if (count === 0) { |
|
165 | 290 |
|
166 | 291 | function handleClear() { |
167 | 292 | notesInput.value = ''; |
168 | | - try { |
169 | | - localStorage.setItem(STORAGE_KEY, ''); |
170 | | - } catch (error) { |
171 | | - console.error('Failed to clear notes in localStorage', error); // eslint-disable-line no-console |
172 | | - } |
| 293 | + safeSetItem(STORAGE_KEY, ''); |
173 | 294 | updateEntriesFrom(''); |
174 | 295 | setStatus('Cleared notes. Add new entries to continue.'); |
175 | 296 | } |
|
198 | 319 | const initialNotes = storedNotes !== null ? storedNotes : DEFAULT_NOTES; |
199 | 320 | notesInput.value = initialNotes; |
200 | 321 | setStatus(''); |
201 | | - updateEntriesFrom(initialNotes); |
| 322 | + const storedDeckState = loadState(); |
| 323 | + updateEntriesFrom(initialNotes, storedDeckState); |
202 | 324 | })(); |
0 commit comments