Skip to content

Commit 0813758

Browse files
committed
Add Friends and Groups pages with API integration
- Created Friends.py page with placeholder for future features. - Developed Groups.py page with functionalities to join, create, and manage groups. - Implemented API request handling with retry logic. - Added debug mode for troubleshooting API interactions. - Included session state management for user authentication and group selection. - Updated requirements.txt to include necessary packages for the project.
1 parent 799d181 commit 0813758

File tree

6 files changed

+820
-0
lines changed

6 files changed

+820
-0
lines changed

ui-poc/Home.py

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
import streamlit as st
2+
import requests
3+
from datetime import datetime
4+
import json
5+
6+
# Configure the page
7+
st.set_page_config(
8+
page_title="Splitwiser",
9+
page_icon="💰",
10+
layout="wide",
11+
initial_sidebar_state="expanded"
12+
)
13+
14+
# Initialize session state variables
15+
if "access_token" not in st.session_state:
16+
st.session_state.access_token = None
17+
if "user_id" not in st.session_state:
18+
st.session_state.user_id = None
19+
if "username" not in st.session_state:
20+
st.session_state.username = None
21+
22+
# Base URL for API
23+
API_URL = "https://splitwiser-production.up.railway.app"
24+
25+
def login(email, password):
26+
"""Login user and store access token in session state"""
27+
try:
28+
response = requests.post(
29+
f"{API_URL}/auth/login/email",
30+
json={"email": email, "password": password}
31+
)
32+
if response.status_code == 200:
33+
data = response.json()
34+
st.session_state.access_token = data.get("access_token")
35+
# Update user_id from the response format
36+
user_data = data.get("user", {})
37+
st.session_state.user_id = user_data.get("_id") # Changed from user_id to _id
38+
st.session_state.username = user_data.get("name") # Changed from username to name
39+
return True, "Login successful!"
40+
else:
41+
return False, f"Login failed: {response.json().get('detail', 'Unknown error')}"
42+
except Exception as e:
43+
return False, f"Error: {str(e)}"
44+
45+
def signup(username, email, password):
46+
"""Register a new user"""
47+
try:
48+
response = requests.post(
49+
f"{API_URL}/auth/signup/email",
50+
json={"name": username, "email": email, "password": password}
51+
)
52+
if response.status_code == 200:
53+
return True, "Registration successful! Please login."
54+
else:
55+
return False, f"Registration failed: {response.json().get('detail', 'Unknown error')}"
56+
except Exception as e:
57+
return False, f"Error: {str(e)}"
58+
59+
def logout():
60+
"""Clear session state and log out user"""
61+
for key in list(st.session_state.keys()):
62+
del st.session_state[key]
63+
st.rerun()
64+
65+
# Main app
66+
def main():
67+
# Sidebar for app navigation
68+
with st.sidebar:
69+
st.title("Splitwiser 💰")
70+
71+
if st.session_state.access_token:
72+
st.success(f"Logged in as {st.session_state.username}")
73+
if st.button("Logout", key="logout_btn"):
74+
logout()
75+
76+
# Main content
77+
if not st.session_state.access_token:
78+
display_auth_page()
79+
else:
80+
display_main_app()
81+
82+
def display_auth_page():
83+
"""Display login/signup interface"""
84+
st.title("Welcome to Splitwiser")
85+
st.write("Track and split expenses with friends and groups")
86+
87+
# Create tabs for login and signup
88+
login_tab, signup_tab = st.tabs(["Login", "Sign Up"])
89+
90+
# Login tab
91+
with login_tab:
92+
with st.form("login_form", clear_on_submit=False):
93+
email = st.text_input("Email", key="login_email")
94+
password = st.text_input("Password", type="password", key="login_password")
95+
submit_button = st.form_submit_button("Login")
96+
97+
if submit_button:
98+
if email and password:
99+
success, message = login(email, password)
100+
if success:
101+
st.success(message)
102+
st.rerun()
103+
else:
104+
st.error(message)
105+
else:
106+
st.warning("Please fill in all fields")
107+
108+
# Sign up tab
109+
with signup_tab:
110+
with st.form("signup_form", clear_on_submit=True):
111+
username = st.text_input("Username", key="signup_username")
112+
email = st.text_input("Email", key="signup_email")
113+
password = st.text_input("Password", type="password", key="signup_password")
114+
confirm_password = st.text_input("Confirm Password", type="password", key="signup_confirm_password")
115+
submit_button = st.form_submit_button("Sign Up")
116+
117+
if submit_button:
118+
if username and email and password and confirm_password:
119+
if password != confirm_password:
120+
st.error("Passwords don't match")
121+
else:
122+
success, message = signup(username, email, password)
123+
if success:
124+
st.success(message)
125+
else:
126+
st.error(message)
127+
else:
128+
st.warning("Please fill in all fields")
129+
130+
def display_main_app():
131+
"""Display the main application after login"""
132+
st.title("Splitwiser Dashboard")
133+
134+
# Create tabs for Groups and Friends
135+
groups_tab, friends_tab = st.tabs(["Groups", "Friends"])
136+
137+
# Groups Tab
138+
with groups_tab:
139+
st.header("Groups")
140+
141+
# Join Group Expander
142+
with st.expander("Join a Group", expanded=False):
143+
with st.form("join_group_form", clear_on_submit=True):
144+
group_code = st.text_input("Enter Group Code", key="join_group_code")
145+
submit_button = st.form_submit_button("Join Group")
146+
147+
if submit_button and group_code:
148+
try:
149+
headers = {"Authorization": f"Bearer {st.session_state.access_token}"}
150+
response = requests.post(
151+
f"{API_URL}/groups/join",
152+
json={"joinCode": group_code},
153+
headers=headers
154+
)
155+
if response.status_code == 200:
156+
st.success("Successfully joined the group!")
157+
st.rerun()
158+
else:
159+
st.error(f"Failed to join group: {response.json().get('detail', 'Unknown error')}")
160+
except Exception as e:
161+
st.error(f"Error: {str(e)}")
162+
163+
# Create Group Expander
164+
with st.expander("Create a New Group", expanded=False):
165+
with st.form("create_group_form", clear_on_submit=True):
166+
group_name = st.text_input("Group Name", key="create_group_name")
167+
group_description = st.text_area("Description (Optional)", key="create_group_desc")
168+
submit_button = st.form_submit_button("Create Group")
169+
170+
if submit_button and group_name:
171+
try:
172+
headers = {"Authorization": f"Bearer {st.session_state.access_token}"}
173+
response = requests.post(
174+
f"{API_URL}/groups/",
175+
json={"name": group_name, "description": group_description or ""},
176+
headers=headers
177+
)
178+
if response.status_code == 201:
179+
st.success("Group created successfully!")
180+
group_data = response.json()
181+
st.info(f"Group Code: {group_data.get('joinCode', 'N/A')}")
182+
st.rerun()
183+
else:
184+
st.error(f"Failed to create group: {response.json().get('detail', 'Unknown error')}")
185+
except Exception as e:
186+
st.error(f"Error: {str(e)}")
187+
188+
# Display User's Groups
189+
try:
190+
headers = {"Authorization": f"Bearer {st.session_state.access_token}"}
191+
response = requests.get(
192+
f"{API_URL}/groups",
193+
headers=headers
194+
)
195+
196+
if response.status_code == 200:
197+
data = response.json()
198+
groups = data.get('groups', [])
199+
if groups:
200+
for group in groups:
201+
with st.container():
202+
col1, col2 = st.columns([3, 1])
203+
with col1:
204+
st.subheader(group.get("name", "Unnamed Group"))
205+
st.caption(f"Group Code: {group.get('joinCode', 'N/A')}")
206+
if group.get("description"):
207+
st.write(group.get("description"))
208+
with col2:
209+
if st.button("View Details", key=f"view_group_{group.get('_id')}"):
210+
st.session_state.selected_group = group
211+
st.rerun()
212+
else:
213+
st.info("You haven't joined any groups yet.")
214+
else:
215+
st.error("Failed to fetch groups.")
216+
except Exception as e:
217+
st.error(f"Error fetching groups: {str(e)}")
218+
219+
# Display Group Details if selected
220+
if "selected_group" in st.session_state:
221+
group = st.session_state.selected_group
222+
st.divider()
223+
st.subheader(f"Group Details: {group.get('name')}")
224+
225+
# Group Info
226+
with st.expander("Group Information", expanded=True):
227+
st.write(f"**Description:** {group.get('description', 'No description')}")
228+
st.write(f"**Group Code:** {group.get('joinCode')}")
229+
st.write(f"**Created On:** {datetime.fromisoformat(group.get('createdAt').replace('Z', '+00:00')).strftime('%Y-%m-%d')}")
230+
231+
# Group Members
232+
with st.expander("Members", expanded=True):
233+
try:
234+
headers = {"Authorization": f"Bearer {st.session_state.access_token}"}
235+
response = requests.get(
236+
f"{API_URL}/groups/{group.get('_id')}/members",
237+
headers=headers
238+
)
239+
240+
if response.status_code == 200:
241+
members = response.json()
242+
for member in members:
243+
st.write(f"• {member.get('user', {}).get('name', 'Unknown User')}")
244+
else:
245+
st.error("Failed to fetch group members.")
246+
except Exception as e:
247+
st.error(f"Error fetching members: {str(e)}")
248+
249+
# Add Expense
250+
with st.expander("Add New Expense", expanded=False):
251+
with st.form(f"add_expense_form_{group.get('_id')}", clear_on_submit=True):
252+
expense_title = st.text_input("Expense Title", key=f"expense_title_{group.get('_id')}")
253+
expense_amount = st.number_input("Amount", min_value=0.01, format="%.2f", key=f"expense_amount_{group.get('_id')}")
254+
expense_description = st.text_area("Description (Optional)", key=f"expense_desc_{group.get('_id')}")
255+
expense_date = st.date_input("Date", key=f"expense_date_{group.get('_id')}")
256+
submit_button = st.form_submit_button("Add Expense")
257+
258+
if submit_button and expense_title and expense_amount:
259+
try:
260+
headers = {"Authorization": f"Bearer {st.session_state.access_token}"}
261+
# Get group members for splitting
262+
try:
263+
members_response = requests.get(
264+
f"{API_URL}/groups/{group.get('_id')}/members",
265+
headers=headers
266+
)
267+
members = members_response.json()
268+
269+
# Create equal splits for all members
270+
equal_split_amount = round(expense_amount / len(members), 2)
271+
splits = [
272+
{
273+
"userId": member.get("userId"),
274+
"amount": equal_split_amount,
275+
"type": "equal"
276+
}
277+
for member in members
278+
]
279+
280+
expense_data = {
281+
"description": expense_title + (f" - {expense_description}" if expense_description else ""),
282+
"amount": expense_amount,
283+
"splits": splits,
284+
"splitType": "equal",
285+
"tags": []
286+
}
287+
except Exception as e:
288+
st.error(f"Error creating expense data: {str(e)}")
289+
st.stop()
290+
291+
response = requests.post(
292+
f"{API_URL}/groups/{group.get('_id')}/expenses",
293+
json=expense_data,
294+
headers=headers
295+
)
296+
297+
if response.status_code == 201:
298+
st.success("Expense added successfully!")
299+
else:
300+
st.error(f"Failed to add expense: {response.json().get('detail', 'Unknown error')}")
301+
except Exception as e:
302+
st.error(f"Error adding expense: {str(e)}")
303+
304+
# Group Expenses
305+
with st.expander("Expenses", expanded=True):
306+
try:
307+
headers = {"Authorization": f"Bearer {st.session_state.access_token}"}
308+
response = requests.get(
309+
f"{API_URL}/groups/{group.get('_id')}/expenses",
310+
headers=headers
311+
)
312+
313+
if response.status_code == 200:
314+
data = response.json()
315+
expenses = data.get('expenses', [])
316+
if expenses:
317+
for expense in expenses:
318+
with st.container():
319+
col1, col2, col3 = st.columns([3, 1, 1])
320+
with col1:
321+
st.write(f"**{expense.get('description', 'Unnamed Expense')}**")
322+
with col2:
323+
st.write(f"**₹{expense.get('amount', 0):.2f}**")
324+
with col3:
325+
date_str = expense.get('createdAt', '')
326+
if date_str:
327+
try:
328+
date_obj = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
329+
st.write(date_obj.strftime('%Y-%m-%d'))
330+
except:
331+
st.write(date_str)
332+
333+
# Get payer info
334+
payer_id = expense.get('createdBy')
335+
st.caption(f"Paid by: {next((m.get('user', {}).get('name', 'Unknown') for m in members if m.get('userId') == payer_id), 'Unknown')}")
336+
337+
st.divider()
338+
else:
339+
st.info("No expenses in this group yet.")
340+
else:
341+
st.error("Failed to fetch expenses.")
342+
except Exception as e:
343+
st.error(f"Error fetching expenses: {str(e)}")
344+
345+
# Friends Tab (Coming Soon)
346+
with friends_tab:
347+
st.header("Friends")
348+
st.info("Coming Soon!")
349+
350+
if __name__ == "__main__":
351+
main()

