TESTING / app.py
HansTran's picture
Upload 21 files
0d872e9 verified
Raw
History Blame Contribute Delete
7.79 kB
import streamlit as st
from components.buttons import render_top_buttons
from components.card import render_card
from components.header import render_header
from services.api import delete_card as api_delete_card
from services.api import load_cards, load_tabs, save_card, save_tab, update_card
from utils.time_utils import build_card_payload, normalize_cards, sort_cards_by_upcoming_time
st.set_page_config(
page_title="Time Card Generator",
page_icon="⏱️",
layout="wide",
initial_sidebar_state="collapsed",
)
def load_css() -> None:
with open("assets/style.css", "r", encoding="utf-8") as css_file:
st.markdown(f"<style>{css_file.read()}</style>", unsafe_allow_html=True)
def initialize_state() -> None:
if "cards" not in st.session_state:
st.session_state.cards = []
if "tabs" not in st.session_state:
st.session_state.tabs = []
if "cards_loaded" not in st.session_state:
st.session_state.cards_loaded = False
if "status_message" not in st.session_state:
st.session_state.status_message = None
if "status_type" not in st.session_state:
st.session_state.status_type = "info"
if "current_tab_id" not in st.session_state:
st.session_state.current_tab_id = ""
if "new_tab_counter" not in st.session_state:
st.session_state.new_tab_counter = 1
if "new_tab_name" not in st.session_state:
st.session_state.new_tab_name = ""
def set_status(message: str, status_type: str = "info") -> None:
st.session_state.status_message = message
st.session_state.status_type = status_type
def clear_status() -> None:
st.session_state.status_message = None
st.session_state.status_type = "info"
def fetch_cards_once() -> None:
if st.session_state.cards_loaded:
return
try:
st.session_state.tabs = load_tabs()
if not st.session_state.tabs:
default_tab = save_tab({"title": "Tab 1"})
st.session_state.tabs = [default_tab]
st.session_state.new_tab_counter = 2
else:
st.session_state.new_tab_counter = len(st.session_state.tabs) + 1
if not st.session_state.current_tab_id:
st.session_state.current_tab_id = st.session_state.tabs[0]["id"]
st.session_state.cards = normalize_cards(load_cards())
assign_missing_tab_ids()
st.session_state.cards_loaded = True
clear_status()
except Exception as exc: # noqa: BLE001
st.session_state.cards = []
st.session_state.tabs = []
st.session_state.cards_loaded = True
set_status(f"Unable to load saved cards: {exc}", "error")
def reload_cards() -> None:
try:
st.session_state.tabs = load_tabs()
if not st.session_state.tabs:
default_tab = save_tab({"title": "Tab 1"})
st.session_state.tabs = [default_tab]
tab_ids = {tab["id"] for tab in st.session_state.tabs}
if st.session_state.current_tab_id not in tab_ids:
st.session_state.current_tab_id = st.session_state.tabs[0]["id"]
st.session_state.new_tab_counter = len(st.session_state.tabs) + 1
st.session_state.cards = normalize_cards(load_cards())
assign_missing_tab_ids()
st.session_state.cards_loaded = True
clear_status()
set_status("Cards refreshed.", "success")
except Exception as exc: # noqa: BLE001
set_status(f"Refresh failed: {exc}", "error")
def add_card() -> None:
card = build_card_payload()
card["tabId"] = st.session_state.current_tab_id
st.session_state.cards.append(card)
clear_status()
def add_tab() -> None:
try:
typed_title = st.session_state.get("new_tab_name", "").strip()
title = typed_title or f"Tab {st.session_state.new_tab_counter}"
new_tab = save_tab({"title": title})
st.session_state.tabs.append(new_tab)
st.session_state.current_tab_id = new_tab["id"]
st.session_state.new_tab_counter += 1
st.session_state.new_tab_name = ""
clear_status()
set_status("Tab created.", "success")
except Exception as exc: # noqa: BLE001
set_status(f"Unable to create tab: {exc}", "error")
def assign_missing_tab_ids() -> None:
if not st.session_state.tabs:
return
fallback_tab_id = st.session_state.tabs[0]["id"]
for card in st.session_state.cards:
if not card.get("tabId"):
card["tabId"] = fallback_tab_id
def delete_local_card(index: int) -> None:
st.session_state.cards.pop(index)
clear_status()
def handle_delete(index: int) -> None:
card = st.session_state.cards[index]
card_id = card.get("id")
if not card_id:
delete_local_card(index)
return
try:
api_delete_card(card_id)
delete_local_card(index)
set_status("Card deleted.", "success")
except Exception as exc: # noqa: BLE001
set_status(f"Delete failed: {exc}", "error")
def handle_save(index: int) -> None:
card = st.session_state.cards[index]
if not card["title"].strip():
set_status("Title is required before saving.", "warning")
return
try:
if card.get("id"):
updated_card = update_card(card)
st.session_state.cards[index] = updated_card
set_status("Card updated.", "success")
else:
saved_card = save_card(card)
st.session_state.cards[index] = saved_card
set_status("Card saved.", "success")
except Exception as exc: # noqa: BLE001
set_status(f"Save failed: {exc}", "error")
load_css()
initialize_state()
fetch_cards_once()
render_top_buttons(add_card, add_tab, reload_cards)
render_header()
if st.session_state.status_message:
getattr(st, st.session_state.status_type)(st.session_state.status_message)
if st.session_state.tabs:
tab_title_by_id = {tab["id"]: tab["title"] for tab in st.session_state.tabs}
tab_ids = [tab["id"] for tab in st.session_state.tabs]
current_tab_id = (
st.session_state.current_tab_id
if st.session_state.current_tab_id in tab_ids
else tab_ids[0]
)
selected_tab_id = st.radio(
"Tabs",
options=tab_ids,
format_func=lambda tab_id: tab_title_by_id.get(tab_id, "Untitled Tab"),
horizontal=True,
key="selected_tab_id",
index=tab_ids.index(current_tab_id),
label_visibility="collapsed",
)
st.session_state.current_tab_id = selected_tab_id
visible_cards = [
card for card in st.session_state.cards
if card.get("tabId", "") == st.session_state.current_tab_id
]
visible_cards = sort_cards_by_upcoming_time(visible_cards)
visible_card_ids = [card.get("id") or card.get("clientKey") for card in visible_cards]
display_indices = []
for visible_id in visible_card_ids:
for index, card in enumerate(st.session_state.cards):
candidate_id = card.get("id") or card.get("clientKey")
if card.get("tabId", "") == st.session_state.current_tab_id and candidate_id == visible_id:
display_indices.append(index)
break
if not display_indices:
st.markdown(
"""
<div class="empty-state">
<h3>No cards in this tab</h3>
<p>Add a card or switch to another tab.</p>
</div>
""",
unsafe_allow_html=True,
)
card_columns = st.columns(3, gap="medium")
for position, index in enumerate(display_indices):
with card_columns[position % 3]:
rank_tone = "danger" if position < 2 else "warning" if position < 4 else "default"
render_card(
index=index,
on_save=handle_save,
on_delete=handle_delete,
rank_tone=rank_tone,
)