diff --git a/frontend/src/components/Auth/ForgotPasswordForm.tsx b/frontend/src/components/Auth/ForgotPasswordForm.tsx index 4cae486..baf718c 100644 --- a/frontend/src/components/Auth/ForgotPasswordForm.tsx +++ b/frontend/src/components/Auth/ForgotPasswordForm.tsx @@ -71,7 +71,7 @@ const ForgotPasswordForm: React.FC = ({ onSubmit }) => { return (
-

+

Forgot Password

diff --git a/frontend/src/components/Auth/LoginForm.tsx b/frontend/src/components/Auth/LoginForm.tsx index cabad08..9170809 100644 --- a/frontend/src/components/Auth/LoginForm.tsx +++ b/frontend/src/components/Auth/LoginForm.tsx @@ -154,7 +154,7 @@ const LoginForm: React.FC = ({ onSubmit, onForgotPassword }) => { onClick={onForgotPassword} > - + Forgot Password diff --git a/frontend/src/components/Auth/OAuth.tsx b/frontend/src/components/Auth/OAuth.tsx index ed2c2e7..422ecda 100644 --- a/frontend/src/components/Auth/OAuth.tsx +++ b/frontend/src/components/Auth/OAuth.tsx @@ -20,7 +20,7 @@ export default function GoogleLogin() { alt="Google logo" className="w-[2em] h-[2em] mr-2" /> - + Sign in with Google diff --git a/frontend/src/components/Input/Button.tsx b/frontend/src/components/Input/Button.tsx index 6e64a2c..6245178 100644 --- a/frontend/src/components/Input/Button.tsx +++ b/frontend/src/components/Input/Button.tsx @@ -15,7 +15,7 @@ const Button: React.FC = ({ return ( + ); +}; + +export default FollowUserButton; diff --git a/frontend/src/components/Layout/ListItem.tsx b/frontend/src/components/Layout/ListItem.tsx index a5a7cf8..dca0440 100644 --- a/frontend/src/components/Layout/ListItem.tsx +++ b/frontend/src/components/Layout/ListItem.tsx @@ -2,13 +2,14 @@ import React from "react"; import { StreamType } from "../../types/StreamType"; import { CategoryType } from "../../types/CategoryType"; import { UserType } from "../../types/UserType"; +import { VodType } from "../../types/VodType"; // Base props that all item types share interface BaseListItemProps { onItemClick?: () => void; extraClasses?: string; } - + // Stream item component interface StreamListItemProps extends BaseListItemProps, Omit {} @@ -124,6 +125,52 @@ const UserListItem: React.FC = ({ ); }; +// VODs item component +interface VodListItemProps extends BaseListItemProps, Omit {} + +const VodListItem: React.FC = ({ + title, + streamer, + datetime, + category, + length, + views, + thumbnail, + url, + onItemClick, + extraClasses = "", +}) => { + return ( +
+
window.open(url, "_blank")} + > +
+ {thumbnail ? ( + {title} + ) : ( +
+ )} +
+
+

+ {title} +

+

{streamer}

+

{category}

+

{new Date(datetime).toLocaleDateString()} | {length} mins

+

{views} views

+
+
+
+ ); +}; + // Legacy wrapper component for backward compatibility export interface ListItemProps { type: "stream" | "category" | "user"; @@ -138,4 +185,4 @@ export interface ListItemProps { isLive?: boolean; } -export { StreamListItem, CategoryListItem, UserListItem }; \ No newline at end of file +export { StreamListItem, CategoryListItem, UserListItem, VodListItem }; \ No newline at end of file diff --git a/frontend/src/components/Layout/ListRow.tsx b/frontend/src/components/Layout/ListRow.tsx index f61293a..0f7af82 100644 --- a/frontend/src/components/Layout/ListRow.tsx +++ b/frontend/src/components/Layout/ListRow.tsx @@ -10,16 +10,17 @@ import React, { } from "react"; import { useNavigate } from "react-router-dom"; import "../../assets/styles/listRow.css"; -import { StreamListItem, CategoryListItem, UserListItem } from "./ListItem"; +import { StreamListItem, CategoryListItem, UserListItem, VodListItem } from "./ListItem"; import { StreamType } from "../../types/StreamType"; import { CategoryType } from "../../types/CategoryType"; import { UserType } from "../../types/UserType"; +import { VodType } from "../../types/VodType" -type ItemType = StreamType | CategoryType | UserType; +type ItemType = StreamType | CategoryType | UserType | VodType; interface ListRowProps { variant?: "default" | "search"; - type: "stream" | "category" | "user"; + type: "stream" | "category" | "user" | "vod"; title?: string; description?: string; items: ItemType[]; @@ -100,6 +101,9 @@ const ListRow = forwardRef((props, ref) => { const isUserType = (item: ItemType): item is UserType => item.type === "user"; + const isVodType = (item: ItemType): item is VodType => + item.type === "vod"; + return (
((props, ref) => { /> ); } + else if (type === "vod" && isVodType(item)) { + return ( + window.open(item.url, "_blank")} + extraClasses={itemExtraClasses} + /> + ); + } return null; })} diff --git a/frontend/src/hooks/useContent.ts b/frontend/src/hooks/useContent.ts index 5e99aaf..3113994 100644 --- a/frontend/src/hooks/useContent.ts +++ b/frontend/src/hooks/useContent.ts @@ -4,8 +4,28 @@ import { useAuth } from "../context/AuthContext"; import { StreamType } from "../types/StreamType"; import { CategoryType } from "../types/CategoryType"; import { UserType } from "../types/UserType"; +import { VodType } from "../types/VodType" import { getCategoryThumbnail } from "../utils/thumbnailUtils"; +// Process API data into our VodType structure +const processVodData = (data: any[]): VodType[] => { + + return data.map((vod) => ({ + type: "vod", + id: vod.id, // Ensure this matches API response + title: vod.title, + streamer: vod.streamer, // Ensure backend sends streamer name or ID + datetime: new Date(vod.datetime).toLocaleString(), + category: vod.category, + length: vod.length, + views: vod.views, + url: vod.url, + thumbnail: "../../images/category_thumbnails/abstract.webp", + })); +}; + + + // Helper function to process API data into our consistent types const processStreamData = (data: any[]): StreamType[] => { return data.map((stream) => ({ @@ -20,6 +40,7 @@ const processStreamData = (data: any[]): StreamType[] => { }; const processCategoryData = (data: any[]): CategoryType[] => { + console.log("Raw API VOD Data:", data); // Debugging return data.map((category) => ({ type: "category", id: category.category_id, @@ -115,9 +136,29 @@ export function useCategories(customUrl?: string): { [isLoggedIn, customUrl] ); + console.log("Fetched Cat Data:", data); // Debugging + + return { categories: data, isLoading, error }; } +export function useVods(customUrl?: string): { + vods: VodType[]; + isLoading: boolean; + error: string | null +} { + const url = customUrl || "api/vods/all"; + const { data, isLoading, error } = useFetchContent( + url, + processVodData, + [customUrl] + ); + + + return { vods: data, isLoading, error }; +} + + export function useUsers(customUrl?: string): { users: UserType[]; isLoading: boolean; diff --git a/frontend/src/pages/CategoryPage.tsx b/frontend/src/pages/CategoryPage.tsx index 5657677..a02bb7b 100644 --- a/frontend/src/pages/CategoryPage.tsx +++ b/frontend/src/pages/CategoryPage.tsx @@ -97,7 +97,7 @@ const CategoryPage: React.FC = () => { = ({ extraClasses = "" const [followedCategories, setFollowedCategories] = useState([]); const { categoryName } = useParams<{ categoryName: string }>(); const { checkCategoryFollowStatus, followCategory, unfollowCategory } = useCategoryFollow(); + useEffect(() => { if (categoryName) checkCategoryFollowStatus(categoryName); @@ -32,7 +34,7 @@ const FollowedCategories: React.FC = ({ extraClasses = "" const fetchFollowedCategories = async () => { try { - const response = await fetch("/api/categories/following"); + const response = await fetch("/api/categories/your_categories"); if (!response.ok) throw new Error("Failed to fetch followed categories"); const data = await response.json(); setFollowedCategories(data); @@ -52,7 +54,7 @@ const FollowedCategories: React.FC = ({ extraClasses = "" className={`top-0 left-0 w-screen h-screen overflow-x-hidden flex flex-col bg-[var(--sideBar-bg)] text-[var(--sideBar-text)] text-center overflow-y-auto scrollbar-hide transition-all duration-500 ease-in-out ${extraClasses}`} > {/* Followed Categories */} -
+
{followedCategories.map((category) => { return (
= ({ extraClasses = "" }) => { - const { showSideBar, setShowSideBar } = useSidebar(); +const Following: React.FC = ({ extraClasses = "" }) => { const navigate = useNavigate(); - const { username, isLoggedIn } = useAuth(); + const { isLoggedIn } = useAuth(); const [followedStreamers, setFollowedStreamers] = useState([]); + const [followingStatus, setFollowingStatus] = useState<{ [key: number]: boolean }>({}); // Store follow status for each streamer + + const { isFollowing, checkFollowStatus, followUser, unfollowUser } = + useFollow(); - // Fetch followed streamers useEffect(() => { const fetchFollowedStreamers = async () => { try { const response = await fetch("/api/user/following"); if (!response.ok) throw new Error("Failed to fetch followed streamers"); const data = await response.json(); - setFollowedStreamers(data.streamers || []); + setFollowedStreamers(data); + + const updatedStatus: { [key: number]: boolean } = {}; + for (const streamer of data || []) { + const status = await checkFollowStatus(streamer.username); + updatedStatus[streamer.user_id] = Boolean(status); + } + setFollowingStatus(updatedStatus); + + console.log("Fetched Follow Status:", updatedStatus); // Log the status } catch (error) { console.error("Error fetching followed streamers:", error); } @@ -40,103 +50,51 @@ const Following: React.FC = ({ extraClasses = "" }) => { } }, [isLoggedIn]); - // Handle sidebar toggle - const handleSideBar = () => { - setShowSideBar(!showSideBar); + + const handleFollowToggle = async (userId: number) => { + const isCurrentlyFollowing = followingStatus[userId]; + + if (isCurrentlyFollowing) { + await unfollowUser(userId); + } else { + await followUser(userId); + } + + // Update local state for this specific streamer + setFollowingStatus((prev) => ({ + ...prev, + [userId]: !isCurrentlyFollowing, // Toggle based on previous state + })); }; return ( - <> - {/* Sidebar Toggle Button */} - - - - {showSideBar && ( - - Press S - - )} - - - {/* Sidebar Container */} +