ui-poc/README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Splitwiser UI POC
2+
3+
This is a proof of concept UI for the Splitwiser application using Streamlit. It demonstrates the basic functionality of the application, including authentication, group management, and expense tracking.
4+
5+
## Features
6+
7+
- Login and Registration
8+
- Group Management:
9+
- Create new groups
10+
- Join existing groups
11+
- View group details
12+
- Add expenses to groups
13+
- Friends Management (Coming Soon)
14+
15+
## Setup and Installation
16+
17+
1. Install the required dependencies:
18+
```
19+
pip install -r requirements.txt
20+
```
21+
22+
2. Run the application:
23+
```
24+
streamlit run Home.py
25+
```
26+
27+
## API Connection
28+
29+
This UI connects to the Splitwiser backend API deployed at:
30+
`https://splitwiser-production.up.railway.app/`
31+
32+
## Session State Management
33+
34+
The application uses Streamlit's session state to manage user authentication and navigation between different views. Key session state variables include:
35+
36+
- `access_token`: User's authentication token
37+
- `user_id`: Current user's ID
38+
- `username`: Current user's username
39+
- `groups_view`: Current view in the Groups page (list or detail)
40+
- `selected_group_id`: ID of the currently selected group for detail view
41+
42+
## Structure
43+
44+
- `Home.py`: Main application file with login/signup and dashboard
45+
- `pages/Groups.py`: Group management functionality
46+
- `pages/Friends.py`: Friends functionality (coming soon)

ui-poc/openapi.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)