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