Merge pull request #11 from andreped/ui-ux-fixes
Browse filesImproved UI; added support for likes; refactored widgets to be more twitter-like
- app.py +76 -38
- postly/clients/postly_client.py +33 -0
- postly/common/css.py +52 -0
- postly/common/models.py +20 -2
app.py
CHANGED
@@ -1,10 +1,17 @@
|
|
1 |
import streamlit as st
|
2 |
|
3 |
from postly.clients.singleton_client import SingletonPostlyClient
|
|
|
4 |
|
5 |
# Initialize the PostlyClient singleton
|
6 |
client = SingletonPostlyClient.get_instance()
|
7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
# Initialize user session state
|
9 |
if "logged_in" not in st.session_state:
|
10 |
st.session_state.logged_in = False
|
@@ -14,8 +21,8 @@ if "current_user" not in st.session_state:
|
|
14 |
|
15 |
def register():
|
16 |
st.title("Register")
|
17 |
-
user_name = st.text_input("Enter user name")
|
18 |
-
password = st.text_input("Enter password", type="password")
|
19 |
if st.button("Register"):
|
20 |
if user_name and password:
|
21 |
try:
|
@@ -32,8 +39,8 @@ def register():
|
|
32 |
|
33 |
def login():
|
34 |
st.title("Login")
|
35 |
-
user_name = st.text_input("Enter user name")
|
36 |
-
password = st.text_input("Enter password", type="password")
|
37 |
if st.button("Login"):
|
38 |
if client.authenticate_user(user_name, password):
|
39 |
st.session_state.logged_in = True
|
@@ -52,7 +59,7 @@ def logout():
|
|
52 |
|
53 |
|
54 |
def delete_own_user():
|
55 |
-
st.title("Delete Account")
|
56 |
if st.button("Delete Account"):
|
57 |
try:
|
58 |
client.delete_user(st.session_state.current_user)
|
@@ -62,19 +69,8 @@ def delete_own_user():
|
|
62 |
st.error(f"Error: {e}")
|
63 |
|
64 |
|
65 |
-
def add_post():
|
66 |
-
st.title("Add Post")
|
67 |
-
post_text = st.text_area("Enter post text")
|
68 |
-
if st.button("Add Post"):
|
69 |
-
try:
|
70 |
-
client.add_post(st.session_state.current_user, post_text)
|
71 |
-
st.success("Post added successfully.")
|
72 |
-
except Exception as e:
|
73 |
-
st.error(f"Error: {e}")
|
74 |
-
|
75 |
-
|
76 |
def get_posts_for_user():
|
77 |
-
st.title("Get Posts for User")
|
78 |
users = client.get_users()
|
79 |
user_name = st.selectbox("Select user name", users)
|
80 |
if st.button("Get Posts"):
|
@@ -82,24 +78,24 @@ def get_posts_for_user():
|
|
82 |
posts = client.get_posts_for_user(user_name)
|
83 |
st.write(f"Posts for user '{user_name}':")
|
84 |
for post in posts:
|
85 |
-
st.
|
86 |
except KeyError as e:
|
87 |
st.error(f"Error: {e}")
|
88 |
|
89 |
|
90 |
def get_posts_for_topic():
|
91 |
-
st.title("Get Posts for Topic")
|
92 |
topics = client.get_topics()
|
93 |
topic = st.selectbox("Enter topic", topics)
|
94 |
if st.button("Get Posts"):
|
95 |
posts = client.get_posts_for_topic(topic)
|
96 |
st.write(f"Posts for topic '{topic}':")
|
97 |
for post in posts:
|
98 |
-
st.
|
99 |
|
100 |
|
101 |
def get_trending_topics():
|
102 |
-
st.title("Get Trending Topics")
|
103 |
current_timestamp = client.get_current_timestamp()
|
104 |
from_timestamp = st.number_input("Enter from timestamp", min_value=0, step=1)
|
105 |
to_timestamp = st.number_input(
|
@@ -116,21 +112,51 @@ def get_trending_topics():
|
|
116 |
|
117 |
|
118 |
def get_all_posts():
|
119 |
-
st.title("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
120 |
posts = client.get_posts()
|
121 |
all_posts = []
|
122 |
for user_name, user_posts in posts.items():
|
123 |
for post in user_posts:
|
124 |
all_posts.append((user_name, post))
|
125 |
-
sorted_posts = sorted(all_posts, key=lambda x: x[1].timestamp)
|
126 |
for user_name, post in sorted_posts:
|
127 |
-
st.
|
128 |
-
|
129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
130 |
|
131 |
|
132 |
def main():
|
133 |
-
st.sidebar.title("Postly
|
|
|
134 |
if st.session_state.logged_in:
|
135 |
st.sidebar.write(f"Logged in as: {st.session_state.current_user}")
|
136 |
if st.sidebar.button("Logout"):
|
@@ -138,18 +164,17 @@ def main():
|
|
138 |
page = st.sidebar.selectbox(
|
139 |
"Choose an action",
|
140 |
[
|
141 |
-
"
|
142 |
"Delete Account",
|
143 |
"Get Posts for User",
|
144 |
"Get Posts for Topic",
|
145 |
"Get Trending Topics",
|
146 |
-
"View All Posts",
|
147 |
],
|
148 |
-
index=
|
149 |
)
|
150 |
|
151 |
-
if page == "
|
152 |
-
|
153 |
elif page == "Delete Account":
|
154 |
delete_own_user()
|
155 |
elif page == "Get Posts for User":
|
@@ -158,14 +183,27 @@ def main():
|
|
158 |
get_posts_for_topic()
|
159 |
elif page == "Get Trending Topics":
|
160 |
get_trending_topics()
|
161 |
-
elif page == "View All Posts":
|
162 |
-
get_all_posts()
|
163 |
else:
|
164 |
-
page = st.sidebar.selectbox("Choose an action", ["
|
165 |
-
if page == "
|
166 |
-
login()
|
167 |
-
elif page == "Register":
|
168 |
register()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
169 |
|
170 |
|
171 |
if __name__ == "__main__":
|
|
|
1 |
import streamlit as st
|
2 |
|
3 |
from postly.clients.singleton_client import SingletonPostlyClient
|
4 |
+
from postly.common.css import get_theme
|
5 |
|
6 |
# Initialize the PostlyClient singleton
|
7 |
client = SingletonPostlyClient.get_instance()
|
8 |
|
9 |
+
# Custom CSS for Twitter-like feel
|
10 |
+
st.markdown(
|
11 |
+
get_theme(),
|
12 |
+
unsafe_allow_html=True,
|
13 |
+
)
|
14 |
+
|
15 |
# Initialize user session state
|
16 |
if "logged_in" not in st.session_state:
|
17 |
st.session_state.logged_in = False
|
|
|
21 |
|
22 |
def register():
|
23 |
st.title("Register")
|
24 |
+
user_name = st.text_input("Enter user name", placeholder="Username")
|
25 |
+
password = st.text_input("Enter password", type="password", placeholder="Password")
|
26 |
if st.button("Register"):
|
27 |
if user_name and password:
|
28 |
try:
|
|
|
39 |
|
40 |
def login():
|
41 |
st.title("Login")
|
42 |
+
user_name = st.text_input("Enter user name", placeholder="Username")
|
43 |
+
password = st.text_input("Enter password", type="password", placeholder="Password")
|
44 |
if st.button("Login"):
|
45 |
if client.authenticate_user(user_name, password):
|
46 |
st.session_state.logged_in = True
|
|
|
59 |
|
60 |
|
61 |
def delete_own_user():
|
62 |
+
st.title("Delete Account ❌")
|
63 |
if st.button("Delete Account"):
|
64 |
try:
|
65 |
client.delete_user(st.session_state.current_user)
|
|
|
69 |
st.error(f"Error: {e}")
|
70 |
|
71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
def get_posts_for_user():
|
73 |
+
st.title("Get Posts for User 🔎")
|
74 |
users = client.get_users()
|
75 |
user_name = st.selectbox("Select user name", users)
|
76 |
if st.button("Get Posts"):
|
|
|
78 |
posts = client.get_posts_for_user(user_name)
|
79 |
st.write(f"Posts for user '{user_name}':")
|
80 |
for post in posts:
|
81 |
+
st.markdown(post)
|
82 |
except KeyError as e:
|
83 |
st.error(f"Error: {e}")
|
84 |
|
85 |
|
86 |
def get_posts_for_topic():
|
87 |
+
st.title("Get Posts for Topic 🔎")
|
88 |
topics = client.get_topics()
|
89 |
topic = st.selectbox("Enter topic", topics)
|
90 |
if st.button("Get Posts"):
|
91 |
posts = client.get_posts_for_topic(topic)
|
92 |
st.write(f"Posts for topic '{topic}':")
|
93 |
for post in posts:
|
94 |
+
st.markdown(post)
|
95 |
|
96 |
|
97 |
def get_trending_topics():
|
98 |
+
st.title("Get Trending Topics 📊")
|
99 |
current_timestamp = client.get_current_timestamp()
|
100 |
from_timestamp = st.number_input("Enter from timestamp", min_value=0, step=1)
|
101 |
to_timestamp = st.number_input(
|
|
|
112 |
|
113 |
|
114 |
def get_all_posts():
|
115 |
+
st.title("Feed")
|
116 |
+
|
117 |
+
# Add post section at the top
|
118 |
+
post_text = st.text_area("What's happening? 💬", key="new_post_text")
|
119 |
+
if st.button("Add Post 🖊️"):
|
120 |
+
try:
|
121 |
+
client.add_post(st.session_state.current_user, post_text)
|
122 |
+
st.success("Post added successfully.")
|
123 |
+
st.rerun()
|
124 |
+
except Exception as e:
|
125 |
+
st.error(f"Error: {e}")
|
126 |
+
|
127 |
+
# Display all posts
|
128 |
posts = client.get_posts()
|
129 |
all_posts = []
|
130 |
for user_name, user_posts in posts.items():
|
131 |
for post in user_posts:
|
132 |
all_posts.append((user_name, post))
|
133 |
+
sorted_posts = sorted(all_posts, key=lambda x: x[1].timestamp)[::-1]
|
134 |
for user_name, post in sorted_posts:
|
135 |
+
liked = st.session_state.current_user in post.liked_by
|
136 |
+
like_button_label = "👍" if not liked else "👎"
|
137 |
+
|
138 |
+
col1, col2 = st.columns([4, 1])
|
139 |
+
with col1:
|
140 |
+
st.markdown(
|
141 |
+
f"""
|
142 |
+
<div class="post-container">
|
143 |
+
<div class="post-header">{user_name}</div>
|
144 |
+
<div class="post-content">{post.content}</div>
|
145 |
+
<div class="post-timestamp">{post.timestamp}</div>
|
146 |
+
<div class="post-likes">Likes: {post.likes}</div>
|
147 |
+
</div>
|
148 |
+
""",
|
149 |
+
unsafe_allow_html=True,
|
150 |
+
)
|
151 |
+
with col2:
|
152 |
+
if st.button(like_button_label, key=f"like_{post.timestamp}"):
|
153 |
+
client.like_post(st.session_state.current_user, post.timestamp)
|
154 |
+
st.rerun()
|
155 |
|
156 |
|
157 |
def main():
|
158 |
+
st.sidebar.title("Postly 📝\nSimple social media platform")
|
159 |
+
|
160 |
if st.session_state.logged_in:
|
161 |
st.sidebar.write(f"Logged in as: {st.session_state.current_user}")
|
162 |
if st.sidebar.button("Logout"):
|
|
|
164 |
page = st.sidebar.selectbox(
|
165 |
"Choose an action",
|
166 |
[
|
167 |
+
"View All Posts",
|
168 |
"Delete Account",
|
169 |
"Get Posts for User",
|
170 |
"Get Posts for Topic",
|
171 |
"Get Trending Topics",
|
|
|
172 |
],
|
173 |
+
index=0,
|
174 |
)
|
175 |
|
176 |
+
if page == "View All Posts":
|
177 |
+
get_all_posts()
|
178 |
elif page == "Delete Account":
|
179 |
delete_own_user()
|
180 |
elif page == "Get Posts for User":
|
|
|
183 |
get_posts_for_topic()
|
184 |
elif page == "Get Trending Topics":
|
185 |
get_trending_topics()
|
|
|
|
|
186 |
else:
|
187 |
+
page = st.sidebar.selectbox("Choose an action", ["Register", "Login"], index=0)
|
188 |
+
if page == "Register":
|
|
|
|
|
189 |
register()
|
190 |
+
elif page == "Login":
|
191 |
+
login()
|
192 |
+
|
193 |
+
st.sidebar.markdown(
|
194 |
+
"""
|
195 |
+
**About Postly**
|
196 |
+
|
197 |
+
Welcome to Postly, a simple social media platform created for fun.
|
198 |
+
This app allows different users to share posts and like each other's posts.
|
199 |
+
|
200 |
+
**Important Information**
|
201 |
+
|
202 |
+
- The entire app is kept in global memory for all users accessing the app on the same instance.
|
203 |
+
- Do not use a username and password actually used with any other apps.
|
204 |
+
- We hash the password, but no real attempt to make a bulletproof solution was made.
|
205 |
+
"""
|
206 |
+
)
|
207 |
|
208 |
|
209 |
if __name__ == "__main__":
|
postly/clients/postly_client.py
CHANGED
@@ -214,3 +214,36 @@ class PostlyClient:
|
|
214 |
|
215 |
# retrieve top topics in descending order
|
216 |
return [topic for topic, _ in topics_frequency.most_common()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
214 |
|
215 |
# retrieve top topics in descending order
|
216 |
return [topic for topic, _ in topics_frequency.most_common()]
|
217 |
+
|
218 |
+
def like_post(self, user_name: str, post_id: int) -> None:
|
219 |
+
"""
|
220 |
+
Like or unlike a post.
|
221 |
+
|
222 |
+
Args:
|
223 |
+
user_name: The name of the user liking the post.
|
224 |
+
post_id: The ID of the post to like or unlike.
|
225 |
+
Returns:
|
226 |
+
None
|
227 |
+
"""
|
228 |
+
post = self.get_post_by_id(post_id)
|
229 |
+
if user_name in post.liked_by:
|
230 |
+
post.liked_by.remove(user_name)
|
231 |
+
post.likes -= 1
|
232 |
+
else:
|
233 |
+
post.liked_by.add(user_name)
|
234 |
+
post.likes += 1
|
235 |
+
|
236 |
+
def get_post_by_id(self, post_id: int) -> Post:
|
237 |
+
"""
|
238 |
+
Get a post by its ID.
|
239 |
+
|
240 |
+
Args:
|
241 |
+
post_id: The ID of the post to retrieve.
|
242 |
+
Returns:
|
243 |
+
The post with the given ID.
|
244 |
+
"""
|
245 |
+
for user_posts in self.userPosts.values():
|
246 |
+
for post in user_posts:
|
247 |
+
if post.timestamp == post_id:
|
248 |
+
return post
|
249 |
+
raise KeyError("Post not found")
|
postly/common/css.py
ADDED
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
def get_theme():
|
2 |
+
return """
|
3 |
+
<style>
|
4 |
+
body {
|
5 |
+
background-color: #ffffff;
|
6 |
+
color: #1DA1F2;
|
7 |
+
}
|
8 |
+
.stButton>button {
|
9 |
+
background-color: #1DA1F2;
|
10 |
+
color: white;
|
11 |
+
}
|
12 |
+
.stTextInput>div>div>input {
|
13 |
+
border: 1px solid #1DA1F2;
|
14 |
+
}
|
15 |
+
.stTextArea>div>div>textarea {
|
16 |
+
border: 1px solid #1DA1F2;
|
17 |
+
}
|
18 |
+
.stSelectbox>div>div>div>div {
|
19 |
+
border: 1px solid #1DA1F2;
|
20 |
+
}
|
21 |
+
.post-container {
|
22 |
+
border: 1px solid #1DA1F2;
|
23 |
+
border-radius: 10px;
|
24 |
+
padding: 10px;
|
25 |
+
margin-bottom: 10px;
|
26 |
+
background-color: #f5f8fa;
|
27 |
+
min-height: 100px; /* Adjust this value as needed */
|
28 |
+
position: relative; /* Ensure absolute positioning works within this container */
|
29 |
+
}
|
30 |
+
.post-header {
|
31 |
+
font-weight: bold;
|
32 |
+
color: #1DA1F2;
|
33 |
+
}
|
34 |
+
.post-content {
|
35 |
+
color: #14171A;
|
36 |
+
}
|
37 |
+
.post-likes {
|
38 |
+
color: #657786;
|
39 |
+
font-size: 14px;
|
40 |
+
position: absolute;
|
41 |
+
bottom: 10px;
|
42 |
+
right: 10px;
|
43 |
+
}
|
44 |
+
.like-button {
|
45 |
+
background-color: transparent;
|
46 |
+
border: none;
|
47 |
+
color: #1DA1F2;
|
48 |
+
cursor: pointer;
|
49 |
+
font-size: 20px;
|
50 |
+
}
|
51 |
+
</style>
|
52 |
+
"""
|
postly/common/models.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1 |
from dataclasses import dataclass
|
|
|
2 |
from typing import List
|
|
|
3 |
|
4 |
from pydantic import BaseModel
|
5 |
|
@@ -8,6 +10,8 @@ class StrictPost(BaseModel):
|
|
8 |
content: str
|
9 |
timestamp: int
|
10 |
topics: List[str]
|
|
|
|
|
11 |
|
12 |
|
13 |
@dataclass
|
@@ -15,11 +19,25 @@ class Post:
|
|
15 |
content: str
|
16 |
timestamp: int
|
17 |
topics: List[str]
|
|
|
|
|
18 |
|
19 |
|
20 |
if __name__ == "__main__":
|
21 |
# this should be OK, as not strictly typed
|
22 |
-
Post(content=1, timestamp=1, topics=["1"])
|
|
|
|
|
|
|
23 |
|
24 |
# this should result in a validation error, as pydantic enforces strict typing on runtime
|
25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
from dataclasses import dataclass
|
2 |
+
from dataclasses import field
|
3 |
from typing import List
|
4 |
+
from typing import Set
|
5 |
|
6 |
from pydantic import BaseModel
|
7 |
|
|
|
10 |
content: str
|
11 |
timestamp: int
|
12 |
topics: List[str]
|
13 |
+
likes: int = 0
|
14 |
+
liked_by: Set[str] = set()
|
15 |
|
16 |
|
17 |
@dataclass
|
|
|
19 |
content: str
|
20 |
timestamp: int
|
21 |
topics: List[str]
|
22 |
+
likes: int = 0
|
23 |
+
liked_by: Set[str] = field(default_factory=set)
|
24 |
|
25 |
|
26 |
if __name__ == "__main__":
|
27 |
# this should be OK, as not strictly typed
|
28 |
+
post = Post(content="1", timestamp=1, topics=["1"])
|
29 |
+
post.likes += 1
|
30 |
+
post.liked_by.add("user1")
|
31 |
+
print(post)
|
32 |
|
33 |
# this should result in a validation error, as pydantic enforces strict typing on runtime
|
34 |
+
try:
|
35 |
+
strict_post = StrictPost(content=1, timestamp=1, topics=["1"])
|
36 |
+
except Exception as e:
|
37 |
+
print(f"Validation error: {e}")
|
38 |
+
|
39 |
+
# this should be OK, as strictly typed
|
40 |
+
strict_post = StrictPost(content="1", timestamp=1, topics=["1"])
|
41 |
+
strict_post.likes += 1
|
42 |
+
strict_post.liked_by.add("user1")
|
43 |
+
print(strict_post)
|