From af86993b7a1855cd647ddb6b736b90c9d4e086ef Mon Sep 17 00:00:00 2001 From: Benjamin Lefebvre Date: Mon, 12 May 2025 12:25:31 -0400 Subject: [PATCH 1/4] Implement ContentList Component for FeedContent --- frontend/src/components/feed/ContentList.tsx | 174 +++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 frontend/src/components/feed/ContentList.tsx diff --git a/frontend/src/components/feed/ContentList.tsx b/frontend/src/components/feed/ContentList.tsx new file mode 100644 index 0000000..7f0d3f8 --- /dev/null +++ b/frontend/src/components/feed/ContentList.tsx @@ -0,0 +1,174 @@ +import { useEffect, useState } from "react"; +import ContentTile from "../content/ContentTile"; +import { Content } from "../../models/Content"; +import { apiURL } from "../../scripts/api"; +import axios from "axios"; +import { normalizeContentDates } from "../../services/contentHelper"; +import { useAuth } from "../../hooks/useAuth"; +import ContentPreviewPopup from "../content/ContentPreviewPopup"; + +export default function ContentList({ tab }: { tab: string }) { + const [isLoading, setIsLoading] = useState(true); + const [contents, setContents] = useState([]); + + const [previewContent, setPreviewContent] = useState(null); + const [error, setError] = useState(null); + + const auth = useAuth(); + + useEffect(() => { + setIsLoading(true); + + fetchContent(); + + setIsLoading(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tab]); + + useEffect(() => { + // Function to reload the Mondiad script (ADS) + const reloadMondiadScript = () => { + const existingScript = document.querySelector( + "script[src='https://ss.mrmnd.com/native.js']" + ); + if (existingScript) { + // Remove the existing script + existingScript.remove(); + } + + // Create a new script element + const script = document.createElement("script"); + script.src = "https://ss.mrmnd.com/native.js"; + script.async = true; + + // Append the script to the document head + document.head.appendChild(script); + }; + + // Reload the script whenever the component renders + reloadMondiadScript(); + }, [contents]); + + async function fetchContent(): Promise { + try { + let response; + + switch (tab) { + case "personalized": + if (!auth.user?.uid) { + setContents([]); + return false; + } + response = await axios.get( + `${apiURL}/content/feed/${auth.user.uid}`, + { timeout: 5000, withCredentials: true } + ); + break; + case "latest": + response = await axios.get(`${apiURL}/content`, { + timeout: 5000, + }); + break; + case "trending": + response = await axios.get(`${apiURL}/content/feed/trending`, { + timeout: 5000, + }); + break; + default: + return false; + } + + if (response.data && response.data.success) { + let normalizedContent; + + switch (tab) { + case "personalized": + normalizedContent = response.data.personalizedContent.map( + (content: Content) => normalizeContentDates(content) + ); + break; + case "latest": + normalizedContent = response.data.content.map((content: Content) => + normalizeContentDates(content) + ); + break; + case "trending": + normalizedContent = response.data.trendingContent.map( + (content: Content) => normalizeContentDates(content) + ); + break; + default: + return false; + } + + setContents(normalizedContent); + return true; + } else { + setContents([]); + setError("No content found"); + } + } catch { + setContents([]); + setError("Failed to fetch content. Please try again."); + } + + setIsLoading(false); + return false; + } + + const openPreview = (content: Content) => { + setPreviewContent(content); + }; + + const closePreview = () => { + setPreviewContent(null); + }; + + return ( +
+ {/* Personalized Content Section */} +

+ {tab === "personalized" + ? "Content For You" + : tab === "latest" + ? "See the Latest Content" + : "What's Trending"} +

+ {isLoading ? ( +
+
+
+
+
+
+
+
+ ) : contents.length === 0 ? ( +

No content found

+ ) : ( +
+ {contents.map((content, index) => ( +
+ {index % 8 === 0 ? ( +
+
+
+ ) : ( + openPreview(c)} + /> + )} +
+ ))} +
+ )} + {error &&

{error}

} + {previewContent && ( + + )} +
+ ); +} From df7f43ec2e546dadf2049c5d68764bd7f7e35a64 Mon Sep 17 00:00:00 2001 From: Benjamin Lefebvre Date: Mon, 12 May 2025 12:25:54 -0400 Subject: [PATCH 2/4] Update the AuthProvider to render a Loading page until the user is fetch --- frontend/src/hooks/AuthProvider.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/hooks/AuthProvider.tsx b/frontend/src/hooks/AuthProvider.tsx index 41a4c3a..7007f96 100644 --- a/frontend/src/hooks/AuthProvider.tsx +++ b/frontend/src/hooks/AuthProvider.tsx @@ -4,17 +4,16 @@ import { User } from "../models/User"; import { fetchUser } from "../services/userService"; import axios from "axios"; import { apiURL } from "../scripts/api"; +import LoadingPage from "../pages/loading/LoadingPage"; -// TODO: REVIEW THE AUTH PROVIDER TO IMPROVE SECURITY export default function AuthProvider({ children, }: React.PropsWithChildren) { const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); const login = async (userUID: string) => { - console.log("Logging in user..."); - - getUserData(userUID); + await getUserData(userUID); }; const logout = async () => { @@ -29,7 +28,6 @@ export default function AuthProvider({ } }; - // Automatically refresh the user token and fetch user data useEffect(() => { const initializeUser = async () => { try { @@ -47,6 +45,9 @@ export default function AuthProvider({ } } catch (err) { console.error("Auto-login failed", err); + setUser(null); + } finally { + setLoading(false); } }; @@ -54,11 +55,9 @@ export default function AuthProvider({ }, []); async function getUserData(userUID: string) { - // Fetch user data from the server console.log("Fetching user data... for UID:", userUID); const userData = await fetchUser(userUID); - console.log("User data:", userData); if (!(userData instanceof Error)) { setUser(userData || null); } else { @@ -67,6 +66,11 @@ export default function AuthProvider({ } } + if (loading) { + // Render a fallback while loading + return ; + } + return ( Date: Mon, 12 May 2025 12:26:10 -0400 Subject: [PATCH 3/4] Loading Page template --- frontend/src/pages/loading/LoadingPage.tsx | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 frontend/src/pages/loading/LoadingPage.tsx diff --git a/frontend/src/pages/loading/LoadingPage.tsx b/frontend/src/pages/loading/LoadingPage.tsx new file mode 100644 index 0000000..01366ad --- /dev/null +++ b/frontend/src/pages/loading/LoadingPage.tsx @@ -0,0 +1,5 @@ +import Background from "../../components/Background"; + +export default function LoadingPage() { + return ; +} From b8ffd49d94700fa5bacc0f453b35ab34c223fce3 Mon Sep 17 00:00:00 2001 From: Benjamin Lefebvre Date: Mon, 12 May 2025 12:26:52 -0400 Subject: [PATCH 4/4] Implement the updated feed with content list component --- frontend/src/pages/Feed.tsx | 408 ++++------------------- frontend/src/styles/feed.scss | 62 +--- frontend/src/styles/navbar.scss | 10 +- frontend/src/styles/profile/profile.scss | 14 +- 4 files changed, 90 insertions(+), 404 deletions(-) diff --git a/frontend/src/pages/Feed.tsx b/frontend/src/pages/Feed.tsx index 8ce15a5..c331f89 100644 --- a/frontend/src/pages/Feed.tsx +++ b/frontend/src/pages/Feed.tsx @@ -1,80 +1,29 @@ import { useEffect, useState } from "react"; -import { Content } from "../models/Content"; -import { User } from "../models/User"; import { useNavigate } from "react-router-dom"; -import { useAuth } from "../hooks/useAuth"; import axios from "axios"; + +import { useAuth } from "../hooks/useAuth"; import { apiURL } from "../scripts/api"; -import ContentTile from "../components/content/ContentTile"; -import ContentPreviewPopup from "../components/content/ContentPreviewPopup"; -import { normalizeContentDates } from "../services/contentHelper"; + +import { User } from "../models/User"; +import ContentList from "../components/feed/ContentList"; export default function Feed() { - const [trendingContent, setTrendingContent] = useState([]); - const [latestContent, setLatestContent] = useState([]); - const [personalizedContent, setPersonalizedContent] = useState([]); const [creatorProfiles, setCreatorProfiles] = useState([]); - const [previewContent, setPreviewContent] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [authLoading, setAuthLoading] = useState(true); const navigate = useNavigate(); const auth = useAuth(); - const [user, setUser] = useState(null); - const [errorTrending, setErrorTrending] = useState(null); - const [errorLatest, setErrorLatest] = useState(null); - const [errorPersonalized, setErrorPersonalized] = useState( - null + const [tab, setTab] = useState<"personalized" | "trending" | "latest">( + auth.user ? "personalized" : "trending" ); - const [errorCreators, setErrorCreators] = useState(null); - - useEffect(() => { - console.log("Auth state:", auth.isAuthenticated, auth.user); - if (auth.isAuthenticated !== undefined) { - setAuthLoading(false); // Auth state is initialized - } - }, [auth.isAuthenticated]); - - useEffect(() => { - // Function to reload the Mondiad script (ADS) - const reloadMondiadScript = () => { - const existingScript = document.querySelector( - "script[src='https://ss.mrmnd.com/native.js']" - ); - if (existingScript) { - // Remove the existing script - existingScript.remove(); - } - - // Create a new script element - const script = document.createElement("script"); - script.src = "https://ss.mrmnd.com/native.js"; - script.async = true; - // Append the script to the document head - document.head.appendChild(script); - }; - - // Reload the script whenever the component renders - reloadMondiadScript(); - }, [trendingContent, personalizedContent, latestContent]); + const [errorCreators, setErrorCreators] = useState(null); useEffect(() => { - if (authLoading) return; // Wait for auth to finish loading and user to be available - const fetchContent = async () => { - setIsLoading(true); - - setErrorTrending(null); - setErrorLatest(null); - setErrorPersonalized(null); setErrorCreators(null); - // Fetch content, after user is verified as logged in - const latestFetched = await fetchLatestContent(); - const trendingFetched = await fetchTrendingContent(); - const personalizedFetched = await fetchPersonalizedContent(); let creatorsFetched = false; // Only fetch related creators if we have a user @@ -82,129 +31,16 @@ export default function Feed() { creatorsFetched = await fetchRelatedCreators(); } - setUser(auth.user); - - if (!latestFetched) { - setErrorLatest( - "Failed to fetch latest content. Please reload the page or contact support." - ); - } - - if (!trendingFetched) { - setErrorTrending( - "Failed to fetch trending content. Please reload the page or contact support." - ); - } - - if (!personalizedFetched) { - setErrorPersonalized( - "Failed to fetch personalized content. Please reload the page or contact support." - ); - } - if (auth.user?.uid && !creatorsFetched) { setErrorCreators( "Failed to fetch related creators. Please reload the page or contact support." ); } - - setIsLoading(false); }; fetchContent(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [authLoading, auth.user]); - - useEffect(() => { - setUser(auth.user); - }, [auth.user]); - - async function fetchTrendingContent(): Promise { - try { - const trendingResponse = await axios.get( - `${apiURL}/content/feed/trending`, - { timeout: 5000 } - ); - - if (trendingResponse.data && trendingResponse.data.success) { - const normalizedContent = trendingResponse.data.trendingContent.map( - (content: Content) => normalizeContentDates(content) - ); - - const withAuthors = await Promise.all( - normalizedContent.map(attachUserData) - ); - setTrendingContent(withAuthors); - console.log("Trending content:", normalizedContent); - return true; - } else { - setTrendingContent([]); - } - } catch { - setTrendingContent([]); - } - - return false; - } - - async function fetchLatestContent(): Promise { - try { - const contentResponse = await axios.get(`${apiURL}/content`, { - timeout: 5000, - }); - - if (contentResponse.data && contentResponse.data.success) { - const latestContent = contentResponse.data.content.map( - (content: Content) => normalizeContentDates(content) - ); - - const withAuthors = await Promise.all( - latestContent.map(attachUserData) - ); - setLatestContent(withAuthors); - console.log("Latest content:", latestContent); - return true; - } else { - setLatestContent([]); - } - } catch { - setLatestContent([]); - } - - return false; - } - - async function fetchPersonalizedContent(): Promise { - if (!auth.user?.uid) { - setPersonalizedContent([]); - return false; - } - - try { - const personalizedResponse = await axios.get( - `${apiURL}/content/feed/${auth.user.uid}`, - { timeout: 5000, withCredentials: true } - ); - - if (personalizedResponse.data && personalizedResponse.data.success) { - const normalizedContent = - personalizedResponse.data.personalizedContent.map( - (content: Content) => normalizeContentDates(content) - ); - - const withAuthors = await Promise.all( - normalizedContent.map(attachUserData) - ); - setPersonalizedContent(withAuthors); - return true; - } else { - setPersonalizedContent([]); - } - } catch { - setPersonalizedContent([]); - } - return false; - } + }, []); async function fetchRelatedCreators(): Promise { if (!auth.user?.uid) { @@ -267,188 +103,82 @@ export default function Feed() { return false; } - async function attachUserData(content: Content): Promise { - if (!content.creatorUID) { - return content; - } - - try { - const userRes = await axios.get(`${apiURL}/user/${content.creatorUID}`); - content.user = userRes.data; - } catch { - console.error(`Failed to fetch author for content ID: ${content.uid}`); - } - return content; - } - - const openPreview = (content: Content) => { - setPreviewContent(content); - }; - - const closePreview = () => { - setPreviewContent(null); - }; - return (
- {user &&

Welcome, {user.firstName}

} + {auth.user &&

Welcome, {auth.user.firstName}

} - {/* Trending Content Section */} -

Top Trending

- {isLoading ? ( -
-
-
-
-
-
-
-
- ) : trendingContent.length === 0 ? ( -

No trending content found

- ) : ( -
- {trendingContent.map((content, index) => ( -
- {index % 8 === 2 ? ( -
-
-
- ) : ( - openPreview(c)} - /> - )} -
- ))} -
- )} - {errorTrending &&

{errorTrending}

} - - {/* Latest Content Section */} -

