From 16dc8f1ea27e7f016ddda2525f1bcf88c9d9647c Mon Sep 17 00:00:00 2001 From: Chris-1010 <122332721@umail.ucc.ie> Date: Fri, 7 Feb 2025 02:11:22 +0000 Subject: [PATCH] FEAT: Added more info & functionality to UserPage & Added ability to follow streamers on both UserPage and VideoPage; Added shortcut to toggle chat; --- frontend/package.json | 2 +- frontend/src/components/Auth/AuthModal.tsx | 101 +++++----- frontend/src/components/Layout/ListRow.tsx | 6 +- frontend/src/components/Layout/Navbar.tsx | 47 ++--- .../src/components/Stream/StreamerRoute.tsx | 13 +- frontend/src/components/Video/ChatPanel.tsx | 43 ++-- frontend/src/components/Video/VideoPlayer.tsx | 4 +- frontend/src/context/StreamsContext.tsx | 5 +- frontend/src/hooks/useAuthModal.ts | 21 ++ frontend/src/hooks/useFollow.ts | 65 ++++++ frontend/src/pages/UserPage.tsx | 186 +++++++++++++----- frontend/src/pages/VideoPage.tsx | 106 ++++++---- frontend/tailwind.config.js | 1 + web_server/blueprints/streams.py | 2 +- web_server/blueprints/user.py | 31 ++- web_server/utils/user_utils.py | 45 ++--- 16 files changed, 438 insertions(+), 240 deletions(-) create mode 100644 frontend/src/hooks/useAuthModal.ts create mode 100644 frontend/src/hooks/useFollow.ts diff --git a/frontend/package.json b/frontend/package.json index 3cbe9ad..6c9fe50 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.0.0", + "version": "0.1.0", "type": "module", "scripts": { "dev": "vite --config vite.config.dev.ts", diff --git a/frontend/src/components/Auth/AuthModal.tsx b/frontend/src/components/Auth/AuthModal.tsx index a4f50e7..31da1fc 100644 --- a/frontend/src/components/Auth/AuthModal.tsx +++ b/frontend/src/components/Auth/AuthModal.tsx @@ -28,66 +28,61 @@ const AuthModal: React.FC = ({ onClose }) => { id="blurring-layer" className="fixed z-50 inset-0 w-screen h-screen backdrop-blur-sm group-has-[input:focus]:backdrop-blur-[5px]" > -
- {/*Container*/} -
- - {/* Login/Register Buttons Container */} -
- {/* Login Toggle */} - setSelectedTab("Login")} - > - - Login - - - {/* Register Toggle */} - setSelectedTab("Register")} - > - - Register - -
-
+ {/* Login/Register Buttons Container */} +
+ {/* Login Toggle */} + setSelectedTab("Login")} > + + Login + - {/*Border Container*/} -
setSelectedTab("Register")} + > + + Register + +
+
+ {/*Border Container*/} +
+
-
- - -
- {selectedTab === "Login" ? ( - - ) : ( - - )} + ✕ +
+ {selectedTab === "Login" ? ( + + ) : ( + + )}
diff --git a/frontend/src/components/Layout/ListRow.tsx b/frontend/src/components/Layout/ListRow.tsx index 538da67..5afc2f2 100644 --- a/frontend/src/components/Layout/ListRow.tsx +++ b/frontend/src/components/Layout/ListRow.tsx @@ -5,6 +5,7 @@ interface ListItemProps { id: number; title: string; streamer?: string; + streamCategory?: string; viewers: number; thumbnail?: string; onItemClick?: () => void; @@ -41,6 +42,7 @@ const ListRow: React.FC = ({ type={item.type} title={item.title} streamer={item.type === "stream" ? item.streamer : undefined} + streamCategory={item.type === "stream" ? item.streamCategory : undefined} viewers={item.viewers} thumbnail={item.thumbnail} onItemClick={() => onClick?.(item.id, item.streamer || item.title)} @@ -52,10 +54,11 @@ const ListRow: React.FC = ({ }; // Individual list entry component -const ListItem: React.FC = ({ +export const ListItem: React.FC = ({ type, title, streamer, + streamCategory, viewers, thumbnail, onItemClick, @@ -79,6 +82,7 @@ const ListItem: React.FC = ({

{title}

{type === "stream" &&

{streamer}

} + {type === "stream" &&

{streamCategory}

}

{viewers} viewers

diff --git a/frontend/src/components/Layout/Navbar.tsx b/frontend/src/components/Layout/Navbar.tsx index 2515fea..bfe15dd 100644 --- a/frontend/src/components/Layout/Navbar.tsx +++ b/frontend/src/components/Layout/Navbar.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import Logo from "./Logo"; import Button from "./Button"; import Sidebar from "./Sidebar"; @@ -11,29 +11,17 @@ import { } from "lucide-react"; import Input from "./Input"; import AuthModal from "../Auth/AuthModal"; +import { useAuthModal } from "../../hooks/useAuthModal"; import { useAuth } from "../../context/AuthContext"; interface NavbarProps { variant?: "home" | "default"; } -const Navbar: React.FC = ({ - variant = "default", -}) => { - const [showAuthModal, setShowAuthModal] = useState(false); +const Navbar: React.FC = ({ variant = "default" }) => { const { isLoggedIn } = useAuth(); + const { showAuthModal, setShowAuthModal } = useAuthModal(); const [showSideBar, setShowSideBar] = useState(false); - - useEffect(() => { - if (showAuthModal) { - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = "unset"; - } - return () => { - document.body.style.overflow = "unset"; - }; - }, [showAuthModal]); const handleLogout = () => { console.log("Logging out..."); @@ -47,12 +35,16 @@ const Navbar: React.FC = ({ const handleSideBar = () => { setShowSideBar(!showSideBar); - } + }; return ( ); - - // Or your loading component } // streamId=0 is a special case for the streamer's latest stream return isLive ? ( ) : streamerName ? ( - + navigate(`/user/${streamerName}`) ) : ( -
Error: Streamer not found
+
Streamer not found
); }; diff --git a/frontend/src/components/Video/ChatPanel.tsx b/frontend/src/components/Video/ChatPanel.tsx index 5dad33a..2536388 100644 --- a/frontend/src/components/Video/ChatPanel.tsx +++ b/frontend/src/components/Video/ChatPanel.tsx @@ -1,9 +1,10 @@ import React, { useState, useEffect, useRef } from "react"; import Input from "../Layout/Input"; -import { useAuth } from "../../context/AuthContext"; -import { useSocket } from "../../context/SocketContext"; import Button from "../Layout/Button"; import AuthModal from "../Auth/AuthModal"; +import { useAuthModal } from "../../hooks/useAuthModal"; +import { useAuth } from "../../context/AuthContext"; +import { useSocket } from "../../context/SocketContext"; interface ChatMessage { chatter_username: string; @@ -16,12 +17,16 @@ interface ChatPanelProps { onViewerCountChange?: (count: number) => void; } -const ChatPanel: React.FC = ({ streamId, onViewerCountChange }) => { +const ChatPanel: React.FC = ({ + streamId, + onViewerCountChange, +}) => { + const { isLoggedIn, username } = useAuth(); + const { showAuthModal, setShowAuthModal } = useAuthModal(); const { socket, isConnected } = useSocket(); const [messages, setMessages] = useState([]); const [inputMessage, setInputMessage] = useState(""); const chatContainerRef = useRef(null); - const { isLoggedIn, username } = useAuth(); // Join chat room when component mounts useEffect(() => { @@ -57,17 +62,16 @@ const ChatPanel: React.FC = ({ streamId, onViewerCountChange }) // Handle incoming messages socket.on("new_message", (data: ChatMessage) => { - console.log("New message:", data); setMessages((prev) => [...prev, data]); }); // Handle live viewership socket.on("status", (data: any) => { - console.log("Live viewership: ", data) // returns dictionary {message: message, num_viewers: num_viewers} + console.log("Live viewership: ", data); // returns dictionary {message: message, num_viewers: num_viewers} if (onViewerCountChange && data.num_viewers) { onViewerCountChange(data.num_viewers); } - }) + }); // Cleanup function return () => { @@ -108,20 +112,6 @@ const ChatPanel: React.FC = ({ streamId, onViewerCountChange }) } }; - //added to show login/reg if not - const [showAuthModal, setShowAuthModal] = useState(false); - - useEffect(() => { - if (showAuthModal) { - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = "unset"; - } - return () => { - document.body.style.overflow = "unset"; - }; - }, [showAuthModal]); - return (
= ({ streamId, onViewerCountChange }) value={inputMessage} onChange={(e) => setInputMessage(e.target.value)} onKeyDown={handleKeyPress} - placeholder={isLoggedIn ? "Type a message..." : "Login to chat"} - disabled={!isLoggedIn} + placeholder="Type a message..." extraClasses="flex-grow w-full focus:w-full" onClick={() => !isLoggedIn && setShowAuthModal(true)} /> @@ -181,18 +170,14 @@ const ChatPanel: React.FC = ({ streamId, onViewerCountChange }) ) : ( )}
- {showAuthModal && ( -
- setShowAuthModal(false)} /> -
- )} + {showAuthModal && setShowAuthModal(false)} />}
); }; diff --git a/frontend/src/components/Video/VideoPlayer.tsx b/frontend/src/components/Video/VideoPlayer.tsx index e9fdfe9..2b93e4d 100644 --- a/frontend/src/components/Video/VideoPlayer.tsx +++ b/frontend/src/components/Video/VideoPlayer.tsx @@ -52,10 +52,10 @@ const VideoPlayer: React.FC = ({ streamId }) => { return (
-
+
); }; diff --git a/frontend/src/context/StreamsContext.tsx b/frontend/src/context/StreamsContext.tsx index 603d823..c2ac55d 100644 --- a/frontend/src/context/StreamsContext.tsx +++ b/frontend/src/context/StreamsContext.tsx @@ -11,6 +11,7 @@ interface Item { interface StreamItem extends Item { type: "stream"; streamer: string; + streamCategory: string; } interface CategoryItem extends Item { @@ -47,13 +48,13 @@ export function StreamsProvider({ children }: { children: React.ReactNode }) { id: stream.user_id, title: stream.title, streamer: stream.username, + streamCategory: stream.category_name, viewers: stream.num_viewers, thumbnail: stream.thumbnail || `/images/thumbnails/categories/${stream.category_name .toLowerCase() - .replace(/ /g, "_")}.webp`, - category: stream.category_name, + .replace(/ /g, "_")}.webp` })); setFeaturedStreams(extractedData); diff --git a/frontend/src/hooks/useAuthModal.ts b/frontend/src/hooks/useAuthModal.ts new file mode 100644 index 0000000..aa427c9 --- /dev/null +++ b/frontend/src/hooks/useAuthModal.ts @@ -0,0 +1,21 @@ +import { useState, useEffect } from 'react'; + +export function useAuthModal() { + const [showAuthModal, setShowAuthModal] = useState(false); + + useEffect(() => { + if (showAuthModal) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = "unset"; + } + return () => { + document.body.style.overflow = "unset"; + }; + }, [showAuthModal]); + + return { + showAuthModal, + setShowAuthModal, + }; +} \ No newline at end of file diff --git a/frontend/src/hooks/useFollow.ts b/frontend/src/hooks/useFollow.ts new file mode 100644 index 0000000..cfe48a6 --- /dev/null +++ b/frontend/src/hooks/useFollow.ts @@ -0,0 +1,65 @@ +import { useState } from 'react'; +import { useAuth } from '../context/AuthContext'; + +export function useFollow() { + const [isFollowing, setIsFollowing] = useState(false); + const { isLoggedIn } = useAuth(); + + const checkFollowStatus = async (username: string) => { + try { + const response = await fetch(`/api/user/following/${username}`); + const data = await response.json(); + setIsFollowing(data.following); + } catch (error) { + console.error("Error checking follow status:", error); + } + }; + + const followUser = async (userId: number, setShowAuthModal?: (show: boolean) => void) => { + if (!isLoggedIn) { + setShowAuthModal?.(true); + return; + } + + try { + const response = await fetch(`/api/user/follow/${userId}`); + const data = await response.json(); + if (data.success) { + console.log(`Now following user ${userId}`); + setIsFollowing(true); + } else { + console.error(`Failed to follow user ${userId}`); + } + } catch (error) { + console.error("Error following user:", error); + } + }; + + const unfollowUser = async (userId: number, setShowAuthModal?: (show: boolean) => void) => { + if (!isLoggedIn) { + setShowAuthModal?.(true); + return; + } + + try { + const response = await fetch(`/api/user/unfollow/${userId}`); + const data = await response.json(); + if (data.success) { + console.log(`Unfollowed user ${userId}`); + setIsFollowing(false); + } else { + console.error(`Failed to unfollow user ${userId}`); + } + } catch (error) { + console.error("Error unfollowing user:", error); + } + }; + + return { + isFollowing, + setIsFollowing, + checkFollowStatus, + followUser, + unfollowUser + }; +} diff --git a/frontend/src/pages/UserPage.tsx b/frontend/src/pages/UserPage.tsx index 7fd2f4c..a7b3ca0 100644 --- a/frontend/src/pages/UserPage.tsx +++ b/frontend/src/pages/UserPage.tsx @@ -1,24 +1,45 @@ import React, { useState, useEffect } from "react"; import Navbar from "../components/Layout/Navbar"; -import { useParams } from "react-router-dom"; +import AuthModal from "../components/Auth/AuthModal"; +import { useAuthModal } from "../hooks/useAuthModal"; import { useAuth } from "../context/AuthContext"; +import { useParams } from "react-router-dom"; +import { ListItem } from "../components/Layout/ListRow"; +import { useFollow } from "../hooks/useFollow"; +import { useNavigate } from "react-router-dom"; +import Button from "../components/Layout/Button"; interface UserProfileData { + id: number; username: string; bio: string; followerCount: number; isPartnered: boolean; + isLive: boolean; + currentStreamTitle?: string; + currentStreamCategory?: string; + currentStreamViewers?: number; + currentStreamStartTime?: string; + currentStreamThumbnail?: string; } const UserPage: React.FC = () => { - const [profileData, setProfileData] = useState(null); + const [userPageVariant, setUserPageVariant] = useState< + "personal" | "streamer" | "user" | "admin" + >("user"); + const [profileData, setProfileData] = useState(); + const { isFollowing, checkFollowStatus, followUser, unfollowUser } = useFollow(); + const { showAuthModal, setShowAuthModal } = useAuthModal(); const { username: loggedInUsername } = useAuth(); const { username } = useParams(); - let userPageVariant = "user"; + const navigate = useNavigate(); - let setUserPageVariant = (currentStream: string) => { - if (username === loggedInUsername) userPageVariant = "personal"; - else if (currentStream) userPageVariant = "streamer"; + const bgColors = { + personal: "", + streamer: "bg-gradient-radial from-[#ff00f1] via-[#0400ff] to-[#ff0000]", // offline streamer + user: "bg-gradient-radial from-[#ff00f1] via-[#0400ff] to-[#ff00f1]", + admin: + "bg-gradient-to-r from-[rgb(255,0,0)] via-transparent to-[rgb(0,0,255)]", }; useEffect(() => { @@ -27,15 +48,52 @@ const UserPage: React.FC = () => { .then((res) => res.json()) .then((data) => { setProfileData({ + id: data.user_id, username: data.username, bio: data.bio || "This user hasn't written a bio yet.", followerCount: data.num_followers || 0, isPartnered: data.isPartnered || false, + isLive: data.is_live, + currentStreamTitle: "", + currentStreamCategory: "", + currentStreamViewers: 0, + currentStreamThumbnail: "", }); - setUserPageVariant(data.current_stream_title); + if (data.is_live) { + // Fetch stream data for this streamer + fetch(`/api/streams/${data.user_id}/data`) + .then((res) => res.json()) + .then((streamData) => { + setProfileData((prevData) => { + if (!prevData) return prevData; + return { + ...prevData, + currentStreamTitle: streamData.title, + currentStreamCategory: streamData.category_id, + currentStreamViewers: streamData.num_viewers, + currentStreamStartTime: streamData.start_time, + currentStreamThumbnail: + streamData.thumbnail || + `/images/thumbnails/categories/${streamData.category_name + .toLowerCase() + .replace(/ /g, "_")}.webp`, + }; + }); + let variant: "user" | "streamer" | "personal" | "admin"; + if (username === loggedInUsername) variant = "personal"; + else if (streamData.title) variant = "streamer"; + // else if (data.isAdmin) variant = "admin"; + else variant = "user"; + setUserPageVariant(variant); + }) + .catch((err) => console.error("Error fetching stream data:", err)); + } }) .catch((err) => console.error("Error fetching profile data:", err)); + + // Check if the *logged-in* user is following this user + if (loggedInUsername && username) checkFollowStatus(username); }, [username]); if (!profileData) { @@ -45,12 +103,17 @@ const UserPage: React.FC = () => {
); } - return ( -
+
-
-
+
+
{/* Profile Section - Left Third */}
{

{profileData.username}

- {userPageVariant.toUpperCase()} + + {userPageVariant.toUpperCase()} + -
- - {profileData.followerCount.toLocaleString()} followers - - {profileData.isPartnered && ( - - Partner - - )} -
+ {/* Follower Count */} + {userPageVariant === "streamer" && ( + <> +
+ + {profileData.followerCount.toLocaleString()} followers + + {profileData.isPartnered && ( + + Partner + + )} +
- + {/* (Un)Follow Button */} + {!isFollowing ? ( + + ) : ( + + )} + + )}
{/* Bio Section */} -
+

About {profileData.username}

@@ -109,34 +192,47 @@ const UserPage: React.FC = () => { {profileData.bio}

- - {/* Additional Stats */} -
-
-
-
0
-
Total Views
-
-
-
0
-
Following
-
-
-
{/* Content Section */}
-

Past Broadcasts

-
- No past broadcasts found -
+ {userPageVariant === "streamer" && ( + <> + {/* ↓↓ Current Stream ↓↓ */} + {profileData.isLive && ( +
+

+ Currently Live! +

+ { + navigate(`/${profileData.username}`); + }} + /> +
+ )} + {/* ↓↓ VODS ↓↓ */} +
+

Past Broadcasts

+
+ No past broadcasts found +
+
+ + )}
+ {showAuthModal && setShowAuthModal(false)} />}
); }; diff --git a/frontend/src/pages/VideoPage.tsx b/frontend/src/pages/VideoPage.tsx index dc2f646..c42ef2e 100644 --- a/frontend/src/pages/VideoPage.tsx +++ b/frontend/src/pages/VideoPage.tsx @@ -3,9 +3,12 @@ import Navbar from "../components/Layout/Navbar"; import Button, { ToggleButton } from "../components/Layout/Button"; import ChatPanel from "../components/Video/ChatPanel"; import { useNavigate, useParams } from "react-router-dom"; +import { useAuthModal } from "../hooks/useAuthModal"; import { useAuth } from "../context/AuthContext"; +import { useFollow } from "../hooks/useFollow"; import VideoPlayer from "../components/Video/VideoPlayer"; import { SocketProvider } from "../context/SocketContext"; +import AuthModal from "../components/Auth/AuthModal"; interface VideoPageProps { streamerId: number; @@ -24,6 +27,9 @@ const VideoPage: React.FC = ({ streamerId }) => { const [streamData, setStreamData] = useState(); const [viewerCount, setViewerCount] = useState(0); const [isChatOpen, setIsChatOpen] = useState(true); + const { isFollowing, checkFollowStatus, followUser, unfollowUser } = + useFollow(); + const { showAuthModal, setShowAuthModal } = useAuthModal(); // const [showCheckout, setShowCheckout] = useState(false); // const showReturn = window.location.search.includes("session_id"); const navigate = useNavigate(); @@ -40,6 +46,7 @@ const VideoPage: React.FC = ({ streamerId }) => { // document.body.style.overflow = "unset"; // }; // }, [showCheckout]); + useEffect(() => { // Fetch stream data for this streamer fetch(`/api/streams/${streamerId}/data`).then((res) => { @@ -57,6 +64,9 @@ const VideoPage: React.FC = ({ streamerId }) => { categoryName: data.category_name, }; setStreamData(transformedData); + + // Check if the logged-in user is following this streamer + if (isLoggedIn) checkFollowStatus(data.username); }) .catch((error) => { console.error("Error fetching stream data:", error); @@ -64,6 +74,21 @@ const VideoPage: React.FC = ({ streamerId }) => { }); }, [streamerId]); + // Keyboard shortcut to toggle chat + useEffect(() => { + const handleKeyPress = (e: KeyboardEvent) => { + if (e.key === "c") { + setIsChatOpen((prev) => !prev); + } + }; + + document.addEventListener("keydown", handleKeyPress); + + return () => { + document.removeEventListener("keydown", handleKeyPress); + }; + }, []); + const toggleChat = () => { setIsChatOpen((prev) => !prev); }; @@ -86,9 +111,11 @@ const VideoPage: React.FC = ({ streamerId }) => { {isChatOpen ? "Hide Chat" : "Show Chat"} + + Press C = ({ streamerId }) => {
-

+

{streamData ? streamData.streamTitle : "Loading..."}

-
-
+
-
- Viewer Count: - {viewerCount} -
-
- Started At: - - {streamData - ? new Date(streamData.startTime).toLocaleString() - : "Loading..."} - -
-
- Category ID: - - {streamData ? streamData.categoryName : "Loading..."} - -
+ + + {/* (Un)Follow Button */} + {!isFollowing ? ( + + ) : ( + + )} +
+
+ Viewers + {viewerCount} +
+
+ Started + + {streamData + ? new Date(streamData.startTime).toLocaleString() + : "Loading..."} + +
+
+ Category + {streamData ? streamData.categoryName : "Loading..."}
- {isLoggedIn && ( - - )}
+ {isLoggedIn && ( + + )}
{/* {showCheckout && setShowCheckout(false)} />} */} {/* {showReturn && } */} + {showAuthModal && setShowAuthModal(false)} />}
); diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 0097773..bfeeb56 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -20,6 +20,7 @@ export default { backgroundImage: { logo: "linear-gradient(45deg, #60A5FA, #8B5CF6, #EC4899, #FACC15,#60A5FA, #8B5CF6, #EC4899, #FACC15)", + 'gradient-radial': 'radial-gradient(circle, var(--tw-gradient-stops))', }, keyframes: { diff --git a/web_server/blueprints/streams.py b/web_server/blueprints/streams.py index f795c56..660a9a0 100644 --- a/web_server/blueprints/streams.py +++ b/web_server/blueprints/streams.py @@ -95,7 +95,7 @@ def get_following_categories_streams(): @stream_bp.route('/user//status') def get_user_live_status(username): """ - Returns a streamer's status, if they are live or not and their most recent stream (their current stream if live) + Returns a streamer's status, if they are live or not and their most recent stream (as a vod) (their current stream if live) """ user_id = get_user_id(username) diff --git a/web_server/blueprints/user.py b/web_server/blueprints/user.py index 6662d58..e8354bc 100644 --- a/web_server/blueprints/user.py +++ b/web_server/blueprints/user.py @@ -10,7 +10,7 @@ r = redis.from_url(redis_url, decode_responses=True) user_bp = Blueprint("user", __name__) @user_bp.route('/user/') -def get_user_data(username): +def get_user_data(username: str): """ Returns a given user's data """ @@ -45,43 +45,38 @@ def user_subscription_expiration(subscribed_id: int): return jsonify({"remaining_time": remaining_time}) ## Follow Routes -@user_bp.route('/user//follows/') -def user_following(user_id: int, followed_id: int): +@user_bp.route('/user/following/') +def user_following(followed_username: str): """ - Checks to see if user is following a streamer + Checks to see if user is following another streamer """ + user_id = session.get("user_id") + followed_id = get_user_id(followed_username) if is_following(user_id, followed_id): return jsonify({"following": True}) return jsonify({"following": False}) @login_required -@user_bp.route('/user/follow/') -def follow_user(username): +@user_bp.route('/user/follow/') +def follow_user(target_user_id: int): """ Follows a user """ user_id = session.get("user_id") - following_id = get_user_id(username) - if follow(user_id, following_id): - return jsonify({"success": True, - "already_following": False}) - return jsonify({"success": True, - "already_following": True}) + return follow(user_id, target_user_id) @login_required -@user_bp.route('/user/unfollow/') -def unfollow_user(username): +@user_bp.route('/user/unfollow/') +def unfollow_user(target_user_id: int): """ Unfollows a user """ user_id = session.get("user_id") - followed_id = get_user_id(username) - unfollow(user_id, followed_id) - return jsonify({"success": True}) + return unfollow(user_id, target_user_id) @login_required @user_bp.route('/user/following') -def get_followed_streamers_(): +def get_followed_streamers(): """ Queries DB to get a list of followed streamers """ diff --git a/web_server/utils/user_utils.py b/web_server/utils/user_utils.py index cb55c9f..ca5d5eb 100644 --- a/web_server/utils/user_utils.py +++ b/web_server/utils/user_utils.py @@ -88,32 +88,33 @@ def is_following(user_id: int, followed_id: int) -> bool: """, (user_id, followed_id)) return bool(result) -def follow(user_id: int, following_id: int): +def follow(user_id: int, followed_id: int): """ - Follows following_id user from user_id user + Follows followed_id user from user_id user """ - if not is_following(user_id, following_id): - with Database() as db: - db.execute(""" - INSERT INTO follows (user_id, followed_id) - VALUES(?,?); - """, (user_id, following_id)) - return True - return False + if is_following(user_id, followed_id): + return {"success": False, "error": "Already following user"}, 400 + + with Database() as db: + db.execute(""" + INSERT INTO follows (user_id, followed_id) + VALUES(?,?); + """, (user_id, followed_id)) + return {"success": True} def unfollow(user_id: int, followed_id: int): """ - Unfollows follow_id user from user_id user + Unfollows followed_id user from user_id user """ - if is_following(user_id, followed_id): - with Database() as db: - db.execute(""" - DELETE FROM follows - WHERE user_id = ? - AND followed_id = ? - """, (user_id, followed_id)) - return True - return False + if not is_following(user_id, followed_id): + return {"success": False, "error": "Not following user"}, 400 + with Database() as db: + db.execute(""" + DELETE FROM follows + WHERE user_id = ? + AND followed_id = ? + """, (user_id, followed_id)) + return {"success": True} def subscription_expiration(user_id: int, subscribed_id: int) -> int: @@ -197,11 +198,11 @@ def get_followed_streamers(user_id: int) -> Optional[List[dict]]: def get_user(user_id: int) -> Optional[dict]: """ - Returns username, bio, number of followers, and if user is partnered from user_id + Returns information about a user from user_id """ with Database() as db: data = db.fetchone(""" - SELECT username, bio, num_followers, is_partnered FROM users + SELECT user_id, username, bio, num_followers, is_partnered, is_live FROM users WHERE user_id = ?; """, (user_id,)) return data \ No newline at end of file