GphaHoa commited on
Commit
9d3281a
·
verified ·
1 Parent(s): 42d26f6

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +213 -40
src/streamlit_app.py CHANGED
@@ -1,40 +1,213 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
- import streamlit as st
5
-
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from supabase import create_client
3
+ from datetime import datetime
4
+ from zoneinfo import ZoneInfo
5
+ from supabase_auth.errors import AuthApiError
6
+ import time
7
+
8
+ # ========== CONFIG ==========
9
+ st.set_page_config(
10
+ page_title="Personal Diary",
11
+ page_icon="📔",
12
+ layout="wide"
13
+ )
14
+
15
+ supabase = create_client(
16
+ st.secrets["SUPABASE_URL"],
17
+ st.secrets["SUPABASE_KEY"]
18
+ )
19
+
20
+ # ========== STYLE ==========
21
+ st.markdown("""
22
+ <style>
23
+ .diary-card {
24
+ padding: 1rem;
25
+ border-radius: 12px;
26
+ background-color: #f8f9fa;
27
+ margin-bottom: 1rem;
28
+ }
29
+ .small-text {
30
+ font-size: 0.8rem;
31
+ color: #6c757d;
32
+ }
33
+ </style>
34
+ """, unsafe_allow_html=True)
35
+
36
+ def signup(email, password):
37
+ return supabase.auth.sign_up({
38
+ "email": email,
39
+ "password": password
40
+ })
41
+
42
+
43
+ # ========== HEADER ==========
44
+ st.title("📔 Personal Diary")
45
+ st.caption("Ghi chép suy nghĩ mỗi ngày – phiên bản demo")
46
+
47
+ # ========== LOGIN ==========
48
+ if "user" not in st.session_state:
49
+ st.subheader("🔐 Tài khoản")
50
+
51
+ tab_login, tab_signup = st.tabs(["Đăng nhập", "Đăng ký"])
52
+
53
+ # ===== LOGIN =====
54
+ with tab_login:
55
+ email = st.text_input("Email", key="login_email")
56
+ password = st.text_input("Mật khẩu", type="password", key="login_pw")
57
+
58
+ if st.button("➡️ Đăng nhập", use_container_width=True):
59
+ res = supabase.auth.sign_in_with_password({
60
+ "email": email,
61
+ "password": password
62
+ })
63
+
64
+ if res.user:
65
+ st.session_state.user = res.user
66
+ st.success("Đăng nhập thành công")
67
+ st.rerun()
68
+ else:
69
+ st.error("Sai email hoặc mật khẩu")
70
+
71
+ # ===== SIGNUP =====
72
+ with tab_signup:
73
+ email = st.text_input("Email đăng ký", key="signup_email")
74
+ password = st.text_input(
75
+ "Mật khẩu (tối thiểu 6 ký tự)",
76
+ type="password",
77
+ key="signup_pw"
78
+ )
79
+
80
+ # init cooldown
81
+ if "signup_cooldown_until" not in st.session_state:
82
+ st.session_state.signup_cooldown_until = 0
83
+
84
+ now = time.time()
85
+ cooldown_left = int(st.session_state.signup_cooldown_until - now)
86
+
87
+ if cooldown_left > 0:
88
+ st.warning(f"⏳ Vui lòng thử lại sau {cooldown_left} giây")
89
+
90
+ signup_disabled = cooldown_left > 0
91
+
92
+ if st.button(
93
+ "🆕 Tạo tài khoản",
94
+ use_container_width=True,
95
+ disabled=signup_disabled
96
+ ):
97
+ try:
98
+ res = signup(email, password)
99
+
100
+ if res.user:
101
+ st.session_state.user = res.user
102
+ st.success("Đăng ký thành công 🎉")
103
+ st.rerun()
104
+ else:
105
+ st.error("Không thể đăng ký")
106
+
107
+ except AuthApiError as e:
108
+ msg = str(e)
109
+
110
+ # Bắt lỗi rate limit 60s
111
+ if "only request this after" in msg:
112
+ # Mặc định 60s nếu parse không được
113
+ wait_seconds = 60
114
+
115
+ # cố gắng parse số giây từ message
116
+ import re
117
+
118
+ match = re.search(r"after (\d+) seconds", msg)
119
+ if match:
120
+ wait_seconds = int(match.group(1))
121
+
122
+ st.session_state.signup_cooldown_until = time.time() + wait_seconds
123
+ st.error(f"🚫 Bạn đã thử quá nhiều lần. Vui lòng đợi {wait_seconds} giây rồi thử lại.")
124
+ st.rerun()
125
+ else:
126
+ st.error("Lỗi đăng ký: " + msg)
127
+
128
+
129
+ # ========== MAIN APP ==========
130
+ else:
131
+ user = st.session_state.user
132
+
133
+ # Top bar
134
+ top_left, top_right = st.columns([4, 1])
135
+ with top_left:
136
+ st.markdown(f"👋 Xin chào **{user.email}**")
137
+ with top_right:
138
+ if st.button("🚪 Đăng xuất", use_container_width=True):
139
+ st.session_state.clear()
140
+ st.rerun()
141
+
142
+ st.divider()
143
+
144
+ # Main layout
145
+ left, right = st.columns([2, 3])
146
+
147
+ # ===== LEFT: WRITE =====
148
+ with left:
149
+ st.subheader("✍️ Viết nhật ký")
150
+
151
+ content = st.text_area(
152
+ "Hôm nay bạn nghĩ gì?",
153
+ height=250,
154
+ placeholder="Viết suy nghĩ của bạn ở đây..."
155
+ )
156
+
157
+ if st.button("💾 Lưu nhật ký", use_container_width=True):
158
+ if content.strip():
159
+ supabase.table("journals").insert({
160
+ "user_id": user.id,
161
+ "content": content,
162
+ "created_at": datetime.utcnow().isoformat()
163
+ }).execute()
164
+
165
+ st.success("Đã lưu nhật ký ✨")
166
+ st.rerun()
167
+ else:
168
+ st.warning("Nội dung đang trống")
169
+
170
+ # ===== RIGHT: LIST =====
171
+ with right:
172
+ st.subheader("📜 Nhật ký của bạn")
173
+
174
+ data = supabase.table("journals") \
175
+ .select("*") \
176
+ .eq("user_id", user.id) \
177
+ .order("created_at", desc=True) \
178
+ .execute()
179
+
180
+ if not data.data:
181
+ st.info("Chưa có nhật ký nào")
182
+ else:
183
+ for row in data.data:
184
+ # Format ngày cho gọn
185
+ created_dt = datetime.fromisoformat(
186
+ row["created_at"].replace("Z", "+00:00")
187
+ ).astimezone(ZoneInfo("Asia/Ho_Chi_Minh"))
188
+
189
+ date_label = created_dt.strftime("%d/%m/%Y – %H:%M")
190
+
191
+ # Dropdown theo ngày
192
+ with st.expander(f"📅 {date_label}", expanded=False):
193
+
194
+ new_content = st.text_area(
195
+ "Nội dung",
196
+ row["content"],
197
+ key=row["id"],
198
+ height=120
199
+ )
200
+
201
+ col_u, col_d = st.columns(2)
202
+
203
+ if col_u.button("✏️ Cập nhật", key=f"u{row['id']}"):
204
+ supabase.table("journals").update({
205
+ "content": new_content,
206
+ "updated_at": datetime.utcnow().isoformat()
207
+ }).eq("id", row["id"]).execute()
208
+ st.rerun()
209
+
210
+ if col_d.button("🗑️ Xoá", key=f"d{row['id']}"):
211
+ supabase.table("journals").delete() \
212
+ .eq("id", row["id"]).execute()
213
+ st.rerun()