| 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: |
| 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: |
| 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: |
| 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: |
| 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: |
| 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, |
| ) |
|
|