diff --git a/package-lock.json b/package-lock.json
index e65b56e..71482c4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.1.1",
"dependencies": {
"@reduxjs/toolkit": "^2.8.2",
+ "@sentry/react": "^10.19.0",
"@studio-freight/lenis": "^1.0.42",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
@@ -22,6 +23,7 @@
"react-intersection-observer": "^9.16.0",
"react-redux": "^9.2.0",
"react-scripts": "5.0.1",
+ "react-toastify": "^11.0.5",
"web-vitals": "^2.1.4"
},
"devDependencies": {
@@ -3963,6 +3965,98 @@
"integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==",
"license": "MIT"
},
+ "node_modules/@sentry-internal/browser-utils": {
+ "version": "10.19.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.19.0.tgz",
+ "integrity": "sha512-E3H6R+tX7sYMIjfCRAMO0qIH43dtUqv2rSo0vv6eHDi4lDXtlDc+Vb67n4VIesT7YVxQD7GIkNhMk3hmRDIwww==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/core": "10.19.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry-internal/feedback": {
+ "version": "10.19.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.19.0.tgz",
+ "integrity": "sha512-AJ8rpzNYgfmWzovmFss51q9FtBaa2qYTLwkbVdTf58fZbLMUrgZ6qf9qMk0ePiS3nB87w9+mpbLzRObYOsK9RA==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/core": "10.19.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry-internal/replay": {
+ "version": "10.19.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.19.0.tgz",
+ "integrity": "sha512-bOWsm/t+d2LCYa3gUjgwFds6kKSW+K6i4pssgDY4XiV/MxHsQtQ2rbHX80chLRQe2HFCX2njvjVSJN+Nsdjmpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry-internal/browser-utils": "10.19.0",
+ "@sentry/core": "10.19.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry-internal/replay-canvas": {
+ "version": "10.19.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.19.0.tgz",
+ "integrity": "sha512-DulLU4lvtrGPExKtpbCveLxPACrFmGx4eEYhzIn35UH8iIx6ONRSLemQyiUJQoLau7KXJy0I8AWxN+SagfebEA==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry-internal/replay": "10.19.0",
+ "@sentry/core": "10.19.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/browser": {
+ "version": "10.19.0",
+ "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.19.0.tgz",
+ "integrity": "sha512-/+B84qFOLg1vJhg4YSA4a7Pneq5Pbt1BXEdrp/UW4tJmtGPZb28qXlMdoPfmFWZgVezrawaPkxLmbu+47/+rsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry-internal/browser-utils": "10.19.0",
+ "@sentry-internal/feedback": "10.19.0",
+ "@sentry-internal/replay": "10.19.0",
+ "@sentry-internal/replay-canvas": "10.19.0",
+ "@sentry/core": "10.19.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/core": {
+ "version": "10.19.0",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.19.0.tgz",
+ "integrity": "sha512-OqZjYDYsK6ZmBG5UzML0uKiKq//G6mMwPcszfuCsFgPt+pg5giUCrCUbt5VIVkHdN1qEEBk321JO2haU5n2Eig==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/react": {
+ "version": "10.19.0",
+ "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.19.0.tgz",
+ "integrity": "sha512-LgADcXfJ4hVVtOSW6IkY3Wsefw4xPHIQpiEux28GHf2EAYkWxyCWWb9uQH4voAacG+FcX63XfJkpUMZjadE9qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/browser": "10.19.0",
+ "@sentry/core": "10.19.0",
+ "hoist-non-react-statics": "^3.3.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.14.0 || 17.x || 18.x || 19.x"
+ }
+ },
"node_modules/@sinclair/typebox": {
"version": "0.24.51",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
@@ -6468,6 +6562,15 @@
"wrap-ansi": "^7.0.0"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -9699,6 +9802,21 @@
"he": "bin/he"
}
},
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
"node_modules/hoopy": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
@@ -14993,6 +15111,18 @@
}
}
},
+
+ "node_modules/react-toastify": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
+ "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
+ "license": "MIT",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19",
+ "react-dom": "^18 || ^19"
"node_modules/react-scripts/node_modules/dotenv": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
diff --git a/package.json b/package.json
index 0d7f6d3..66ae933 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^2.8.2",
+ "@sentry/react": "^10.19.0",
"@studio-freight/lenis": "^1.0.42",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
@@ -17,6 +18,7 @@
"react-intersection-observer": "^9.16.0",
"react-redux": "^9.2.0",
"react-scripts": "5.0.1",
+ "react-toastify": "^11.0.5",
"web-vitals": "^2.1.4"
},
"scripts": {
diff --git a/src/components/Browse.js b/src/components/Browse.js
index 808e69b..667565f 100644
--- a/src/components/Browse.js
+++ b/src/components/Browse.js
@@ -10,6 +10,7 @@ import {
searchMovies,
searchTVShows
} from '../utils/vidsrcApi';
+import { logger } from '../utils/logger';
// --- NEW: We will create this new component in the next step ---
import LazyLoadCarousel from './LazyLoadCarousel';
@@ -74,7 +75,7 @@ const Browse = () => {
}
}));
} catch (error) {
- console.error('Error loading priority content:', error);
+ logger.error('Failed to load priority content', error, true);
} finally {
setInitialPageLoad(false); // The main page is now ready
}
@@ -110,7 +111,7 @@ const Browse = () => {
}
}));
} catch (error) {
- console.error(`Error loading initial data for ${categoryKey}:`, error);
+ logger.error(`Failed to load initial data for ${categoryKey}`, error, true);
// Mark as loaded to prevent retrying on error
setContent(prev => ({ ...prev, [categoryKey]: { ...prev[categoryKey], loaded: true } }));
}
@@ -147,7 +148,7 @@ const Browse = () => {
}));
} catch (error) {
- console.error(`Error loading more ${categoryKey}:`, error);
+ logger.error(`Failed to load more ${categoryKey}`, error, true);
setContent(prev => ({ ...prev, [categoryKey]: { ...prev[categoryKey], loadingMore: false, hasMore: false } }));
}
}, [content]);
@@ -170,7 +171,7 @@ const Browse = () => {
setSearchResults(combinedResults);
} catch (error) {
- console.error('Search error:', error);
+ logger.error('Search failed', error, true);
setSearchResults([]);
} finally {
setIsSearching(false);
diff --git a/src/components/Login.js b/src/components/Login.js
index 5870ef4..16dae83 100644
--- a/src/components/Login.js
+++ b/src/components/Login.js
@@ -9,6 +9,7 @@ import {
updateProfile,
} from "firebase/auth";
import { useNavigate, useLocation } from "react-router-dom";
+import { logger } from "../utils/logger";
// Enhanced Login component with better responsiveness and animations
const Login = () => {
@@ -66,8 +67,8 @@ const Login = () => {
} catch (error) {
const errorCode = error.code;
const errorMessage = error.message;
- console.error("Sign up error:", errorCode, errorMessage);
-
+ logger.error(`Sign up failed: ${errorCode}`, { errorCode, errorMessage }, true);
+
// Provide user-friendly error messages
let friendlyMessage = "";
switch (errorCode) {
@@ -111,8 +112,8 @@ const Login = () => {
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
- console.error("Sign In error:", errorCode, errorMessage);
-
+ logger.error(`Sign in failed: ${errorCode}`, { errorCode, errorMessage }, true);
+
// Provide user-friendly error messages
let friendlyMessage = "";
switch (errorCode) {
diff --git a/src/components/MovieCard.js b/src/components/MovieCard.js
index d5e5e17..ac27f97 100644
--- a/src/components/MovieCard.js
+++ b/src/components/MovieCard.js
@@ -1,5 +1,6 @@
import React, { memo } from 'react';
import { getImageUrl, getYear, getBackupImageUrl } from '../utils/vidsrcApi';
+import { logger } from '../utils/logger';
const MovieCard = memo(({ movie, onClick, isTV = false, customBadge = null }) => {
const title = isTV ? movie.name : movie.title;
@@ -8,11 +9,11 @@ const MovieCard = memo(({ movie, onClick, isTV = false, customBadge = null }) =>
// Debug logging to see what data we're getting
if (!title) {
- console.log('š MovieCard Debug - Movie data:', movie);
+ logger.debug('MovieCard: Missing title', { movie });
}
const handleImageError = (e) => {
- console.log('š¼ļø Image failed to load:', e.target.src);
+ logger.debug('MovieCard: Image failed to load', { src: e.target.src });
// Try alternative poster sizes first
if (e.target.src.includes('w500')) {
e.target.src = getImageUrl(posterPath, 'w342');
diff --git a/src/components/MovieDetails.js b/src/components/MovieDetails.js
index 6ac8fb6..f9f74bc 100644
--- a/src/components/MovieDetails.js
+++ b/src/components/MovieDetails.js
@@ -3,6 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom';
import { getMovieDetails, getMovieCredits, getMovieVideos, getMovieRecommendations } from '../utils/vidsrcApi';
import { safeOpenExternal } from '../utils/safeNavigation';
import VideoPlayer from './VideoPlayer';
+import { logger } from '../utils/logger';
const MovieDetails = () => {
const { id } = useParams();
@@ -58,7 +59,7 @@ const MovieDetails = () => {
}, 300);
}
} catch (error) {
- console.error('Error loading movie details:', error);
+ logger.error('Error loading movie details', error, true);
setCredits({ cast: [], crew: [] });
setVideos([]);
setRecommendations([]);
@@ -335,7 +336,7 @@ const MovieDetails = () => {
const url = `https://www.youtube.com/watch?v=${encodeURIComponent(trailer.key)}`;
const success = safeOpenExternal(url);
if (!success) {
- console.warn('Failed to open trailer safely');
+ logger.warn('Failed to open trailer safely');
}
}}
className="border-2 border-gray-500 text-gray-300 hover:border-red-500 hover:text-red-400 px-6 sm:px-8 py-4 sm:py-3 rounded-lg font-['JetBrains_Mono',monospace] font-bold flex items-center justify-center transition-all duration-300 w-full sm:w-auto order-3"
@@ -409,7 +410,7 @@ const MovieDetails = () => {
className="bg-gray-900/50 rounded-lg p-4 text-center cursor-pointer hover:bg-gray-800/50 transition-all duration-300 transform hover:scale-105"
onClick={() => {
// You can add navigation to actor details page here if you have one
- console.log('Navigate to actor:', actor.name);
+ logger.debug('Navigate to actor:', actor.name);
}}
>
diff --git a/src/components/Movies.js b/src/components/Movies.js
index 885fb92..568619c 100644
--- a/src/components/Movies.js
+++ b/src/components/Movies.js
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { fetchPopularMovies, fetchTopRatedMovies, fetchTrendingMovies, searchContent } from '../utils/vidsrcApi';
import { detectDevice, mobileCache } from '../utils/mobileApiHelper';
+import { logger } from '../utils/logger';
import MovieCard from './MovieCard';
import VideoPlayer from './VideoPlayer';
@@ -35,21 +36,21 @@ const Movies = () => {
if (device.isMobile) {
const handleOnline = () => {
setMobileStatus(prev => ({ ...prev, isOffline: false }));
- console.log('š± Mobile device came online, refreshing movie data...');
+ logger.mobileEvent('device_online', { action: 'refreshing_movie_data' });
};
const handleOffline = () => {
setMobileStatus(prev => ({ ...prev, isOffline: true }));
- console.log('š± Mobile device went offline, using cached movie data...');
+ logger.mobileEvent('device_offline', { action: 'using_cached_data' });
};
const handleConnectionChange = () => {
if (navigator.connection) {
- setMobileStatus(prev => ({
- ...prev,
- connectionType: navigator.connection.effectiveType
+ setMobileStatus(prev => ({
+ ...prev,
+ connectionType: navigator.connection.effectiveType
}));
- console.log('š± Mobile connection changed to:', navigator.connection.effectiveType);
+ logger.mobileEvent('connection_change', { type: navigator.connection.effectiveType });
}
};
@@ -100,23 +101,32 @@ const Movies = () => {
// Show user feedback for mobile fallback data
if (response.isMockData && mobileStatus.isMobile) {
- console.log('š± Using offline movie content for mobile device');
+ logger.mobileEvent('using_offline_content', { filter: currentFilter });
}
-
+
+
+ setMovies(movieData);
+ setFilteredMovies(movieData);
+
+
// Cache data for mobile devices
if (mobileStatus.isMobile && movieData.length > 0) {
mobileCache.set(`movies_${currentFilter}`, movieData, 600000); // 10 minutes
}
-
+
} catch (error) {
+
+ logger.error('Failed to load movies', error, true);
+
console.error('Error loading movies:', error);
setHasMore(false); // Disable load more on error
+
// Mobile fallback - try to use any cached data
if (mobileStatus.isMobile) {
const cachedData = mobileCache.get(`movies_${currentFilter}`);
if (cachedData && cachedData.length > 0) {
- console.log('š± Using emergency movie cache for mobile');
+ logger.mobileEvent('using_emergency_cache', { filter: currentFilter });
setMovies(cachedData);
setFilteredMovies(cachedData);
} else {
@@ -199,7 +209,7 @@ const Movies = () => {
: [];
setFilteredMovies(movieResults);
} catch (error) {
- console.error('Search error:', error);
+ logger.error('Search failed', error, true);
setFilteredMovies([]);
} finally {
setIsSearching(false);
diff --git a/src/components/NeuralChat.js b/src/components/NeuralChat.js
index cac0e89..ac749b3 100644
--- a/src/components/NeuralChat.js
+++ b/src/components/NeuralChat.js
@@ -1,7 +1,11 @@
import React, { useState, useRef, useEffect } from "react";
+
+import { logger } from "../utils/logger";
+
import { X, x } from "lucide-react"
import { ArrowRightToLine } from 'lucide-react';
+
// Simple markdown parser component for chat messages
const MarkdownRenderer = ({ content }) => {
const parseMarkdown = (text) => {
@@ -86,6 +90,7 @@ const MarkdownRenderer = ({ content }) => {
return
{parseMarkdown(content)}
;
};
+
const NeuralChat = () => {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
@@ -214,11 +219,11 @@ ${selectedMovies.map((movie, i) => `${i + 1}. **${movie}**`).join('\n')}
try {
const apiKey = process.env.REACT_APP_GEMINI_API_KEY;
if (!apiKey) {
- console.log('ā No Gemini API key found, using fallback');
+ logger.info('No Gemini API key found, using fallback');
return getSmartMovieRecommendation(userMessage);
}
- console.log('š¤ Calling Gemini API for real AI recommendations...');
+ logger.info('Calling Gemini API for real AI recommendations');
const moviePrompt = `You are the NEXUS Movie Recommendation AI, the coolest movie buddy on the planet! You LOVE movies and get super excited about helping people find their next favorite film.
@@ -246,8 +251,12 @@ Give them amazing movie suggestions that match what they want!`;
for (const attempt of apiAttempts) {
try {
+
+ logger.debug(`Trying Gemini model: ${attempt.name}`);
+
console.log(`š Trying: ${attempt.name}`);
+
const response = await fetch(attempt.url, {
method: "POST",
headers: {
@@ -271,22 +280,40 @@ Give them amazing movie suggestions that match what they want!`;
if (response.ok) {
const data = await response.json();
if (data.candidates && data.candidates[0] && data.candidates[0].content && data.candidates[0].content.parts && data.candidates[0].content.parts[0]) {
- console.log(`ā
SUCCESS with: ${attempt.name}`);
+ logger.info(`SUCCESS with: ${attempt.name}`);
return data.candidates[0].content.parts[0].text;
}
} else {
+
+ logger.warn(`${attempt.name} failed: ${response.status}`);
+ if (response.status === 503) {
+ logger.debug('Service temporarily unavailable');
+ } else if (response.status === 404) {
+ logger.debug('Model not found');
+ } else if (response.status === 429) {
+ logger.debug('Rate limit exceeded');
+ } else if (response.status === 400) {
+ logger.debug('Bad request - check API key');
+ }
+
console.log(`ā ${attempt.name} failed: ${response.status}`);
+
}
} catch (modelError) {
- console.log(`ā ${attempt.name} error:`, modelError.message);
+ logger.warn(`${attempt.name} error: ${modelError.message}`);
}
}
+
+ // If all API attempts fail, use smart fallback
+ logger.warn('All Gemini APIs failed, using intelligent fallback system');
+
console.log('š All Gemini APIs failed, using intelligent fallback system');
+
return getSmartMovieRecommendation(userMessage);
} catch (error) {
- console.error("Movie AI Error:", error);
+ logger.error('Movie AI Error', error, true);
return getSmartMovieRecommendation(userMessage);
}
};
@@ -318,7 +345,7 @@ Give them amazing movie suggestions that match what they want!`;
setMessages(prev => [...prev, newBotMessage]);
} catch (error) {
- console.error("Error:", error);
+ logger.error('Error in NeuralChat', error, true);
const errorMessage = {
text: "## š¬ Oops! Movie Magic Glitch!\n\nSomething went wrong with my movie magic! Try asking me about movies again - **I love talking about films!** šæ\n\n*Try asking:* \"What's a good comedy?\" or \"Best action movies\"",
isBot: true,
diff --git a/src/components/TVShows.js b/src/components/TVShows.js
index 0cfe61e..ba37e66 100644
--- a/src/components/TVShows.js
+++ b/src/components/TVShows.js
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { fetchPopularTV, fetchTopRatedTV, fetchTrendingTV, searchContent } from '../utils/vidsrcApi';
import { detectDevice, mobileCache } from '../utils/mobileApiHelper';
+import { logger } from '../utils/logger';
import MovieCard from './MovieCard';
import VideoPlayer from './VideoPlayer';
@@ -37,21 +38,21 @@ const TVShows = () => {
if (device.isMobile) {
const handleOnline = () => {
setMobileStatus(prev => ({ ...prev, isOffline: false }));
- console.log('š± Mobile device came online, refreshing TV show data...');
+ logger.mobileEvent('device_online', { action: 'refreshing_tv_data' });
};
const handleOffline = () => {
setMobileStatus(prev => ({ ...prev, isOffline: true }));
- console.log('š± Mobile device went offline, using cached TV show data...');
+ logger.mobileEvent('device_offline', { action: 'using_cached_data' });
};
const handleConnectionChange = () => {
if (navigator.connection) {
- setMobileStatus(prev => ({
- ...prev,
- connectionType: navigator.connection.effectiveType
+ setMobileStatus(prev => ({
+ ...prev,
+ connectionType: navigator.connection.effectiveType
}));
- console.log('š± Mobile TV connection changed to:', navigator.connection.effectiveType);
+ logger.mobileEvent('connection_change', { type: navigator.connection.effectiveType });
}
};
@@ -102,23 +103,33 @@ const TVShows = () => {
// Show user feedback for mobile fallback data
if (response.isMockData && mobileStatus.isMobile) {
- console.log('š± Using offline TV show content for mobile device');
+ logger.mobileEvent('using_offline_content', { filter: activeFilter });
}
-
+
+
+ setShows(showData);
+ setFilteredShows(showData);
+
+
// Cache data for mobile devices
if (mobileStatus.isMobile && showData.length > 0) {
mobileCache.set(`tvshows_${activeFilter}`, showData, 600000); // 10 minutes
}
-
+
} catch (error) {
+
+ logger.error('Failed to load TV shows', error, true);
+
+
console.error('Error loading TV shows:', error);
setHasMore(false); // Disable load more on error
+
// Mobile fallback - try to use any cached data
if (mobileStatus.isMobile) {
const cachedData = mobileCache.get(`tvshows_${activeFilter}`);
if (cachedData && cachedData.length > 0) {
- console.log('š± Using emergency TV show cache for mobile');
+ logger.mobileEvent('using_emergency_cache', { filter: activeFilter });
setShows(cachedData);
setFilteredShows(cachedData);
} else {
@@ -200,7 +211,7 @@ const TVShows = () => {
: [];
setFilteredShows(tvResults);
} catch (error) {
- console.error('Search error:', error);
+ logger.error('TV show search failed', error, true);
setFilteredShows([]);
} finally {
setIsSearching(false);
diff --git a/src/components/VideoPlayer.js b/src/components/VideoPlayer.js
index f009940..f3a3e78 100644
--- a/src/components/VideoPlayer.js
+++ b/src/components/VideoPlayer.js
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { addWatchedItem } from '../utils/userSlice';
+import { logger } from '../utils/logger';
import { getMovieEmbedUrl, getTVEmbedUrl, setupPlayerEventListener, initializeWatchProgress, getWatchProgress, getMovieRecommendations, fetchTrendingMovies, fetchTrendingTV } from '../utils/vidsrcApi';
// Mock TV shows data for testing when TMDB API is not available
@@ -234,7 +235,7 @@ const addToWatchHistory = (movie, isTV = false, season = null, episode = null) =
}
} catch (error) {
-
+ logger.error('Error adding to watch history', error);
}
};
@@ -315,7 +316,7 @@ const VideoPlayer = ({ movie, isTV = false, season = 1, episode = 1, onClose, on
if (!seasonData || !seasonData.episodes || seasonData.episodes.length === 0) {
// Try to fetch episodes from season 1 if current season fails
if (targetSeason !== 1) {
- console.log('[NEXUS] Trying season 1 as fallback...');
+ logger.info('[NEXUS] Trying season 1 as fallback...');
const fallbackSeasonData = await fetchSeasonDetails(movie.id, 1);
if (fallbackSeasonData && fallbackSeasonData.episodes && fallbackSeasonData.episodes.length > 0) {
setNextEpisodes(fallbackSeasonData.episodes);
@@ -409,7 +410,7 @@ const VideoPlayer = ({ movie, isTV = false, season = 1, episode = 1, onClose, on
setRecommendations(recommendedContent);
} catch (error) {
- console.error('Error loading recommendations:', error);
+ logger.error('Error loading recommendations', error);
// Fallback to trending content
try {
const trendingMovies = await fetchTrendingMovies();
@@ -419,7 +420,7 @@ const VideoPlayer = ({ movie, isTV = false, season = 1, episode = 1, onClose, on
.slice(0, 15);
setRecommendations(allTrending);
} catch (fallbackError) {
- console.error('Fallback error:', fallbackError);
+ logger.error('Fallback error', fallbackError);
}
} finally {
setLoadingRecommendations(false);
@@ -435,7 +436,7 @@ const VideoPlayer = ({ movie, isTV = false, season = 1, episode = 1, onClose, on
useEffect(() => {
const loadUrl = async () => {
if (!movie || !movie.id) {
- console.warn('VideoPlayer: Movie data is incomplete');
+ logger.warn('VideoPlayer: Movie data is incomplete');
return;
}
@@ -450,11 +451,11 @@ const VideoPlayer = ({ movie, isTV = false, season = 1, episode = 1, onClose, on
if (url && url.trim() !== '') {
setCurrentEmbedUrl(url);
} else {
- console.warn('VideoPlayer: Generated URL is empty or invalid');
+ logger.warn('VideoPlayer: Generated URL is empty or invalid');
setHasError(true);
}
} catch (error) {
- console.error('VideoPlayer: Error generating embed URL:', error);
+ logger.error('VideoPlayer: Error generating embed URL', error);
setHasError(true);
}
};
@@ -467,17 +468,17 @@ const VideoPlayer = ({ movie, isTV = false, season = 1, episode = 1, onClose, on
const cleanup = setupPlayerEventListener((eventData) => {
try {
if (!eventData || typeof eventData !== 'object') {
- console.warn('VideoPlayer: Invalid event data received');
+ logger.warn('VideoPlayer: Invalid event data received');
return;
}
const { eventType, mediaData } = eventData;
-
+
if (!eventType) {
- console.warn('VideoPlayer: Event type missing');
+ logger.warn('VideoPlayer: Event type missing');
return;
}
-
+
switch (eventType) {
case 'play':
if (movie && movie.id) {
@@ -494,7 +495,7 @@ const VideoPlayer = ({ movie, isTV = false, season = 1, episode = 1, onClose, on
}
break;
case 'ended':
- console.log('VideoPlayer: Video ended');
+ logger.info('VideoPlayer: Video ended');
break;
case 'timeupdate':
if (mediaData) {
@@ -506,10 +507,10 @@ const VideoPlayer = ({ movie, isTV = false, season = 1, episode = 1, onClose, on
break;
}
} catch (error) {
- console.warn('VideoPlayer: Error handling player event:', error.message);
+ logger.warn('VideoPlayer: Error handling player event', error);
}
});
-
+
return () => {
if (cleanup && typeof cleanup === 'function') {
cleanup();
@@ -581,7 +582,7 @@ const VideoPlayer = ({ movie, isTV = false, season = 1, episode = 1, onClose, on
};
const handleSeasonSelect = async (seasonNumber) => {
- console.log('[NEXUS] Season selected:', seasonNumber);
+ logger.info('[NEXUS] Season selected', seasonNumber);
if (onSeasonEpisodeChange) {
setCurrentSeason(seasonNumber);
diff --git a/src/index.js b/src/index.js
index c24913a..b036d53 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,8 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
+import { ToastContainer } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
+import { initSentry, logger } from './utils/logger';
+
+// Initialize Sentry for error tracking
+initSentry();
// Global error suppression for third-party video player errors
const originalConsoleError = console.error;
@@ -32,71 +38,73 @@ const suppressedErrorPatterns = [
console.error = (...args) => {
const message = args.join(' ');
-
+
// Check if this is a suppressed error
- const shouldSuppress = suppressedErrorPatterns.some(pattern =>
+ const shouldSuppress = suppressedErrorPatterns.some(pattern =>
message.includes(pattern)
);
-
+
if (shouldSuppress) {
// Log to NEXUS internal logging instead
- console.log('%c[NEXUS SYSTEM]%c Third-party service internal operation - no action required',
- 'color: #ef4444; font-weight: bold;',
- 'color: #888888;'
- );
+ logger.info('Third-party service internal operation - no action required');
return;
}
-
- // Allow other errors through
- originalConsoleError.apply(console, args);
+
+ // Log other errors through logger
+ logger.error(message, { args }, false);
};
console.warn = (...args) => {
const message = args.join(' ');
-
- const shouldSuppress = suppressedErrorPatterns.some(pattern =>
+
+ const shouldSuppress = suppressedErrorPatterns.some(pattern =>
message.includes(pattern)
);
-
+
if (shouldSuppress) {
return; // Suppress video player warnings
}
-
- originalConsoleWarn.apply(console, args);
+
+ logger.warn(message, { args });
};
// Global error handler for unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
const message = event.reason?.toString() || '';
-
- const shouldSuppress = suppressedErrorPatterns.some(pattern =>
+
+ const shouldSuppress = suppressedErrorPatterns.some(pattern =>
message.includes(pattern)
);
-
+
if (shouldSuppress) {
event.preventDefault(); // Prevent the error from being logged
return;
}
+
+ logger.error('Unhandled promise rejection', event.reason, false);
});
// Global error handler for runtime errors
window.addEventListener('error', (event) => {
const message = event.message || '';
-
- const shouldSuppress = suppressedErrorPatterns.some(pattern =>
+
+ const shouldSuppress = suppressedErrorPatterns.some(pattern =>
message.includes(pattern)
);
-
+
if (shouldSuppress) {
event.preventDefault(); // Prevent the error from being logged
return;
}
+
+ logger.error('Runtime error', event.message, false);
});
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
+
);
diff --git a/src/utils/apiTest.js b/src/utils/apiTest.js
index 023ad9e..1503ebf 100644
--- a/src/utils/apiTest.js
+++ b/src/utils/apiTest.js
@@ -1,16 +1,17 @@
// API Testing utilities for The Nexus
-import {
- testTMDBConnection,
+import {
+ testTMDBConnection,
testVidSrcConnection,
fetchTrendingMovies,
fetchPopularMovies,
getMovieEmbedUrl,
getEpisodeEmbedUrl
} from './vidsrcApi';
+import { logger } from './logger.js';
// Comprehensive API testing function
export const runAPITests = async () => {
- console.log('š Starting API Tests for The Nexus...\n');
+ logger.info('š Starting API Tests for The Nexus...\n');
const results = {
tmdb: { success: false, error: null },
@@ -20,83 +21,83 @@ export const runAPITests = async () => {
};
// Test 1: TMDB API Connection
- console.log('š” Testing TMDB API Connection...');
+ logger.info('š” Testing TMDB API Connection...');
try {
const tmdbTest = await testTMDBConnection();
results.tmdb = tmdbTest;
- console.log(tmdbTest.success ? 'ā
TMDB API: Connected' : 'ā TMDB API: Failed');
+ logger.info(tmdbTest.success ? 'ā
TMDB API: Connected' : 'ā TMDB API: Failed');
} catch (error) {
results.tmdb = { success: false, error: error.message };
- console.log('ā TMDB API: Connection Error');
+ logger.info('ā TMDB API: Connection Error');
}
// Test 2: VidSrc API Connection
- console.log('šŗ Testing VidSrc API Connection...');
+ logger.info('šŗ Testing VidSrc API Connection...');
try {
const vidsrcTest = await testVidSrcConnection();
results.vidsrc = vidsrcTest;
- console.log(vidsrcTest.success ? 'ā
VidSrc API: Connected' : 'ā VidSrc API: Failed');
+ logger.info(vidsrcTest.success ? 'ā
VidSrc API: Connected' : 'ā VidSrc API: Failed');
} catch (error) {
results.vidsrc = { success: false, error: error.message };
- console.log('ā VidSrc API: Connection Error');
+ logger.info('ā VidSrc API: Connection Error');
}
// Test 3: TMDB Data Fetching
- console.log('š¬ Testing TMDB Data Fetching...');
+ logger.info('š¬ Testing TMDB Data Fetching...');
try {
const [trending, popular] = await Promise.all([
fetchTrendingMovies(),
fetchPopularMovies()
]);
-
+
if (trending && trending.results && popular && popular.results) {
- results.movieData = {
- success: true,
+ results.movieData = {
+ success: true,
trendingCount: trending.results.length,
popularCount: popular.results.length
};
- console.log(`ā
Movie Data: ${trending.results.length} trending, ${popular.results.length} popular`);
+ logger.info(`ā
Movie Data: ${trending.results.length} trending, ${popular.results.length} popular`);
} else {
results.movieData = { success: false, error: 'No data returned' };
- console.log('ā Movie Data: No results found');
+ logger.info('ā Movie Data: No results found');
}
} catch (error) {
results.movieData = { success: false, error: error.message };
- console.log('ā Movie Data: Fetch Error');
+ logger.info('ā Movie Data: Fetch Error');
}
// Test 4: Streaming URL Generation
- console.log('š Testing Streaming URL Generation...');
+ logger.info('š Testing Streaming URL Generation...');
try {
const movieUrl = getMovieEmbedUrl('385687', { autoplay: true, defaultLang: 'en' });
const episodeUrl = getEpisodeEmbedUrl('1399', 1, 1, { autoplay: true, autonext: true });
-
+
if (movieUrl && episodeUrl) {
- results.streaming = {
- success: true,
+ results.streaming = {
+ success: true,
movieUrl: movieUrl.substring(0, 50) + '...',
episodeUrl: episodeUrl.substring(0, 50) + '...'
};
- console.log('ā
Streaming URLs: Generated successfully');
+ logger.info('ā
Streaming URLs: Generated successfully');
} else {
results.streaming = { success: false, error: 'URL generation failed' };
- console.log('ā Streaming URLs: Generation failed');
+ logger.info('ā Streaming URLs: Generation failed');
}
} catch (error) {
results.streaming = { success: false, error: error.message };
- console.log('ā Streaming URLs: Generation error');
+ logger.info('ā Streaming URLs: Generation error');
}
// Summary
- console.log('\nš API Test Summary:');
- console.log('='.repeat(50));
-
+ logger.info('\nš API Test Summary:');
+ logger.info('='.repeat(50));
+
const allSuccess = Object.values(results).every(r => r.success);
- console.log(`Overall Status: ${allSuccess ? 'ā
All APIs Working' : 'ā ļø Some Issues Found'}`);
-
+ logger.info(`Overall Status: ${allSuccess ? 'ā
All APIs Working' : 'ā ļø Some Issues Found'}`);
+
Object.entries(results).forEach(([key, result]) => {
const status = result.success ? 'ā
' : 'ā';
- console.log(`${status} ${key.toUpperCase()}: ${result.success ? 'OK' : result.error}`);
+ logger.info(`${status} ${key.toUpperCase()}: ${result.success ? 'OK' : result.error}`);
});
return results;
diff --git a/src/utils/firebase.js b/src/utils/firebase.js
index c777ef4..80b6040 100644
--- a/src/utils/firebase.js
+++ b/src/utils/firebase.js
@@ -1,6 +1,7 @@
import { initializeApp } from "firebase/app";
// import { getAnalytics, isSupported } from "firebase/analytics"; // Disabled to prevent GTM conflicts
import { getAuth } from "firebase/auth";
+import { logger } from './logger.js';
// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
@@ -24,9 +25,9 @@ let analytics = null;
// Analytics disabled to prevent Google Tag Manager conflicts and related errors
// Firebase Analytics was causing googletag.destroySlots errors and ERR_UNSAFE_REDIRECT issues
try {
- console.log('[NEXUS] Firebase Analytics disabled to prevent GTM conflicts');
+ logger.info('Firebase Analytics disabled to prevent GTM conflicts');
} catch (error) {
- console.log('[NEXUS] Firebase Analytics error caught:', error);
+ logger.info('Firebase Analytics error caught:', error);
}
export const auth = getAuth(app);
diff --git a/src/utils/logger.js b/src/utils/logger.js
new file mode 100644
index 0000000..ef6dab8
--- /dev/null
+++ b/src/utils/logger.js
@@ -0,0 +1,91 @@
+import * as Sentry from '@sentry/react';
+import { toast } from 'react-toastify';
+
+// Initialize Sentry (this will be called in index.js)
+export const initSentry = () => {
+ if (process.env.REACT_APP_SENTRY_DSN) {
+ Sentry.init({
+ dsn: process.env.REACT_APP_SENTRY_DSN,
+ environment: process.env.NODE_ENV,
+ tracesSampleRate: 1.0,
+ });
+ }
+};
+
+// Logger class
+class Logger {
+ constructor() {
+ this.isDevelopment = process.env.NODE_ENV === 'development';
+ }
+
+ // Info level logging
+ info(message, ...args) {
+ if (this.isDevelopment) {
+ console.log(`[NEXUS INFO] ${message}`, ...args);
+ } else {
+ Sentry.captureMessage(message, 'info');
+ }
+ }
+
+ // Warning level logging
+ warn(message, ...args) {
+ if (this.isDevelopment) {
+ console.warn(`[NEXUS WARN] ${message}`, ...args);
+ } else {
+ Sentry.captureMessage(message, 'warning');
+ }
+ }
+
+ // Error level logging
+ error(message, error = null, showToast = false, ...args) {
+ if (this.isDevelopment) {
+ console.error(`[NEXUS ERROR] ${message}`, error, ...args);
+ } else {
+ if (error) {
+ Sentry.captureException(error, {
+ tags: { message },
+ extra: { args }
+ });
+ } else {
+ Sentry.captureMessage(message, 'error');
+ }
+ }
+
+ // Show user-facing error toast if requested
+ if (showToast) {
+ toast.error(message, {
+ position: "top-right",
+ autoClose: 5000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ });
+ }
+ }
+
+ // Debug logging (only in development)
+ debug(message, ...args) {
+ if (this.isDevelopment) {
+ console.debug(`[NEXUS DEBUG] ${message}`, ...args);
+ }
+ }
+
+ // API call logging
+ apiCall(endpoint, method = 'GET', success = true, error = null) {
+ const message = `API ${method} ${endpoint}: ${success ? 'SUCCESS' : 'FAILED'}`;
+ if (success) {
+ this.info(message);
+ } else {
+ this.error(message, error);
+ }
+ }
+
+ // Mobile-specific logging
+ mobileEvent(event, data = {}) {
+ this.info(`Mobile ${event}`, data);
+ }
+}
+
+export const logger = new Logger();
+export default logger;
diff --git a/src/utils/safeNavigation.js b/src/utils/safeNavigation.js
index fcfa2e4..4ca2505 100644
--- a/src/utils/safeNavigation.js
+++ b/src/utils/safeNavigation.js
@@ -1,4 +1,5 @@
// Utility functions for safe navigation and error handling
+import { logger } from './logger.js';
export const safeOpenExternal = (url, options = {}) => {
try {
// Validate URL
@@ -6,7 +7,7 @@ export const safeOpenExternal = (url, options = {}) => {
const allowedHosts = ['www.youtube.com', 'youtube.com', 'youtu.be'];
if (!allowedHosts.includes(urlObj.hostname)) {
- console.warn('Blocked unsafe external URL:', url);
+ logger.warn('Blocked unsafe external URL:', url);
return false;
}
@@ -21,7 +22,7 @@ export const safeOpenExternal = (url, options = {}) => {
return false;
} catch (error) {
- console.warn('Failed to open external URL:', error);
+ logger.warn('Failed to open external URL', error);
return false;
}
};
@@ -30,20 +31,20 @@ export const safeNavigate = (path) => {
try {
// Validate internal path
if (typeof path !== 'string' || !path.startsWith('/')) {
- console.warn('Invalid navigation path:', path);
+ logger.warn('Invalid navigation path:', path);
return false;
}
window.location.href = path;
return true;
} catch (error) {
- console.warn('Navigation failed, using fallback:', error);
+ logger.warn('Navigation failed, using fallback', error);
try {
window.history.pushState({}, '', path);
window.location.reload();
return true;
} catch (fallbackError) {
- console.error('All navigation methods failed:', fallbackError);
+ logger.error('All navigation methods failed', fallbackError);
return false;
}
}
@@ -59,7 +60,7 @@ export const suppressRuntimeErrors = () => {
}
window.googletag.cmd = window.googletag.cmd || [];
window.googletag.destroySlots = window.googletag.destroySlots || function(slots) {
- console.log('[NEXUS] Runtime googletag.destroySlots intercepted');
+ logger.info('Runtime googletag.destroySlots intercepted');
return slots || [];
};
@@ -82,24 +83,24 @@ export const suppressRuntimeErrors = () => {
console.error = function(...args) {
const message = args.join(' ');
const stack = (new Error()).stack || '';
-
- const shouldSuppress = runtimePatterns.some(pattern =>
+
+ const shouldSuppress = runtimePatterns.some(pattern =>
message.includes(pattern) || stack.includes(pattern)
);
-
+
if (shouldSuppress) {
- console.log('[NEXUS] Runtime error suppressed:', message);
+ logger.info('Runtime error suppressed:', message);
return;
}
-
- originalError.apply(console, args);
+
+ logger.error(message, { args }, false);
};
// Additional window error handler
window.addEventListener('error', function(event) {
const message = event.message || '';
if (message.includes('googletag') || message.includes('destroySlots')) {
- console.log('[NEXUS] Runtime window error suppressed:', message);
+ logger.info('Runtime window error suppressed:', message);
event.preventDefault();
event.stopPropagation();
return false;
diff --git a/src/utils/vidsrcApi.js b/src/utils/vidsrcApi.js
index 0bc1212..0883023 100644
--- a/src/utils/vidsrcApi.js
+++ b/src/utils/vidsrcApi.js
@@ -6,6 +6,7 @@ import {
generateMockTVData,
initializeMobileOptimizations
} from './mobileApiHelper.js';
+import { logger } from './logger.js';
// Use environment variables for security
const API_KEY = process.env.REACT_APP_TMDB_API_KEY;
@@ -16,7 +17,7 @@ const BACKDROP_BASE_URL = 'https://image.tmdb.org/t/p/w1280';
// Initialize mobile optimizations on module load
const deviceInfo = initializeMobileOptimizations();
-console.log('TMDB API initialized for device:', deviceInfo);
+logger.info('TMDB API initialized for device:', deviceInfo);
// Enhanced fetch wrapper with mobile-specific optimizations
const fetchFromTMDB = async (endpoint, page = 1) => { // Added page parameter with default value
@@ -361,7 +362,7 @@ export const fetchLatestMoviesFromVidSrc = async (page = 1) => {
}
return await response.json();
} catch (error) {
- console.error('VidSrc Latest Movies Error:', error);
+ logger.error('VidSrc Latest Movies Error', error);
return { movies: [], error: error.message };
}
};
@@ -375,7 +376,7 @@ export const fetchLatestTVShowsFromVidSrc = async (page = 1) => {
}
return await response.json();
} catch (error) {
- console.error('VidSrc Latest TV Shows Error:', error);
+ logger.error('VidSrc Latest TV Shows Error', error);
return { tvshows: [], error: error.message };
}
};
@@ -389,7 +390,7 @@ export const fetchLatestEpisodesFromVidSrc = async (page = 1) => {
}
return await response.json();
} catch (error) {
- console.error('VidSrc Latest Episodes Error:', error);
+ logger.error('VidSrc Latest Episodes Error', error);
return { episodes: [], error: error.message };
}
};
@@ -397,12 +398,12 @@ export const fetchLatestEpisodesFromVidSrc = async (page = 1) => {
// Test TMDB API connection
export const testTMDBConnection = async () => {
try {
- console.log('Testing TMDB API connection...');
+ logger.info('Testing TMDB API connection...');
const response = await fetchFromTMDB('/configuration');
- console.log('TMDB API Connection Successful:', response);
+ logger.info('TMDB API Connection Successful:', response);
return { success: true, data: response };
} catch (error) {
- console.error('TMDB API Connection Failed:', error);
+ logger.error('TMDB API Connection Failed', error);
return { success: false, error: error.message };
}
};
@@ -410,12 +411,12 @@ export const testTMDBConnection = async () => {
// Test VidSrc API connection
export const testVidSrcConnection = async () => {
try {
- console.log('Testing VidSrc API connection...');
+ logger.info('Testing VidSrc API connection...');
const response = await fetchLatestMoviesFromVidSrc(1);
- console.log('VidSrc API Connection Successful:', response);
+ logger.info('VidSrc API Connection Successful:', response);
return { success: true, data: response };
} catch (error) {
- console.error('VidSrc API Connection Failed:', error);
+ logger.error('VidSrc API Connection Failed', error);
return { success: false, error: error.message };
}
-};
\ No newline at end of file
+};