Merge branch 'main' of https://github.com/john-david3/cs3305-team11
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
export const paths = {
|
||||
pfps: "",
|
||||
category_thumbnails: "",
|
||||
icons: "",
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.5.0",
|
||||
"version": "0.15.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --config vite.config.dev.ts",
|
||||
|
||||
@@ -110,6 +110,7 @@ interface VodListItemProps extends BaseListItemProps, Omit<VodType, "type"> {
|
||||
}
|
||||
|
||||
const VodListItem: React.FC<VodListItemProps> = ({
|
||||
vod_id,
|
||||
title,
|
||||
username,
|
||||
category_name,
|
||||
@@ -137,7 +138,7 @@ const VodListItem: React.FC<VodListItemProps> = ({
|
||||
|
||||
<div className="p-3">
|
||||
<h3 className="font-semibold text-lg text-white truncate max-w-full">{title}</h3>
|
||||
<p className="text-sm text-gray-300">{username}</p>
|
||||
{variant != "vodDashboard" && <p className="text-sm text-gray-300">{username}</p>}
|
||||
<p className="text-sm text-gray-400">{category_name}</p>
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<p className="text-xs text-gray-500">{datetime}</p>
|
||||
@@ -147,20 +148,21 @@ const VodListItem: React.FC<VodListItemProps> = ({
|
||||
</div>
|
||||
{variant === "vodDashboard" && (
|
||||
<div className="flex justify-evenly items-stretch rounded-b-lg">
|
||||
<button
|
||||
{/* <button
|
||||
className="flex justify-around w-full h-full bg-black/50 hover:bg-black/80 p-2 mx-1 font-semibold rounded-full border border-transparent hover:border-white"
|
||||
onClick={() => console.log("Publish")}
|
||||
>
|
||||
<UploadIcon />
|
||||
Publish
|
||||
</button>
|
||||
<button
|
||||
</button> */}
|
||||
<a
|
||||
className="flex justify-around w-full h-full bg-black/50 hover:bg-black/80 p-2 mx-1 font-semibold rounded-full border border-transparent hover:border-white"
|
||||
onClick={() => console.log("Download")}
|
||||
href={`/vods/${username}/${vod_id}.mp4`}
|
||||
download={`${username}_vod_${vod_id}.mp4`}
|
||||
>
|
||||
<DownloadIcon />
|
||||
Download
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -164,7 +164,6 @@ const ListRow = forwardRef<ListRowRef, ListRowProps>((props, ref) => {
|
||||
username={item.username}
|
||||
isLive={item.isLive}
|
||||
viewers={item.viewers}
|
||||
thumbnail={item.thumbnail}
|
||||
onItemClick={() => onItemClick(item.username)}
|
||||
extraClasses={itemExtraClasses}
|
||||
/>
|
||||
|
||||
@@ -141,7 +141,7 @@ const Sidebar: React.FC<SideBarProps> = ({ extraClasses = "" }) => {
|
||||
return (
|
||||
<div
|
||||
key={`${sidebarId.current}-category-${category.category_id}`}
|
||||
className="group relative flex flex-col items-center justify-center h-full max-h-[50px] border border-[--text-color]
|
||||
className="group relative flex flex-col items-center justify-center w-full h-full max-h-[50px] border border-[--text-color]
|
||||
rounded-lg overflow-hidden hover:shadow-lg transition-all text-white hover:text-purple-500 cursor-pointer"
|
||||
onClick={() => (window.location.href = `/category/${category.category_name}`)}
|
||||
>
|
||||
|
||||
@@ -225,13 +225,13 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
|
||||
const handleEndStream = async () => {
|
||||
console.log("Ending stream...");
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("key", streamData.stream_key);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/end_stream", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ key: streamData.stream_key }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -375,7 +375,9 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
|
||||
streamCategory={streamData.category_name || "Category"}
|
||||
viewers={streamData.viewer_count}
|
||||
thumbnail={thumbnailPreview.url || ""}
|
||||
onItemClick={() => {}}
|
||||
onItemClick={() => {
|
||||
window.open(`/${username}`, "_blank");
|
||||
}}
|
||||
extraClasses="max-w-[20vw]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ const processVodData = (data: any[]): VodType[] => {
|
||||
|
||||
// Helper function to process API data into our consistent types
|
||||
const processStreamData = (data: any[]): StreamType[] => {
|
||||
if (!data || data.length === 0 || !data[0] || !data[0].user_id) return [];
|
||||
return data.map((stream) => ({
|
||||
type: "stream",
|
||||
id: stream.user_id,
|
||||
@@ -76,8 +77,9 @@ export function useFetchContent<T>(
|
||||
throw new Error(`Error fetching data: ${response.status}`);
|
||||
}
|
||||
|
||||
const rawData = await response.json();
|
||||
const processedData = processor(rawData);
|
||||
const rawData = await response.json();
|
||||
let processedData = processor(Array.isArray(rawData) ? rawData : (rawData ? [rawData] : []));
|
||||
console.log("processedData", processedData);
|
||||
setData(processedData);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
@@ -126,7 +128,7 @@ export function useVods(customUrl?: string): {
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
} {
|
||||
const url = customUrl || "api/vods/all"; //TODO: Change this to the correct URL or implement it
|
||||
const url = customUrl || "api/vods/all";
|
||||
const { data, isLoading, error } = useFetchContent<VodType>(url, processVodData, [customUrl]);
|
||||
|
||||
return { vods: data, isLoading, error };
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
export function useSameUser({ username }: { username: string | undefined }) {
|
||||
const [isSame, setIsSame] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStatus = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/user/same/${username}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to validate user");
|
||||
}
|
||||
const data = await response.json();
|
||||
setIsSame(data.same);
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchStatus();
|
||||
}, []);
|
||||
|
||||
return isSame;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import AuthModal from "../components/Auth/AuthModal";
|
||||
import { useAuthModal } from "../hooks/useAuthModal";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
@@ -10,8 +10,8 @@ import DynamicPageContent from "../components/Layout/DynamicPageContent";
|
||||
import LoadingScreen from "../components/Layout/LoadingScreen";
|
||||
import { StreamListItem } from "../components/Layout/ListItem";
|
||||
import { EditIcon } from "lucide-react";
|
||||
import { getCategoryThumbnail } from "../utils/thumbnailUtils";
|
||||
import { useSameUser } from "../hooks/useSameUser";
|
||||
import ListRow from "../components/Layout/ListRow";
|
||||
import { useStreams, useVods } from "../hooks/useContent";
|
||||
|
||||
interface UserProfileData {
|
||||
id: number;
|
||||
@@ -20,22 +20,38 @@ interface UserProfileData {
|
||||
followerCount: number;
|
||||
isPartnered: boolean;
|
||||
isLive: boolean;
|
||||
currentStreamTitle?: string;
|
||||
currentStreamCategory?: string;
|
||||
currentStreamViewers?: number;
|
||||
currentStreamStartTime?: string;
|
||||
currentStreamThumbnail?: string;
|
||||
}
|
||||
|
||||
const UserPage: React.FC = () => {
|
||||
const [userPageVariant, setUserPageVariant] = useState<"personal" | "streamer" | "user" | "admin">("user");
|
||||
const [userPageVariant, setUserPageVariant] = useState<"personal" | "user" | "admin">("user");
|
||||
const [profileData, setProfileData] = useState<UserProfileData>();
|
||||
const { isFollowing, checkFollowStatus, followUser, unfollowUser } = useFollow();
|
||||
const { showAuthModal, setShowAuthModal } = useAuthModal();
|
||||
const { username: loggedInUsername } = useAuth();
|
||||
const { username } = useParams();
|
||||
const isUser = useSameUser({ username });
|
||||
const { vods } = useVods(`/api/vods/${username}`);
|
||||
const navigate = useNavigate();
|
||||
const { streams } = useStreams(`/api/streams/${username}/data`);
|
||||
const currentStream = streams[0];
|
||||
|
||||
const fetchProfileData = useCallback(async () => {
|
||||
try {
|
||||
// Profile data
|
||||
const profileResponse = await fetch(`/api/user/${username}`);
|
||||
const profileData = await profileResponse.json();
|
||||
setProfileData({
|
||||
id: profileData.user_id,
|
||||
username: profileData.username,
|
||||
bio: profileData.bio || "This user hasn't written a bio yet.",
|
||||
followerCount: profileData.num_followers || 0,
|
||||
isPartnered: profileData.isPartnered || false,
|
||||
isLive: profileData.is_live,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error fetching profile data:", err);
|
||||
window.location.href = "/404";
|
||||
}
|
||||
}, [username]);
|
||||
|
||||
// Saves uploaded image as profile picture for the user
|
||||
const saveUploadedImage = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -62,58 +78,20 @@ const UserPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Check if the current user is the currently logged-in user
|
||||
useEffect(() => {
|
||||
// Fetch user profile data
|
||||
fetch(`/api/user/${username}`)
|
||||
.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: "",
|
||||
});
|
||||
if (username === loggedInUsername) setUserPageVariant("personal");
|
||||
// else if (data.isAdmin) setUserPageVariant("admin");
|
||||
else setUserPageVariant("user");
|
||||
|
||||
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: getCategoryThumbnail(streamData.category_name, streamData.thumbnail),
|
||||
};
|
||||
});
|
||||
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);
|
||||
navigate("/404");
|
||||
});
|
||||
|
||||
// Check if the *logged-in* user is following this user
|
||||
if (loggedInUsername && username) checkFollowStatus(username);
|
||||
}, [username]);
|
||||
}, [username, loggedInUsername, checkFollowStatus]);
|
||||
|
||||
// Fetch user profile data
|
||||
useEffect(() => {
|
||||
if (!username) return;
|
||||
fetchProfileData();
|
||||
}, [fetchProfileData]);
|
||||
|
||||
if (!profileData) return <LoadingScreen />;
|
||||
|
||||
@@ -148,16 +126,21 @@ const UserPage: React.FC = () => {
|
||||
} inset-0 z-20`}
|
||||
style={{ boxShadow: "var(--user-pfp-border-shadow)" }}
|
||||
>
|
||||
<label className={`w-full h-full ${isUser ? "group cursor-pointer" : ""} overflow-visible rounded-full`}>
|
||||
<label
|
||||
className={`w-full h-full ${userPageVariant === "personal" ? "group cursor-pointer" : ""} overflow-visible rounded-full`}
|
||||
>
|
||||
{/* If user is live then displays a live div */}
|
||||
{profileData.isLive && (
|
||||
{profileData.isLive ? (
|
||||
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-[#ff0000] text-white text-sm font-bold py-1 sm:px-5 px-4 z-30 flex items-center justify-center rounded-tr-xl rounded-bl-xl rounded-tl-xl rounded-br-xl">
|
||||
LIVE
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<img
|
||||
src={`/user/${profileData.username}/profile_picture`}
|
||||
onError={(e) => {
|
||||
console.log("no error")
|
||||
e.currentTarget.src = "/images/pfps/default.png";
|
||||
e.currentTarget.onerror = null;
|
||||
}}
|
||||
@@ -166,7 +149,7 @@ const UserPage: React.FC = () => {
|
||||
/>
|
||||
|
||||
{/* If current user is the profile user then allow profile picture swap */}
|
||||
{isUser && (
|
||||
{userPageVariant === "personal" && (
|
||||
<div className="absolute top-0 bottom-0 left-0 right-0 m-auto flex items-center justify-center opacity-0 z-50 group-hover:opacity-100 transition-opacity duration-200">
|
||||
<EditIcon size={75} className="text-white bg-black/50 p-1 rounded-3xl" />
|
||||
<input type="file" className="hidden" onChange={saveUploadedImage} accept="image/*" />
|
||||
@@ -181,34 +164,32 @@ const UserPage: React.FC = () => {
|
||||
</h1>
|
||||
|
||||
{/* Follower Count */}
|
||||
{userPageVariant === "streamer" && (
|
||||
<>
|
||||
<div className="flex items-center space-x-2 mb-6">
|
||||
<span className="text-gray-400">{profileData.followerCount.toLocaleString()} followers</span>
|
||||
{profileData.isPartnered && <span className="bg-purple-600 text-white text-sm px-2 py-1 rounded">Partner</span>}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mb-6">
|
||||
<span className="text-gray-400">{profileData.followerCount.toLocaleString()} followers</span>
|
||||
{profileData.isPartnered && <span className="bg-purple-600 text-white text-sm px-2 py-1 rounded">Partner</span>}
|
||||
</div>
|
||||
|
||||
{/* (Un)Follow Button */}
|
||||
{!isFollowing ? (
|
||||
<Button
|
||||
extraClasses="w-full bg-purple-700 hover:bg-[#28005e]"
|
||||
onClick={() => followUser(profileData.id, setShowAuthModal)}
|
||||
>
|
||||
Follow
|
||||
</Button>
|
||||
) : (
|
||||
<Button extraClasses="w-full bg-[#a80000] z-50" onClick={() => unfollowUser(profileData?.id, setShowAuthModal)}>
|
||||
Unfollow
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
{/* (Un)Follow Button */}
|
||||
{userPageVariant != "personal" ? (
|
||||
!isFollowing ? (
|
||||
<Button
|
||||
extraClasses="w-full bg-purple-700 z-50 hover:bg-[#28005e]"
|
||||
onClick={() => followUser(profileData.id, setShowAuthModal)}
|
||||
>
|
||||
Follow
|
||||
</Button>
|
||||
) : (
|
||||
<Button extraClasses="w-full bg-[#a80000] z-50" onClick={() => unfollowUser(profileData?.id, setShowAuthModal)}>
|
||||
Unfollow
|
||||
</Button>
|
||||
)
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="settings"
|
||||
className="col-span-1 bg-[var(--user-sideBox)] rounded-lg p-6 grid grid-rows-[auto_1fr] text-center items-center justify-center"
|
||||
>
|
||||
{/* Bio */}
|
||||
<div className="col-span-1 bg-[var(--user-sideBox)] rounded-lg p-6 grid grid-rows-[auto_1fr] text-center items-center justify-center">
|
||||
{/* User Type (e.g., "USER") */}
|
||||
<small className="text-green-400">{userPageVariant.toUpperCase()}</small>
|
||||
|
||||
@@ -223,44 +204,42 @@ const UserPage: React.FC = () => {
|
||||
id="content"
|
||||
className="col-span-2 bg-[var(--user-contentBox)] rounded-lg p-6 grid grid-rows-[auto_1fr] text-center items-center justify-center"
|
||||
>
|
||||
{userPageVariant === "streamer" && (
|
||||
<>
|
||||
{profileData.isLive ? (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl bg-[#ff0000] border py-4 px-12 font-black mb-4 rounded-[4rem]">Currently Live!</h2>
|
||||
<StreamListItem
|
||||
id={profileData.id}
|
||||
title={profileData.currentStreamTitle || ""}
|
||||
streamCategory=""
|
||||
username=""
|
||||
viewers={profileData.currentStreamViewers || 0}
|
||||
thumbnail={profileData.currentStreamThumbnail}
|
||||
onItemClick={() => {
|
||||
navigate(`/${profileData.username}`);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<h1>Currently not live</h1>
|
||||
)}
|
||||
|
||||
{/* ↓↓ VODS ↓↓ */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">Past Broadcasts</h2>
|
||||
<div className="text-gray-400 rounded-none">No past broadcasts found</div>
|
||||
</div>
|
||||
</>
|
||||
{/* Stream */}
|
||||
{currentStream && (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl bg-[#ff0000] border py-4 px-12 font-black mb-4 rounded-[4rem]">Currently Live!</h2>
|
||||
<StreamListItem
|
||||
id={profileData.id}
|
||||
title={currentStream.title || ""}
|
||||
streamCategory=""
|
||||
username=""
|
||||
viewers={currentStream.viewers || 0}
|
||||
thumbnail={currentStream.thumbnail}
|
||||
onItemClick={() => {
|
||||
navigate(`/${profileData.username}`);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{userPageVariant === "user" && (
|
||||
<>
|
||||
{/* ↓↓ VODS ↓↓ */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">Past Broadcasts</h2>
|
||||
<div className="text-gray-400 rounded-none">No past broadcasts found</div>
|
||||
</div>
|
||||
</>
|
||||
{/* VODs */}
|
||||
{vods.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4"></h2>
|
||||
<ListRow
|
||||
type="vod"
|
||||
title={`Past Broadcasts (${vods.length})`}
|
||||
items={vods}
|
||||
onItemClick={(vod) => {
|
||||
console.log("VOD Clicked:", vod);
|
||||
}}
|
||||
extraClasses="w-fit max-w-[40vw] py-0 mt-0"
|
||||
amountForScroll={2}
|
||||
itemExtraClasses="w-[15vw]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* No Content */}
|
||||
{vods.length === 0 && currentStream && <h2 className="text-2xl font-bold mb-4">No Content Made Yet</h2>}
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -294,7 +273,7 @@ const UserPage: React.FC = () => {
|
||||
onMouseEnter={(e) => (e.currentTarget.style.boxShadow = "var(--follow-shadow)")}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}
|
||||
>
|
||||
<button onClick={() => navigate(`/user/${username}/yourCategories`)}>Categories</button>
|
||||
<button onClick={() => navigate(`/user/${username}/followedCategories`)}>Categories</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,5 +6,4 @@ export interface UserType {
|
||||
username: string;
|
||||
isLive: boolean;
|
||||
viewers: number;
|
||||
thumbnail?: string;
|
||||
}
|
||||
@@ -20,11 +20,11 @@ export default defineConfig({
|
||||
target: "http://localhost:8080",
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
},
|
||||
"/stream": {
|
||||
target: "http://localhost:8080",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
"/stream": {
|
||||
target: "http://localhost:8080",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/images": {
|
||||
target: "http://localhost:8080",
|
||||
changeOrigin: true,
|
||||
@@ -33,6 +33,10 @@ export default defineConfig({
|
||||
target: "http://localhost:8080",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/vods": {
|
||||
target: "http://localhost:8080",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
||||
Reference in New Issue
Block a user