Skip to content

Commit 3898711

Browse files
authored
Merge pull request #130 from DenisValeev/codex/remove-title-and-text-from-app
Persist Total Recall deck state
2 parents 9f42d8e + 763fde9 commit 3898711

2 files changed

Lines changed: 151 additions & 51 deletions

File tree

apps/total-recall/app.js

Lines changed: 151 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@
2424
}
2525

2626
const STORAGE_KEY = 'total-recall-notes';
27+
const STATE_KEY = 'total-recall-state';
2728
const DEFAULT_NOTES = [
2829
"Why don't scientists trust atoms? Because they make up everything.",
2930
"I told my computer I needed a break, and it said 'No problem — I'll go to sleep.'",
3031
'Why did the scarecrow get a promotion? He was outstanding in his field.'
3132
].join('\n\n');
3233

3334
let entries = [];
34-
let deck = [];
35+
let deckOrder = [];
3536
let index = 0;
3637

3738
function setStatus(message) {
@@ -47,6 +48,22 @@
4748
}
4849
}
4950

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+
5067
function parseNotes(raw) {
5168
if (typeof raw !== 'string') {
5269
return [];
@@ -67,20 +84,94 @@
6784
return copy;
6885
}
6986

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);
74150
return;
75151
}
76152

77-
deck = shuffle(entries);
78-
index = 0;
153+
const payload = JSON.stringify({ deck: deckOrder, index });
154+
safeSetItem(STATE_KEY, payload);
79155
}
80156

81157
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();
84175
}
85176
}
86177

@@ -100,23 +191,34 @@
100191
}
101192

102193
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];
104202

105203
entryEl.textContent = current;
106204
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;
109207
prevButton.disabled = disableNav;
110208
nextButton.disabled = disableNav;
111-
shuffleButton.disabled = deck.length <= 1;
209+
shuffleButton.disabled = deckOrder.length <= 1;
112210
}
113211

114212
function showNext() {
115213
if (!entries.length) {
116214
return;
117215
}
118216
ensureDeck();
119-
index = (index + 1) % deck.length;
217+
if (!deckOrder.length) {
218+
return;
219+
}
220+
index = (index + 1) % deckOrder.length;
221+
saveState();
120222
render();
121223
}
122224

@@ -125,33 +227,56 @@
125227
return;
126228
}
127229
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();
129235
render();
130236
}
131237

132238
function reshuffleDeck() {
133239
if (entries.length <= 1) {
134240
return;
135241
}
136-
deck = shuffle(entries);
242+
deckOrder = shuffle(createSequentialDeck());
137243
index = 0;
244+
saveState();
138245
render();
139246
setStatus('Deck reshuffled.');
140247
}
141248

142-
function updateEntriesFrom(raw) {
249+
function updateEntriesFrom(raw, storedState = null) {
143250
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;
145273
render();
274+
saveState();
146275
}
147276

148277
function handleSave() {
149278
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);
155280
updateEntriesFrom(raw);
156281
const count = entries.length;
157282
if (count === 0) {
@@ -165,11 +290,7 @@
165290

166291
function handleClear() {
167292
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, '');
173294
updateEntriesFrom('');
174295
setStatus('Cleared notes. Add new entries to continue.');
175296
}
@@ -198,5 +319,6 @@
198319
const initialNotes = storedNotes !== null ? storedNotes : DEFAULT_NOTES;
199320
notesInput.value = initialNotes;
200321
setStatus('');
201-
updateEntriesFrom(initialNotes);
322+
const storedDeckState = loadState();
323+
updateEntriesFrom(initialNotes, storedDeckState);
202324
})();

apps/total-recall/index.html

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -99,24 +99,6 @@
9999
box-shadow: 0 0 0 3px rgba(74, 99, 255, 0.18);
100100
}
101101

102-
.app-header {
103-
display: flex;
104-
flex-direction: column;
105-
gap: 8px;
106-
}
107-
108-
.app-header h1 {
109-
margin: 0;
110-
font-size: clamp(1.9rem, 2.4vw + 1.2rem, 2.6rem);
111-
font-weight: 700;
112-
}
113-
114-
.app-header p {
115-
margin: 0;
116-
color: var(--text-secondary);
117-
line-height: 1.6;
118-
}
119-
120102
.visually-hidden {
121103
position: absolute;
122104
width: 1px;
@@ -365,12 +347,8 @@
365347
<span>Home</span>
366348
</a>
367349
</nav>
368-
<header class="app-header">
369-
<h1>Total Recall</h1>
370-
</header>
371350
<article class="card" aria-live="polite">
372351
<div class="card__meta">
373-
<span>Memory card</span>
374352
<span id="card-counter" data-counter>0 of 0</span>
375353
</div>
376354
<div class="card__body">

0 commit comments

Comments
 (0)