aboutsummaryrefslogtreecommitdiff
path: root/components/persistence.py
diff options
context:
space:
mode:
authorTyler <tyler@tylerhoang.xyz>2026-05-17 00:48:26 -0700
committerTyler <tyler@tylerhoang.xyz>2026-05-17 00:48:26 -0700
commite885be599095037f93209d42ba55c77c2fb6b6ee (patch)
tree5773174f4b4e11ad3cc03ec6fda4cac47638ffeb /components/persistence.py
parent2ea1abbe88b2b4acb1c58a74022fcec42d02a4c2 (diff)
Add session persistence and watchlist exit button
Persists watchlist and active ticker across browser refreshes via a localStorage bridge (components/persistence.py), and adds a sidebar "← Watchlist" button to clear the active ticker and return to the QuoteTable landing page. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'components/persistence.py')
-rw-r--r--components/persistence.py54
1 files changed, 54 insertions, 0 deletions
diff --git a/components/persistence.py b/components/persistence.py
new file mode 100644
index 0000000..1dafdbe
--- /dev/null
+++ b/components/persistence.py
@@ -0,0 +1,54 @@
+import json
+import streamlit as st
+import streamlit.components.v1 as components
+from utils.security import escape_html
+
+
+def render_persistence_bridge() -> None:
+ """Sync watchlist + ticker to/from localStorage."""
+ restored_wl = st.session_state.get("_persist_wl", "")
+ restored_tk = st.session_state.get("_persist_tk", "")
+ if restored_wl and not st.session_state.get("_persist_loaded"):
+ try:
+ st.session_state["watchlist"] = json.loads(restored_wl)
+ except Exception:
+ pass
+ if restored_tk:
+ st.session_state["ticker"] = restored_tk
+ st.session_state["_persist_loaded"] = True
+ st.rerun()
+
+ st.text_input("persist_wl", key="_persist_wl", label_visibility="collapsed")
+ st.text_input("persist_tk", key="_persist_tk", label_visibility="collapsed")
+
+ wl_json = escape_html(json.dumps(st.session_state.get("watchlist", [])))
+ tk_val = escape_html(st.session_state.get("ticker") or "")
+
+ html = (
+ "<div id='d' data-wl='" + wl_json + "' data-tk='" + tk_val + "'></div>"
+ "<script>"
+ "(function() {"
+ " var d = document.getElementById('d');"
+ " localStorage.setItem('prism_watchlist', d.dataset.wl);"
+ " if (d.dataset.tk) localStorage.setItem('prism_ticker', d.dataset.tk);"
+ " else localStorage.removeItem('prism_ticker');"
+ " var stored_wl = localStorage.getItem('prism_watchlist');"
+ " var stored_tk = localStorage.getItem('prism_ticker') || '';"
+ " if (!stored_wl) return;"
+ " function setInput(label, val) {"
+ " var inputs = window.parent.document.querySelectorAll('input[type=text]');"
+ " for (var i = 0; i < inputs.length; i++) {"
+ " if (inputs[i].getAttribute('aria-label') === label) {"
+ " var setter = Object.getOwnPropertyDescriptor(window.parent.HTMLInputElement.prototype, 'value').set;"
+ " setter.call(inputs[i], val);"
+ " inputs[i].dispatchEvent(new window.parent.Event('input', { bubbles: true }));"
+ " return;"
+ " }"
+ " }"
+ " }"
+ " setInput('persist_wl', stored_wl);"
+ " if (stored_tk) setInput('persist_tk', stored_tk);"
+ "})();"
+ "</script>"
+ )
+ components.html(html, height=0, scrolling=False)