Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions frontend/src/components/feed/ContentList.tsx
Original file line number Diff line number Diff line change
@@ -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<Content[]>([]);

const [previewContent, setPreviewContent] = useState<Content | null>(null);
const [error, setError] = useState<string | null>(null);

const auth = useAuth();

useEffect(() => {
setIsLoading(true);

fetchContent();

setIsLoading(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tab]);
Comment on lines +19 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

isLoading toggles off before the data arrives

setIsLoading(false) runs immediately after calling fetchContent(), so the spinner disappears before the AJAX request finishes. Move the flag – and the early setIsLoading(true) – inside an awaited async wrapper.

-useEffect(() => {
-  setIsLoading(true);
-
-  fetchContent();
-
-  setIsLoading(false);
+useEffect(() => {
+  let isMounted = true;
+  const load = async () => {
+    setIsLoading(true);
+    await fetchContent();          // wait until the promise settles
+    if (isMounted) setIsLoading(false);
+  };
+  load();
+
+  return () => {
+    isMounted = false;             // avoid set-state on unmounted component
+  };
   // eslint-disable-next-line react-hooks/exhaustive-deps
 }, [tab]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
setIsLoading(true);
fetchContent();
setIsLoading(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tab]);
useEffect(() => {
let isMounted = true;
const load = async () => {
setIsLoading(true);
await fetchContent(); // wait until the promise settles
if (isMounted) {
setIsLoading(false);
}
};
load();
return () => {
isMounted = false; // avoid set-state on unmounted component
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tab]);
🤖 Prompt for AI Agents (early access)
In frontend/src/components/feed/ContentList.tsx around lines 19 to 26, the
isLoading state is set to false immediately after calling fetchContent(),
causing the loading spinner to disappear before the data fetch completes. To fix
this, wrap the fetchContent call inside an async function within useEffect,
await the fetchContent call, and only then set isLoading to false. Also, move
setIsLoading(true) inside this async function before the fetch starts to
properly reflect the loading state during the data fetch.


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<boolean> {
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;
}
Comment on lines +52 to +79
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

fetchContent duplicates endpoint logic & loses error context

  1. The switch block repeats three times – once for the request and again for data extraction.
  2. Silent catch {} swallows the real error, making debugging difficult.
  3. Returning false isn’t used anywhere.

Refactor into a lookup table and propagate the error so the caller can decide.

+interface Endpoint {
+  url: string;
+  extract: (d: any) => Content[];
+}
+
+const endpoints: Record<string, Endpoint> = {
+  personalized: {
+    url: `${apiURL}/content/feed/${auth.user?.uid}`,
+    extract: (d) => d.personalizedContent,
+  },
+  latest: {
+    url: `${apiURL}/content`,
+    extract: (d) => d.content,
+  },
+  trending: {
+    url: `${apiURL}/content/feed/trending`,
+    extract: (d) => d.trendingContent,
+  },
+};

Use it in one pass and remove the boolean return value.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents (early access)
In frontend/src/components/feed/ContentList.tsx around lines 52 to 79, the
fetchContent function duplicates endpoint logic in multiple places and swallows
errors silently, losing error context. Refactor by creating a lookup table
mapping tabs to their respective API endpoints, then perform the axios request
in a single step using this table. Remove the boolean return value and instead
propagate any caught errors to the caller by rethrowing them, allowing the
caller to handle errors appropriately.


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;
}

Comment on lines +81 to +103
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Set error to null on success and use optional chaining

If a previous fetch failed, error remains non-null and the message persists even after a successful retry.

-  if (response.data && response.data.success) {
+  if (response?.data?.success) {
     ...
     setContents(normalizedContent);
+    setError(null);

(The optional-chaining also satisfies the Biome hint.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
}
if (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);
setError(null);
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 81-81: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🤖 Prompt for AI Agents (early access)
In frontend/src/components/feed/ContentList.tsx around lines 81 to 103, after a
successful fetch indicated by response.data.success, the error state is not
reset to null, causing old error messages to persist after retries. Fix this by
setting the error state to null upon success. Additionally, use optional
chaining when accessing nested properties like response.data.personalizedContent
to safely handle undefined values and satisfy the Biome hint.

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 (
<div>
{/* Personalized Content Section */}
<h2 className='feed-section-h2'>
{tab === "personalized"
? "Content For You"
: tab === "latest"
? "See the Latest Content"
: "What's Trending"}
</h2>
{isLoading ? (
<div className='content-list'>
<div className='loading-tile' />
<div className='loading-tile' />
<div className='loading-tile' />
<div className='loading-tile' />
<div className='loading-tile' />
<div className='loading-tile' />
</div>
) : contents.length === 0 ? (
<h3>No content found</h3>
) : (
<div className='content-list'>
{contents.map((content, index) => (
<div key={`${content.uid || index}`}>
{index % 8 === 0 ? (
<div className='ad-tile' key={`ad-personalized-${index}`}>
<div data-mndazid='ead3e00e-3a1a-42f1-b990-c294631f3d97'></div>
</div>
) : (
<ContentTile
key={`content-personalized-${content.uid || index}`}
content={content}
index={index}
onPreview={(c) => openPreview(c)}
/>
)}
</div>
))}
</div>
)}
{error && <p className='error'>{error}</p>}
{previewContent && (
<ContentPreviewPopup content={previewContent} onClose={closePreview} />
)}
</div>
);
}
21 changes: 11 additions & 10 deletions frontend/src/hooks/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<object>) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);

const login = async (userUID: string) => {
console.log("Logging in user...");

getUserData(userUID);
await getUserData(userUID);
};

const logout = async () => {
Expand All @@ -29,7 +28,6 @@ export default function AuthProvider({
}
};

// Automatically refresh the user token and fetch user data
useEffect(() => {
const initializeUser = async () => {
try {
Expand All @@ -56,22 +54,20 @@ export default function AuthProvider({
}
}
} catch (err) {
// This is expected if the user is not logged in, so we don't need to show an error
console.log("Not currently logged in or refresh token expired");
// Clear any stale user data
console.error("Auto-login failed", err);
setUser(null);
} finally {
setLoading(false);
}
};

initializeUser();
}, []);

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 {
Expand All @@ -80,6 +76,11 @@ export default function AuthProvider({
}
}

if (loading) {
// Render a fallback while loading
return <LoadingPage />;
}

return (
<AuthContext.Provider
value={{ user, login, logout, isAuthenticated: !!user }}
Expand Down
Loading