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