Latest Post

- {isLoading ? ( -
-
-
-
-
-
-
-
- ) : latestContent.length === 0 ? ( -

No latest content found

- ) : ( -
- {latestContent.map((content, index) => ( - openPreview(c)} - /> - ))} -
- )} - {errorLatest &&

{errorLatest}

} - - {user && ( + {auth.user && (
{/* Related Creators Section */}

Creators You Might Like

- {isLoading ? ( -
-
-
-
-
-
-
-
- ) : creatorProfiles.length === 0 ? ( -

No related creators found

- ) : ( -
-
- {creatorProfiles.map((creator) => ( -
navigate(`/profile/${creator.uid}`)} - > - {creator.profileImage ? ( - {creator.username} - ) : ( -
- {creator.firstName?.charAt(0) || "?"} -
- )} -
- - @{creator.username} - - - {creator.firstName} {creator.lastName} - - - {creator.followers?.length || 0} followers - -
-
- ))} -
-
- )} - {errorCreators &&

{errorCreators}

} - {/* Personalized Content Section */} -

Content For You

- {isLoading ? ( -
-
-
-
-
-
-
-
- ) : personalizedContent.length === 0 ? ( -

No content found

+ {creatorProfiles.length === 0 ? ( +

No related creators found

) : ( -
- {personalizedContent.map((content, index) => ( -
- {index % 8 === 0 ? ( -
-
-
- ) : ( - openPreview(c)} +
+ {creatorProfiles.map((creator) => ( +
navigate(`/profile/${creator.uid}`)} + > + {creator.profileImage ? ( + {creator.username} + ) : ( +
+

{creator.firstName?.charAt(0) || "?"}

+
)} +
+ + @{creator.username} + + + {creator.firstName} {creator.lastName} + + + {creator.followers?.length || 0} followers + +
))}
)} - {errorPersonalized &&

{errorPersonalized}

} + {errorCreators &&

{errorCreators}

}
)} - {previewContent && ( - - )} + + {/* TABS */} +
+ {auth.user && ( + + )} + + +
+ +
); } diff --git a/frontend/src/styles/feed.scss b/frontend/src/styles/feed.scss index b57eebd..ef616c5 100644 --- a/frontend/src/styles/feed.scss +++ b/frontend/src/styles/feed.scss @@ -3,22 +3,10 @@ .content-list { display: grid; - grid-template-columns: repeat(6, 1fr); + grid-template-columns: repeat(6, 1fr) !important; gap: 1rem; } -.content-list-horizontal { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - gap: 1rem; - width: 100%; - overflow-y: scroll; - margin: -25px -25px; - padding: 25px 25px; -} - .feed-section-title { margin-bottom: 1rem; } @@ -83,7 +71,7 @@ border-width: 1px; box-sizing: border-box; @include global.glassmorphic-background; - + background: linear-gradient( 90deg, rgba(255, 255, 255, 0.1) 0%, @@ -138,57 +126,17 @@ } } -.related-creators-section { - margin-bottom: 2rem; - padding: 0.5rem 0; - width: 100%; - max-width: 100%; - overflow: hidden; - box-sizing: border-box; -} - -.related-creators-section h3 { - margin-bottom: 1rem; - font-size: 1.3rem; - font-weight: 600; -} - -.related-creators-section .no-creators-message { - font-size: 0.9rem; - color: rgba(255, 255, 255, 0.7); - margin-bottom: 1rem; - font-style: italic; -} - .creator-profiles-list { display: flex; flex-wrap: nowrap; overflow-x: auto; gap: 1.2rem; - padding: 0.5rem 0; - margin-bottom: 1rem; - width: 100%; - max-width: 100%; - -ms-overflow-style: none; - scrollbar-width: thin; - padding-bottom: 10px; + scrollbar-width: none; + margin: -20px; + padding: 20px; justify-content: flex-start; } -.creator-profiles-list::-webkit-scrollbar { - height: 6px; -} - -.creator-profiles-list::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.05); - border-radius: 10px; -} - -.creator-profiles-list::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.2); - border-radius: 10px; -} - .creator-profile-card { width: 100%; padding: 1.2rem; diff --git a/frontend/src/styles/navbar.scss b/frontend/src/styles/navbar.scss index ffab79d..8c08aa9 100644 --- a/frontend/src/styles/navbar.scss +++ b/frontend/src/styles/navbar.scss @@ -26,7 +26,7 @@ top: 0; left: 0; margin: var(--navbar-margin); - z-index: 2; + z-index: 5; display: flex; align-items: center; } @@ -127,7 +127,7 @@ right: 0; padding: 10px 30px; margin: var(--navbar-margin); - z-index: 2; + z-index: 10; min-width: 150px; } @@ -179,7 +179,7 @@ top: 90px; right: -150px; transform: translateX(-50%); - z-index: 2; + z-index: 10; width: 400px; } @@ -277,7 +277,7 @@ left: 50%; transform: translateX(-50%); width: 50%; - z-index: 2; + z-index: 10; } .navbar-page-overlay { @@ -289,7 +289,7 @@ background-color: rgba(0, 0, 0, 0.5); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); - z-index: 1; + z-index: 2; } .close-menu { diff --git a/frontend/src/styles/profile/profile.scss b/frontend/src/styles/profile/profile.scss index 6fdf201..a9e80b4 100644 --- a/frontend/src/styles/profile/profile.scss +++ b/frontend/src/styles/profile/profile.scss @@ -1,3 +1,6 @@ +@use "../colors.scss"; +@use "../global.scss"; + .profile-banner { display: flex; gap: 1rem; @@ -100,9 +103,10 @@ .tabs { display: flex; margin: 30px 0 -15px 0; - - /* Center tabs horizontally */ justify-content: center; + position: sticky; + top: 100px; + z-index: 1; } /* Base tab button styles */ @@ -113,6 +117,8 @@ background-color: var(--tile-color); color: var(--text-color); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); cursor: pointer; transition: color 0.3s ease; @@ -120,12 +126,13 @@ width: 160px; } -// First tab button styling +/* First tab button styling */ .tab-active:first-child, .tab-inactive:first-child { border-radius: 20px 0 0 20px; } +/* Last tab button styling */ .tab-active:last-child, .tab-inactive:last-child { border-radius: 0 20px 20px 0; @@ -136,6 +143,7 @@ font-weight: bold; color: var(--text-color); background-color: var(--background-color); + z-index: 2; } /* Inactive tab styling */