Skip to content

Commit 21a1988

Browse files
committed
feat: persist edits in localStorage
Previously, edits existed only in memory. Reloading the page lost all changes, forcing users to export with Copy to preserve anything. This change persists visa dates and trips to the browser's localStorage on every mutation, restoring them on load in preference to trips.js. The Reset button clears the saved copy and returns to the file. All data remains in the browser; no account or server is involved.
1 parent 088280a commit 21a1988

2 files changed

Lines changed: 46 additions & 2 deletions

File tree

index.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,8 +509,9 @@ <h1>UK ILR absence tracker</h1>
509509
<div class="card grow">
510510
<h2>Trips</h2>
511511
<p class="note">
512-
Edit a date and everything recalculates. Edits are not saved
513-
&mdash; use Copy to keep them.
512+
Edit a date and everything recalculates. Edits are saved in this
513+
browser and restored when you come back. Copy exports them as
514+
trips.js; Reset discards them.
514515
</p>
515516
<div class="scrolly"><table id="triptable"></table></div>
516517
<p class="badnote" id="tripwarn"></p>

src/app.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,43 @@ interface Domain {
140140
}));
141141
}
142142

143+
/**
144+
* Edits persist in localStorage so a revisit finds them immediately, with
145+
* no sign-in and nothing leaving the browser. The saved copy wins over
146+
* trips.js on load; Reset discards it and returns to the file.
147+
*/
148+
const DATA_KEY = "ilr.data.v1";
149+
150+
function saveData(): void {
151+
const payload = JSON.stringify({ visaValidFrom, firstArrivalOn, trips });
152+
try { localStorage.setItem(DATA_KEY, payload); } catch { /* private mode */ }
153+
}
154+
155+
function clearSavedData(): void {
156+
try { localStorage.removeItem(DATA_KEY); } catch { /* private mode */ }
157+
}
158+
159+
/** True when saved data existed and was loaded. Malformed data is ignored. */
160+
function restoreData(): boolean {
161+
let raw: string | null = null;
162+
try { raw = localStorage.getItem(DATA_KEY); } catch { /* private mode */ }
163+
if (!raw) return false;
164+
try {
165+
const d = JSON.parse(raw);
166+
if (!d || !Array.isArray(d.trips)) return false;
167+
const str = (v: unknown) => (typeof v === "string" ? v : "");
168+
visaValidFrom = str(d.visaValidFrom);
169+
firstArrivalOn = str(d.firstArrivalOn);
170+
trips = d.trips.map((t: unknown) => {
171+
const o = (t || {}) as Record<string, unknown>;
172+
return { depart: str(o.depart), ret: str(o.ret), place: str(o.place), reason: str(o.reason) };
173+
});
174+
return true;
175+
} catch {
176+
return false;
177+
}
178+
}
179+
143180
/**
144181
* The initial stay as a pseudo-trip in front of the real ones, so overlap
145182
* and picker-bound checks can treat all occupied date ranges uniformly.
@@ -653,6 +690,7 @@ interface Domain {
653690
if (!had && overlapsRow(0)) {
654691
if (f === "visa") visaValidFrom = prev; else firstArrivalOn = prev;
655692
}
693+
saveData();
656694
render();
657695
return;
658696
}
@@ -668,13 +706,15 @@ interface Domain {
668706
} else {
669707
trip[f as keyof Trip] = t.value;
670708
}
709+
saveData();
671710
render();
672711
});
673712

674713
el("triptable").addEventListener("click", (e) => {
675714
const t = e.target as HTMLElement;
676715
if (t.dataset.del === undefined) return;
677716
trips.splice(Number(t.dataset.del), 1);
717+
saveData();
678718
render();
679719
});
680720

@@ -687,10 +727,12 @@ interface Domain {
687727
if (t.depart && t.ret) last = Math.max(last, toDay(t.ret) + 30);
688728
}
689729
trips.push({ depart: toISO(last), ret: toISO(last + 7), place: "", reason: "" });
730+
saveData();
690731
render();
691732
});
692733

693734
el("reset").addEventListener("click", () => {
735+
clearSavedData();
694736
loadFromFile();
695737
render();
696738
flash("reset to trips.js");
@@ -806,6 +848,7 @@ interface Domain {
806848
restoreSplit();
807849
initSplitter();
808850
loadFromFile();
851+
restoreData();
809852
render();
810853
watchPlotHeight();
811854
})();

0 commit comments

Comments
 (0)