HansTran commited on
Commit
ad813fc
·
verified ·
1 Parent(s): c1dcb16

Upload 4 files

Browse files
Files changed (4) hide show
  1. .gitignore +3 -0
  2. README.md +85 -0
  3. app.py +131 -0
  4. requirements.txt +2 -0
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
README.md ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Time Card Generator
3
+ emoji: ⏱️
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: streamlit
7
+ sdk_version: 1.37.0
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
11
+
12
+ # Time Card Generator
13
+
14
+ A Streamlit application for creating, editing, and saving shift cards to a Google Sheet through a Google Apps Script Web App. The UI is optimized for Hugging Face Streamlit Spaces and uses only HTTP requests for persistence.
15
+
16
+ ## Features
17
+
18
+ - Add unlimited time cards
19
+ - Edit title and start time inline
20
+ - Automatically calculate end time as `+2 hours`
21
+ - Save new cards and update existing cards
22
+ - Delete cards locally or from Google Sheets
23
+ - Load saved cards automatically on startup
24
+ - Use `st.session_state` for responsive card management
25
+
26
+ ## Project Structure
27
+
28
+ ```text
29
+ streamlit_app/
30
+ ├── app.py
31
+ ├── requirements.txt
32
+ ├── README.md
33
+ ├── .gitignore
34
+ ├── assets/
35
+ │ └── style.css
36
+ ├── components/
37
+ │ ├── buttons.py
38
+ │ ├── card.py
39
+ │ └── header.py
40
+ ├── services/
41
+ │ └── api.py
42
+ ├── utils/
43
+ │ └── time_utils.py
44
+ └── .streamlit/
45
+ └── secrets.toml.example
46
+ ```
47
+
48
+ ## Configuration
49
+
50
+ 1. Deploy the Google Apps Script from the `google_apps_script/` folder.
51
+ 2. Copy the Web App URL.
52
+ 3. Create `.streamlit/secrets.toml` from the example file:
53
+
54
+ ```toml
55
+ WEB_APP_URL="https://script.google.com/macros/s/your-deployment-id/exec"
56
+ ```
57
+
58
+ This repository is already configured with the deployed URL in `.streamlit/secrets.toml`:
59
+
60
+ ```toml
61
+ WEB_APP_URL="https://script.google.com/macros/s/AKfycbxJbzf0n0KWpAiWz9lXq5yGYw7igNFmAzrZ2qgwYRWqWPodW4CRw75qEOIrQW8knRNz5Q/exec"
62
+ ```
63
+
64
+ If you create a new Apps Script deployment later, update that file or replace it with a Hugging Face Space secret.
65
+
66
+ ## Run Locally
67
+
68
+ ```bash
69
+ cd streamlit_app
70
+ python -m venv .venv
71
+ source .venv/bin/activate
72
+ pip install -r requirements.txt
73
+ streamlit run app.py
74
+ ```
75
+
76
+ ## Deploy to Hugging Face Streamlit Space
77
+
78
+ 1. Create a new Hugging Face Space.
79
+ 2. Choose `Streamlit` as the SDK.
80
+ 3. Upload the contents of `streamlit_app/`.
81
+ 4. This repo can run without extra edits because `.streamlit/secrets.toml` is included.
82
+ 5. Optionally, add a Hugging Face Space secret named `WEB_APP_URL` if you prefer managing the URL outside the repo.
83
+ 6. Commit and let the Space build automatically.
84
+
85
+ For detailed setup, see the root guide in [`../google_apps_script/README.md`](/home/huy/timemini/google_apps_script/README.md) and the Apps Script deployment section there.
app.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+
3
+ from components.buttons import render_add_card_button
4
+ from components.card import render_card
5
+ from components.header import render_header
6
+ from services.api import delete_card as api_delete_card
7
+ from services.api import load_cards, save_card, update_card
8
+ from utils.time_utils import build_card_payload, normalize_cards
9
+
10
+
11
+ st.set_page_config(
12
+ page_title="Time Card Generator",
13
+ page_icon="⏱️",
14
+ layout="wide",
15
+ initial_sidebar_state="collapsed",
16
+ )
17
+
18
+
19
+ def load_css() -> None:
20
+ with open("assets/style.css", "r", encoding="utf-8") as css_file:
21
+ st.markdown(f"<style>{css_file.read()}</style>", unsafe_allow_html=True)
22
+
23
+
24
+ def initialize_state() -> None:
25
+ if "cards" not in st.session_state:
26
+ st.session_state.cards = []
27
+ if "cards_loaded" not in st.session_state:
28
+ st.session_state.cards_loaded = False
29
+ if "status_message" not in st.session_state:
30
+ st.session_state.status_message = None
31
+ if "status_type" not in st.session_state:
32
+ st.session_state.status_type = "info"
33
+
34
+
35
+ def set_status(message: str, status_type: str = "info") -> None:
36
+ st.session_state.status_message = message
37
+ st.session_state.status_type = status_type
38
+
39
+
40
+ def clear_status() -> None:
41
+ st.session_state.status_message = None
42
+ st.session_state.status_type = "info"
43
+
44
+
45
+ def fetch_cards_once() -> None:
46
+ if st.session_state.cards_loaded:
47
+ return
48
+
49
+ try:
50
+ st.session_state.cards = normalize_cards(load_cards())
51
+ st.session_state.cards_loaded = True
52
+ clear_status()
53
+ except Exception as exc: # noqa: BLE001
54
+ st.session_state.cards = []
55
+ st.session_state.cards_loaded = True
56
+ set_status(f"Unable to load saved cards: {exc}", "error")
57
+
58
+
59
+ def add_card() -> None:
60
+ st.session_state.cards.append(build_card_payload())
61
+ clear_status()
62
+
63
+
64
+ def delete_local_card(index: int) -> None:
65
+ st.session_state.cards.pop(index)
66
+ clear_status()
67
+
68
+
69
+ def handle_delete(index: int) -> None:
70
+ card = st.session_state.cards[index]
71
+ card_id = card.get("id")
72
+
73
+ if not card_id:
74
+ delete_local_card(index)
75
+ return
76
+
77
+ try:
78
+ api_delete_card(card_id)
79
+ delete_local_card(index)
80
+ set_status("Card deleted.", "success")
81
+ except Exception as exc: # noqa: BLE001
82
+ set_status(f"Delete failed: {exc}", "error")
83
+
84
+
85
+ def handle_save(index: int) -> None:
86
+ card = st.session_state.cards[index]
87
+
88
+ if not card["title"].strip():
89
+ set_status("Title is required before saving.", "warning")
90
+ return
91
+
92
+ try:
93
+ if card.get("id"):
94
+ updated_card = update_card(card)
95
+ st.session_state.cards[index] = updated_card
96
+ set_status("Card updated.", "success")
97
+ else:
98
+ saved_card = save_card(card)
99
+ st.session_state.cards[index] = saved_card
100
+ set_status("Card saved.", "success")
101
+ except Exception as exc: # noqa: BLE001
102
+ set_status(f"Save failed: {exc}", "error")
103
+
104
+
105
+ load_css()
106
+ initialize_state()
107
+ fetch_cards_once()
108
+ render_header()
109
+
110
+ if st.session_state.status_message:
111
+ getattr(st, st.session_state.status_type)(st.session_state.status_message)
112
+
113
+ render_add_card_button(add_card)
114
+
115
+ if not st.session_state.cards:
116
+ st.markdown(
117
+ """
118
+ <div class="empty-state">
119
+ <h3>No time cards yet</h3>
120
+ <p>Create your first card to start tracking shifts and save them to Google Sheets.</p>
121
+ </div>
122
+ """,
123
+ unsafe_allow_html=True,
124
+ )
125
+
126
+ for index, _card in enumerate(st.session_state.cards):
127
+ render_card(
128
+ index=index,
129
+ on_save=handle_save,
130
+ on_delete=handle_delete,
131
+ )
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ streamlit>=1.37.0
2
+ requests>=2.32.0