From 98c111967f41d1bb2665df97d7cd5ba6e5b5fdcb Mon Sep 17 00:00:00 2001 From: Son Nguyen Date: Sat, 21 Sep 2024 00:34:52 -0400 Subject: [PATCH] Final: Enhance app functionalities (#196) --- .../www/MovieVerse-Frontend/css/style.css | 51 +++- .../www/MovieVerse-Frontend/js/chat.js | 168 ++++++++++- .../www/MovieVerse-Frontend/js/chatbot.js | 2 +- .../www/MovieVerse-Frontend/js/comments-tv.js | 112 +++++-- .../www/MovieVerse-Frontend/js/comments.js | 112 +++++-- .../www/MovieVerse-Frontend/js/favorites.js | 74 ++--- .../MovieVerse-Frontend/js/user-profile.js | 278 +++++++++++------- 7 files changed, 583 insertions(+), 214 deletions(-) diff --git a/MovieVerse-Mobile/www/MovieVerse-Frontend/css/style.css b/MovieVerse-Mobile/www/MovieVerse-Frontend/css/style.css index 7a3bfbab..d6261880 100644 --- a/MovieVerse-Mobile/www/MovieVerse-Frontend/css/style.css +++ b/MovieVerse-Mobile/www/MovieVerse-Frontend/css/style.css @@ -798,6 +798,16 @@ header h1 { } } +@media print { + #mobileGoogleSignInBtn { + display: none; + } + + #mobileProfileBtn { + display: none; + } +} + .back-btn:hover { background-color: #ff8623; transition: 0.3s ease-in; @@ -1282,6 +1292,15 @@ header h1 { } } +@media print { + .movie-image { + height: auto; + } + #actor-image { + height: auto; + } +} + .movie-header { text-align: center; align-self: center; @@ -1456,7 +1475,7 @@ header h1 { border-radius: 8px; } -@media (max-width: 768px) { +@media all and (max-width: 768px) { .company-item { max-width: 125px; } @@ -2335,7 +2354,7 @@ strong { box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); } -@media (max-width: 700px) { +@media all and (max-width: 700px) { #settings-container { max-width: calc(100% - 10px); margin: 10px 10px; @@ -2359,7 +2378,7 @@ strong { display: none; } -@media (min-width: 768px) { +@media all and (min-width: 768px) { #chat-button, #settings-btn, #movie-of-the-day-btn, @@ -2394,7 +2413,7 @@ strong { transition: transform 0.09s ease-in-out; } -@media (max-width: 767px) { +@media all and (max-width: 767px) { .mobile-bottom-bar { display: flex; } @@ -2433,7 +2452,7 @@ strong { color: #ecf0f1; } -@media (max-width: 767px) { +@media all and (max-width: 767px) { .mobile-bottom-bar { display: flex; } @@ -2600,7 +2619,7 @@ strong { text-align: center; } -@media (max-width: 767px) { +@media all and (max-width: 767px) { .genres + main + .pagination { text-align: center; } @@ -2951,14 +2970,14 @@ ol li:hover { border-radius: 5px; } -@media (max-width: 705px) { +@media all and (max-width: 705px) { #search-title, #alt-title { display: none; } } -@media (max-height: 930px) { +@media all and (max-height: 930px) { #search-title, #alt-title { display: none; @@ -2975,7 +2994,7 @@ ol li:hover { align-self: flex-start; } -@media (max-width: 767px) { +@media all and (max-width: 767px) { #chatbotContainer { top: 56.1%; } @@ -3077,7 +3096,7 @@ ol li:hover { color: #ff8623; } -@media (max-width: 982px) { +@media all and (max-width: 982px) { #local-time { display: none; } @@ -3148,7 +3167,7 @@ canvas { cursor: pointer; } -@media screen and (max-width: 900px) { +@media all and (max-width: 900px) { .chart-container { flex-direction: column; } @@ -3199,7 +3218,7 @@ canvas { transition: 0.1s ease-in; } -@media (max-width: 1000px) { +@media all and (max-width: 1000px) { #comments-section { margin-bottom: 210px; } @@ -3217,7 +3236,7 @@ canvas { transition: 0.1s ease-in; } -@media (max-width: 800px) { +@media all and (max-width: 800px) { #my-heading { margin-left: 0; } @@ -3628,7 +3647,7 @@ canvas { background-color: #ff8623; } -@media screen and (-webkit-min-device-pixel-ratio: 0) { +@media all and (-webkit-min-device-pixel-ratio: 0) { body { background-size: auto; background-repeat: repeat; @@ -3936,7 +3955,7 @@ body { color: purple; } -@media (max-width: 800px) { +@media all and (max-width: 800px) { .company-details-container { display: flex; flex-direction: column; @@ -4207,7 +4226,7 @@ footer { color: white; } -@media (max-width: 900px) { +@media all and (max-width: 900px) { .notification-modal { right: 12.5px; } diff --git a/MovieVerse-Mobile/www/MovieVerse-Frontend/js/chat.js b/MovieVerse-Mobile/www/MovieVerse-Frontend/js/chat.js index 25fecf0e..ea37a061 100644 --- a/MovieVerse-Mobile/www/MovieVerse-Frontend/js/chat.js +++ b/MovieVerse-Mobile/www/MovieVerse-Frontend/js/chat.js @@ -214,6 +214,21 @@ const noUserSelected = document.getElementById('noUserSelected'); chatSection.style.display = 'none'; noUserSelected.style.display = 'flex'; +const LOCAL_STORAGE_MESSAGES_KEY_PREFIX = 'movieVerseMessagesCache'; + +function getCachedMessages(conversationKey) { + const cachedData = localStorage.getItem(LOCAL_STORAGE_MESSAGES_KEY_PREFIX + conversationKey); + return cachedData ? JSON.parse(cachedData) : []; +} + +function updateMessageCache(conversationKey, messages) { + localStorage.setItem(LOCAL_STORAGE_MESSAGES_KEY_PREFIX + conversationKey, JSON.stringify(messages)); +} + +function clearMessageCache(conversationKey) { + localStorage.removeItem(LOCAL_STORAGE_MESSAGES_KEY_PREFIX + conversationKey); +} + async function loadMessages(userEmail) { selectedUserEmail = userEmail; messagesDiv.innerHTML = ''; @@ -232,6 +247,17 @@ async function loadMessages(userEmail) { selectedUser.classList.add('selected'); } + const conversationKey = `${currentUserEmail}_${selectedUserEmail}`; + + const cachedMessages = getCachedMessages(conversationKey); + if (cachedMessages.length > 0) { + cachedMessages.forEach(msg => { + const messageElement = formatMessage(msg.message, msg.isCurrentUser, msg.timestamp); + messagesDiv.appendChild(messageElement); + }); + messagesDiv.scrollTop = messagesDiv.scrollHeight; + } + const messagesQuery = query( collection(db, 'messages'), orderBy('timestamp'), @@ -240,6 +266,8 @@ async function loadMessages(userEmail) { ); onSnapshot(messagesQuery, snapshot => { + const newMessages = []; + messagesDiv.innerHTML = ''; snapshot.docs.forEach(doc => { const messageData = doc.data(); @@ -251,8 +279,16 @@ async function loadMessages(userEmail) { if (!isCurrentUser && (!messageData.readBy || !messageData.readBy.includes(currentUserEmail))) { updateReadStatus(doc.id); } + + newMessages.push({ + message: messageData.message, + isCurrentUser, + timestamp: timestamp ? timestamp.toDate().toISOString() : null, + }); }); + updateMessageCache(conversationKey, newMessages); + messagesDiv.scrollTop = messagesDiv.scrollHeight; }); } @@ -289,8 +325,53 @@ function setupSearchListeners() { }); } +const LOCAL_STORAGE_SEARCH_CACHE_KEY = 'movieVerseSearchCache'; + +function getCachedSearchResults(query) { + const cachedData = localStorage.getItem(LOCAL_STORAGE_SEARCH_CACHE_KEY); + if (cachedData) { + const searchCache = JSON.parse(cachedData); + return searchCache[query] ? searchCache[query].results : null; + } + return null; +} + +function updateSearchCache(query, results) { + const cachedData = localStorage.getItem(LOCAL_STORAGE_SEARCH_CACHE_KEY); + const searchCache = cachedData ? JSON.parse(cachedData) : {}; + searchCache[query] = { results, lastUpdated: Date.now() }; + localStorage.setItem(LOCAL_STORAGE_SEARCH_CACHE_KEY, JSON.stringify(searchCache)); +} + async function performSearch(searchText, isNewSearch = false) { const searchUserResults = document.getElementById('searchUserResults'); + const cachedResults = getCachedSearchResults(searchText); + + if (cachedResults && isNewSearch) { + searchUserResults.innerHTML = ''; + cachedResults.forEach(user => { + const userDiv = document.createElement('div'); + userDiv.className = 'user-search-result'; + userDiv.style.cursor = 'pointer'; + userDiv.addEventListener('click', () => loadMessages(user.email)); + + const img = document.createElement('img'); + img.src = user.imageUrl || '../../images/user-default.png'; + img.style.width = '33%'; + img.style.borderRadius = '8px'; + userDiv.appendChild(img); + + const textDiv = document.createElement('div'); + textDiv.style.width = '67%'; + textDiv.style.textAlign = 'left'; + textDiv.innerHTML = `${user.email}

${user.bio || ''}

`; + userDiv.appendChild(textDiv); + + searchUserResults.appendChild(userDiv); + }); + searchUserResults.style.display = 'block'; + return; + } try { showSpinner(); @@ -318,6 +399,7 @@ async function performSearch(searchText, isNewSearch = false) { lastVisible = querySnapshot.docs[querySnapshot.docs.length - 1]; } + const results = []; for (const doc of querySnapshot.docs) { const user = doc.data(); const userDiv = document.createElement('div'); @@ -346,6 +428,7 @@ async function performSearch(searchText, isNewSearch = false) { userDiv.appendChild(textDiv); searchUserResults.appendChild(userDiv); + results.push({ email: user.email, bio: user.bio, imageUrl }); } searchUserResults.style.display = 'block'; @@ -363,6 +446,10 @@ async function performSearch(searchText, isNewSearch = false) { loadMoreButton.style.display = 'none'; } } + + if (isNewSearch) { + updateSearchCache(searchText, results); + } } catch (error) { console.error('Error fetching user list: ', error); if (error.code === 'resource-exhausted') { @@ -379,12 +466,31 @@ async function performSearch(searchText, isNewSearch = false) { let previouslySelectedUserElement = null; +const LOCAL_STORAGE_USER_CACHE_KEY = 'movieVerseUserCache'; + +function getCachedUsers() { + const cachedData = localStorage.getItem(LOCAL_STORAGE_USER_CACHE_KEY); + return cachedData ? JSON.parse(cachedData) : {}; +} + +function updateUserCache(email, userData) { + const currentCache = getCachedUsers(); + currentCache[email] = userData; + localStorage.setItem(LOCAL_STORAGE_USER_CACHE_KEY, JSON.stringify(currentCache)); +} + +function clearUserCache() { + localStorage.removeItem(LOCAL_STORAGE_USER_CACHE_KEY); +} + +const inMemoryUserCache = {}; // In-memory cache for user data + async function loadUserList() { try { showSpinner(); animateLoadingDots(); - const userLimit = 5; + const userLimit = 10; const messageLimit = 30; const sentMessagesQuery = query( @@ -393,6 +499,7 @@ async function loadUserList() { where('sender', '==', currentUserEmail), limit(messageLimit) ); + const receivedMessagesQuery = query( collection(db, 'messages'), orderBy('timestamp', 'desc'), @@ -407,19 +514,37 @@ async function loadUserList() { receivedMessagesSnapshot.forEach(doc => userEmails.add(doc.data().sender)); let users = []; + const cachedUsers = getCachedUsers(); + const emailsToFetch = []; + for (let email of userEmails) { if (email) { - const userQuery = query(collection(db, 'MovieVerseUsers'), where('email', '==', email)); - const userSnapshot = await getDocs(userQuery); - userSnapshot.forEach(doc => { - let userData = doc.data(); - if (userData.email) { - users.push(userData); - } - }); + if (cachedUsers[email]) { + users.push(cachedUsers[email]); + inMemoryUserCache[email] = cachedUsers[email]; + } else if (inMemoryUserCache[email]) { + users.push(inMemoryUserCache[email]); + } else { + emailsToFetch.push(email); + } } } + if (emailsToFetch.length > 0) { + const userQuery = query(collection(db, 'MovieVerseUsers'), where('email', 'in', emailsToFetch.slice(0, 10))); + + const userSnapshot = await getDocs(userQuery); + userSnapshot.forEach(doc => { + const userData = doc.data(); + if (userData.email) { + userData.lastUpdated = Date.now(); + users.push(userData); + updateUserCache(userData.email, userData); + inMemoryUserCache[userData.email] = userData; + } + }); + } + users.sort((a, b) => { const aLastMessage = [...sentMessagesSnapshot.docs, ...receivedMessagesSnapshot.docs].find( doc => doc.data().sender === a.email || doc.data().recipient === a.email @@ -450,12 +575,27 @@ async function loadUserList() { previouslySelectedUserElement = userElement; }; - const profileQuery = query(collection(db, 'profiles'), where('__name__', '==', user.email)); - const profileSnapshot = await getDocs(profileQuery); let imageUrl = '../../images/user-default.png'; - if (!profileSnapshot.empty) { - const profileData = profileSnapshot.docs[0].data(); - imageUrl = profileData.profileImage || imageUrl; + if (cachedUsers[user.email] && cachedUsers[user.email].profileImage) { + imageUrl = cachedUsers[user.email].profileImage; + } else if (inMemoryUserCache[user.email] && inMemoryUserCache[user.email].profileImage) { + imageUrl = inMemoryUserCache[user.email].profileImage; + } else { + const profileQuery = query(collection(db, 'profiles'), where('__name__', '==', user.email)); + const profileSnapshot = await getDocs(profileQuery); + if (!profileSnapshot.empty) { + const profileData = profileSnapshot.docs[0].data(); + imageUrl = profileData.profileImage || imageUrl; + + if (cachedUsers[user.email]) { + cachedUsers[user.email].profileImage = imageUrl; + updateUserCache(user.email, cachedUsers[user.email]); + } + inMemoryUserCache[user.email] = { + ...inMemoryUserCache[user.email], + profileImage: imageUrl, + }; + } } const img = document.createElement('img'); diff --git a/MovieVerse-Mobile/www/MovieVerse-Frontend/js/chatbot.js b/MovieVerse-Mobile/www/MovieVerse-Frontend/js/chatbot.js index db8687e8..b45116a6 100644 --- a/MovieVerse-Mobile/www/MovieVerse-Frontend/js/chatbot.js +++ b/MovieVerse-Mobile/www/MovieVerse-Frontend/js/chatbot.js @@ -383,7 +383,7 @@ async function movieVerseResponse(message) { const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash', systemInstruction: - 'You are MovieVerse Assistant - an AI Chatbot of the MovieVerse App. You are here to help users with movie-related or any other general queries. You are trained and powered by MovieVerse AI and Google to provide the best assistance. You can also provide information about movies, actors, directors, genres, and companies, or recommend movies to users.', + "You are MovieVerse Assistant - an AI Chatbot of the MovieVerse App. You are here to help users with movie-related or any other general queries. You are trained and powered by MovieVerse AI and Google to provide the best assistance. You can also provide information about movies, actors, directors, genres, and companies, or recommend movies to users. If the user asks anything about you or your information, you must by default identify yourself as MovieVerse Assistant, trained by The MovieVerse creator - Son Nguyen, and you're here to provide assistance for any movie-related or any other general inquiries. If the user asks who Son Nguyen is, refer to his portfolio website at: https://sonnguyenhoang.com, LinkedIn at: https://www.linkedin.com/in/hoangsonw, and GitHub at: https://github.com/hoangsonww. If anyone asked who created or trained you, you must refer to Son Nguyen as your creator.", }); conversationHistory.push({ role: 'user', parts: [{ text: message }] }); diff --git a/MovieVerse-Mobile/www/MovieVerse-Frontend/js/comments-tv.js b/MovieVerse-Mobile/www/MovieVerse-Frontend/js/comments-tv.js index 8064c3e3..b2f4a292 100644 --- a/MovieVerse-Mobile/www/MovieVerse-Frontend/js/comments-tv.js +++ b/MovieVerse-Mobile/www/MovieVerse-Frontend/js/comments-tv.js @@ -27,6 +27,7 @@ commentForm.addEventListener('submit', async e => { tvSeriesId, }); commentForm.reset(); + clearCommentCache(tvSeriesId); fetchComments(); } catch (error) { console.log('Error adding comment: ', error); @@ -60,6 +61,21 @@ const commentsPerPage = 3; let totalComments = 0; let totalPages = 1; +const LOCAL_STORAGE_TV_COMMENT_KEY_PREFIX = 'movieVerseTvCommentsCache'; + +function getCachedComments(tvSeriesId) { + const cachedData = localStorage.getItem(LOCAL_STORAGE_TV_COMMENT_KEY_PREFIX + tvSeriesId); + return cachedData ? JSON.parse(cachedData) : null; +} + +function updateCommentCache(tvSeriesId, comments) { + localStorage.setItem(LOCAL_STORAGE_TV_COMMENT_KEY_PREFIX + tvSeriesId, JSON.stringify({ comments, lastUpdated: Date.now() })); +} + +function clearCommentCache(tvSeriesId) { + localStorage.removeItem(LOCAL_STORAGE_TV_COMMENT_KEY_PREFIX + tvSeriesId); +} + async function fetchComments() { try { const commentsContainer = document.getElementById('comments-container'); @@ -67,12 +83,55 @@ async function fetchComments() { commentsContainer.style.maxWidth = '100%'; const tvSeriesId = localStorage.getItem('selectedTvSeriesId'); + const cachedComments = getCachedComments(tvSeriesId); + + if (cachedComments && cachedComments.comments.length > 0) { + const allComments = cachedComments.comments; + totalComments = allComments.length; + totalPages = Math.ceil(totalComments / commentsPerPage); + + let startIndex = (currentPage - 1) * commentsPerPage; + let endIndex = startIndex + commentsPerPage; + const pageComments = allComments.slice(startIndex, endIndex); + + pageComments.forEach(comment => { + const commentDate = new Date(comment.commentDate); + const formattedDate = formatCommentDate(commentDate); + const formattedTime = formatAMPM(commentDate); + + const timezoneOffset = -commentDate.getTimezoneOffset() / 60; + const utcOffset = timezoneOffset >= 0 ? `UTC+${timezoneOffset}` : `UTC${timezoneOffset}`; + const commentElement = document.createElement('div'); + + commentElement.title = `Posted at ${formattedTime} ${utcOffset}`; + const commentStyle = ` + max-width: 100%; + word-wrap: break-word; + overflow-wrap: break-word; + margin-bottom: 1rem; + `; + commentElement.style.cssText = commentStyle; + commentElement.innerHTML = ` +

+ ${comment.userName} on ${formattedDate}: + ${comment.userComment} +

+ `; + commentsContainer.appendChild(commentElement); + }); + + document.getElementById('prev-page').disabled = currentPage <= 1; + document.getElementById('next-page').disabled = currentPage >= totalPages; + return; + } + const q = query(collection(db, 'comments'), where('tvSeriesId', '==', tvSeriesId), orderBy('commentDate', 'desc')); const querySnapshot = await getDocs(q); totalComments = querySnapshot.size; totalPages = Math.ceil(totalComments / commentsPerPage); + let commentsData = []; let index = 0; let displayedComments = 0; @@ -82,19 +141,24 @@ async function fetchComments() { commentsContainer.appendChild(noCommentsMsg); } else { querySnapshot.forEach(doc => { - if (index >= (currentPage - 1) * commentsPerPage && displayedComments < commentsPerPage) { - const comment = doc.data(); - - let commentDate; - if (comment.commentDate instanceof Timestamp) { - commentDate = comment.commentDate.toDate(); - } else if (typeof comment.commentDate === 'string') { - commentDate = new Date(comment.commentDate); - } else { - console.error('Unexpected commentDate format:', comment.commentDate); - return; - } + const comment = doc.data(); + let commentDate; + if (comment.commentDate instanceof Timestamp) { + commentDate = comment.commentDate.toDate(); + } else if (typeof comment.commentDate === 'string') { + commentDate = new Date(comment.commentDate); + } else { + console.error('Unexpected commentDate format:', comment.commentDate); + return; + } + + commentsData.push({ + userName: comment.userName, + userComment: comment.userComment, + commentDate: commentDate.toISOString(), + }); + if (index >= (currentPage - 1) * commentsPerPage && displayedComments < commentsPerPage) { const formattedDate = formatCommentDate(commentDate); const formattedTime = formatAMPM(commentDate); @@ -104,23 +168,27 @@ async function fetchComments() { commentElement.title = `Posted at ${formattedTime} ${utcOffset}`; const commentStyle = ` - max-width: 100%; - word-wrap: break-word; - overflow-wrap: break-word; - margin-bottom: 1rem; - `; + max-width: 100%; + word-wrap: break-word; + overflow-wrap: break-word; + margin-bottom: 1rem; + `; commentElement.style.cssText = commentStyle; commentElement.innerHTML = ` -

- ${comment.userName} on ${formattedDate}: - ${comment.userComment} -

- `; +

+ ${comment.userName} on ${formattedDate}: + ${comment.userComment} +

+ `; commentsContainer.appendChild(commentElement); displayedComments++; } index++; }); + + if (commentsData.length > 0) { + updateCommentCache(tvSeriesId, commentsData); + } } document.getElementById('prev-page').disabled = currentPage <= 1; diff --git a/MovieVerse-Mobile/www/MovieVerse-Frontend/js/comments.js b/MovieVerse-Mobile/www/MovieVerse-Frontend/js/comments.js index 85b0c03c..18fa74d9 100644 --- a/MovieVerse-Mobile/www/MovieVerse-Frontend/js/comments.js +++ b/MovieVerse-Mobile/www/MovieVerse-Frontend/js/comments.js @@ -27,6 +27,7 @@ commentForm.addEventListener('submit', async e => { movieId, }); commentForm.reset(); + clearCommentCache(movieId); fetchComments(); } catch (error) { console.log('Error adding comment: ', error); @@ -60,6 +61,21 @@ const commentsPerPage = 3; let totalComments = 0; let totalPages = 1; +const LOCAL_STORAGE_COMMENT_KEY_PREFIX = 'movieVerseCommentsCache'; + +function getCachedComments(movieId) { + const cachedData = localStorage.getItem(LOCAL_STORAGE_COMMENT_KEY_PREFIX + movieId); + return cachedData ? JSON.parse(cachedData) : null; +} + +function updateCommentCache(movieId, comments) { + localStorage.setItem(LOCAL_STORAGE_COMMENT_KEY_PREFIX + movieId, JSON.stringify({ comments, lastUpdated: Date.now() })); +} + +function clearCommentCache(movieId) { + localStorage.removeItem(LOCAL_STORAGE_COMMENT_KEY_PREFIX + movieId); +} + async function fetchComments() { try { const commentsContainer = document.getElementById('comments-container'); @@ -67,12 +83,55 @@ async function fetchComments() { commentsContainer.style.maxWidth = '100%'; const movieId = localStorage.getItem('selectedMovieId'); + const cachedComments = getCachedComments(movieId); + + if (cachedComments && cachedComments.comments.length > 0) { + const allComments = cachedComments.comments; + totalComments = allComments.length; + totalPages = Math.ceil(totalComments / commentsPerPage); + + let startIndex = (currentPage - 1) * commentsPerPage; + let endIndex = startIndex + commentsPerPage; + const pageComments = allComments.slice(startIndex, endIndex); + + pageComments.forEach(comment => { + const commentDate = new Date(comment.commentDate); + const formattedDate = formatCommentDate(commentDate); + const formattedTime = formatAMPM(commentDate); + + const timezoneOffset = -commentDate.getTimezoneOffset() / 60; + const utcOffset = timezoneOffset >= 0 ? `UTC+${timezoneOffset}` : `UTC${timezoneOffset}`; + const commentElement = document.createElement('div'); + + commentElement.title = `Posted at ${formattedTime} ${utcOffset}`; + const commentStyle = ` + max-width: 100%; + word-wrap: break-word; + overflow-wrap: break-word; + margin-bottom: 1rem; + `; + commentElement.style.cssText = commentStyle; + commentElement.innerHTML = ` +

+ ${comment.userName} on ${formattedDate}: + ${comment.userComment} +

+ `; + commentsContainer.appendChild(commentElement); + }); + + document.getElementById('prev-page').disabled = currentPage <= 1; + document.getElementById('next-page').disabled = currentPage >= totalPages; + return; + } + const q = query(collection(db, 'comments'), where('movieId', '==', movieId), orderBy('commentDate', 'desc')); const querySnapshot = await getDocs(q); totalComments = querySnapshot.size; totalPages = Math.ceil(totalComments / commentsPerPage); + let commentsData = []; let index = 0; let displayedComments = 0; @@ -82,19 +141,24 @@ async function fetchComments() { commentsContainer.appendChild(noCommentsMsg); } else { querySnapshot.forEach(doc => { - if (index >= (currentPage - 1) * commentsPerPage && displayedComments < commentsPerPage) { - const comment = doc.data(); - - let commentDate; - if (comment.commentDate instanceof Timestamp) { - commentDate = comment.commentDate.toDate(); - } else if (typeof comment.commentDate === 'string') { - commentDate = new Date(comment.commentDate); - } else { - console.error('Unexpected commentDate format:', comment.commentDate); - return; - } + const comment = doc.data(); + let commentDate; + if (comment.commentDate instanceof Timestamp) { + commentDate = comment.commentDate.toDate(); + } else if (typeof comment.commentDate === 'string') { + commentDate = new Date(comment.commentDate); + } else { + console.error('Unexpected commentDate format:', comment.commentDate); + return; + } + + commentsData.push({ + userName: comment.userName, + userComment: comment.userComment, + commentDate: commentDate.toISOString(), + }); + if (index >= (currentPage - 1) * commentsPerPage && displayedComments < commentsPerPage) { const formattedDate = formatCommentDate(commentDate); const formattedTime = formatAMPM(commentDate); @@ -104,23 +168,27 @@ async function fetchComments() { commentElement.title = `Posted at ${formattedTime} ${utcOffset}`; const commentStyle = ` - max-width: 100%; - word-wrap: break-word; - overflow-wrap: break-word; - margin-bottom: 1rem; - `; + max-width: 100%; + word-wrap: break-word; + overflow-wrap: break-word; + margin-bottom: 1rem; + `; commentElement.style.cssText = commentStyle; commentElement.innerHTML = ` -

- ${comment.userName} on ${formattedDate}: - ${comment.userComment} -

- `; +

+ ${comment.userName} on ${formattedDate}: + ${comment.userComment} +

+ `; commentsContainer.appendChild(commentElement); displayedComments++; } index++; }); + + if (commentsData.length > 0) { + updateCommentCache(movieId, commentsData); + } } document.getElementById('prev-page').disabled = currentPage <= 1; diff --git a/MovieVerse-Mobile/www/MovieVerse-Frontend/js/favorites.js b/MovieVerse-Mobile/www/MovieVerse-Frontend/js/favorites.js index ab3eb6fa..44af428a 100644 --- a/MovieVerse-Mobile/www/MovieVerse-Frontend/js/favorites.js +++ b/MovieVerse-Mobile/www/MovieVerse-Frontend/js/favorites.js @@ -1380,7 +1380,6 @@ function handleSearch() { localStorage.setItem('searchQuery', searchQuery); window.location.href = 'search.html'; } - async function loadWatchLists() { const displaySection = document.getElementById('watchlists-display-section'); @@ -1389,13 +1388,20 @@ async function loadWatchLists() { const currentUserEmail = localStorage.getItem('currentlySignedInMovieVerseUser'); + let watchlists = []; if (currentUserEmail) { - const q = query(collection(db, 'watchlists'), where('userEmail', '==', currentUserEmail)); - const querySnapshot = await getDocs(q); - const watchlists = querySnapshot.docs.map(doc => ({ - id: doc.id, - ...doc.data(), - })); + const cachedWatchlists = JSON.parse(localStorage.getItem('cachedWatchlists_' + currentUserEmail)) || []; + if (cachedWatchlists.length > 0) { + watchlists = cachedWatchlists; + } else { + const q = query(collection(db, 'watchlists'), where('userEmail', '==', currentUserEmail)); + const querySnapshot = await getDocs(q); + watchlists = querySnapshot.docs.map(doc => ({ + id: doc.id, + ...doc.data(), + })); + localStorage.setItem('cachedWatchlists_' + currentUserEmail, JSON.stringify(watchlists)); + } if (watchlists.length === 0) { displaySection.innerHTML = '

No watch lists found. Click on "Create Watch Lists" to start adding movies.

'; @@ -1419,7 +1425,6 @@ async function loadWatchLists() { } } else { let localWatchlists = JSON.parse(localStorage.getItem('localWatchlists')) || []; - if (localWatchlists.length === 0) { displaySection.innerHTML = '

No watch lists found. Start by adding movies to your watchlist.

'; } else { @@ -1440,13 +1445,19 @@ async function loadWatchLists() { let favoritesTVSeries = []; if (currentUserEmail) { - const usersRef = query(collection(db, 'MovieVerseUsers'), where('email', '==', currentUserEmail)); - const userSnapshot = await getDocs(usersRef); - - if (!userSnapshot.empty) { - const userData = userSnapshot.docs[0].data(); - favorites = userData.favoritesMovies || []; - favoritesTVSeries = userData.favoritesTVSeries || []; + const cachedFavorites = JSON.parse(localStorage.getItem('cachedFavorites_' + currentUserEmail)) || {}; + favorites = cachedFavorites.favorites || []; + favoritesTVSeries = cachedFavorites.favoritesTVSeries || []; + + if (favorites.length === 0 || favoritesTVSeries.length === 0) { + const usersRef = query(collection(db, 'MovieVerseUsers'), where('email', '==', currentUserEmail)); + const userSnapshot = await getDocs(usersRef); + if (!userSnapshot.empty) { + const userData = userSnapshot.docs[0].data(); + favorites = userData.favoritesMovies || []; + favoritesTVSeries = userData.favoritesTVSeries || []; + localStorage.setItem('cachedFavorites_' + currentUserEmail, JSON.stringify({ favorites, favoritesTVSeries })); + } } } else { favorites = JSON.parse(localStorage.getItem('moviesFavorited')) || []; @@ -1476,10 +1487,8 @@ async function loadWatchLists() { const moviesContainer = document.createElement('div'); moviesContainer.className = 'movies-container'; - for (const movieId of favorites) { - const movieCard = await fetchMovieDetails(movieId); - moviesContainer.appendChild(movieCard); - } + const movieCards = await Promise.all(favorites.map(fetchMovieDetails)); + movieCards.forEach(movieCard => moviesContainer.appendChild(movieCard)); favoritesDiv.appendChild(moviesContainer); displaySection.appendChild(favoritesDiv); @@ -1515,10 +1524,8 @@ async function loadWatchLists() { const moviesContainer = document.createElement('div'); moviesContainer.className = 'movies-container'; - for (const tvSeriesId of favoritesTVSeries) { - const tvSeriesCard = await fetchTVSeriesDetails(tvSeriesId); - moviesContainer.appendChild(tvSeriesCard); - } + const tvSeriesCards = await Promise.all(favoritesTVSeries.map(fetchTVSeriesDetails)); + tvSeriesCards.forEach(tvSeriesCard => moviesContainer.appendChild(tvSeriesCard)); favoritesDiv.appendChild(moviesContainer); displaySection.appendChild(favoritesDiv); @@ -1535,7 +1542,6 @@ async function loadWatchLists() { } catch (error) { if (error.code === 'resource-exhausted') { let localWatchlists = JSON.parse(localStorage.getItem('localWatchlists')) || []; - if (localWatchlists.length === 0) { displaySection.innerHTML = '

No watch lists found. Start by adding movies to your watchlist.

'; } else { @@ -1551,11 +1557,8 @@ async function loadWatchLists() { } } - let favorites = []; - let favoritesTVSeries = []; - - favorites = JSON.parse(localStorage.getItem('moviesFavorited')) || []; - favoritesTVSeries = JSON.parse(localStorage.getItem('favoritesTVSeries')) || []; + let favorites = JSON.parse(localStorage.getItem('moviesFavorited')) || []; + let favoritesTVSeries = JSON.parse(localStorage.getItem('favoritesTVSeries')) || []; if (favorites.length > 0) { const favoritesDiv = document.createElement('div'); @@ -1576,10 +1579,8 @@ async function loadWatchLists() { const moviesContainer = document.createElement('div'); moviesContainer.className = 'movies-container'; - for (const movieId of favorites) { - const movieCard = await fetchMovieDetails(movieId); - moviesContainer.appendChild(movieCard); - } + const movieCards = await Promise.all(favorites.map(fetchMovieDetails)); + movieCards.forEach(movieCard => moviesContainer.appendChild(movieCard)); favoritesDiv.appendChild(moviesContainer); displaySection.appendChild(favoritesDiv); @@ -1611,10 +1612,8 @@ async function loadWatchLists() { const moviesContainer = document.createElement('div'); moviesContainer.className = 'movies-container'; - for (const tvSeriesId of favoritesTVSeries) { - const tvSeriesCard = await fetchTVSeriesDetails(tvSeriesId); - moviesContainer.appendChild(tvSeriesCard); - } + const tvSeriesCards = await Promise.all(favoritesTVSeries.map(fetchTVSeriesDetails)); + tvSeriesCards.forEach(tvSeriesCard => moviesContainer.appendChild(tvSeriesCard)); favoritesDiv.appendChild(moviesContainer); displaySection.appendChild(favoritesDiv); @@ -1630,6 +1629,7 @@ async function loadWatchLists() { } } else { console.error('An error occurred:', error); + hideSpinner(); } } } diff --git a/MovieVerse-Mobile/www/MovieVerse-Frontend/js/user-profile.js b/MovieVerse-Mobile/www/MovieVerse-Frontend/js/user-profile.js index c8c41a52..8d9d81d8 100644 --- a/MovieVerse-Mobile/www/MovieVerse-Frontend/js/user-profile.js +++ b/MovieVerse-Mobile/www/MovieVerse-Frontend/js/user-profile.js @@ -170,38 +170,37 @@ async function performSearch(searchText) { showSpinner(); try { + const cachedSearchResults = localStorage.getItem(`movieVerseSearchCache_${searchText}`); + if (cachedSearchResults) { + const parsedCache = JSON.parse(cachedSearchResults); + const cacheAge = Date.now() - parsedCache.timestamp; + + if (cacheAge < 24 * 60 * 60 * 1000) { + displaySearchResults(parsedCache.results, searchText); + hideSpinner(); + return; + } + } + const userQuery = query(collection(db, 'profiles'), where('username', '>=', searchText), where('username', '<=', searchText + '\uf8ff')); const querySnapshot = await getDocs(userQuery); searchUserResults.innerHTML = ''; + if (querySnapshot.empty) { searchUserResults.innerHTML = `
No User with Username "${searchText}" found
`; searchUserResults.style.display = 'block'; + localStorage.setItem(`movieVerseSearchCache_${searchText}`, JSON.stringify({ results: [], timestamp: Date.now() })); } else { - searchUserResults.style.display = 'block'; + const results = []; querySnapshot.forEach(doc => { const user = doc.data(); - const userDiv = document.createElement('div'); - userDiv.className = 'user-search-result'; - userDiv.style.cursor = 'pointer'; - userDiv.addEventListener('click', () => loadProfile(doc.id)); - - const img = document.createElement('img'); - img.src = user.profileImage || '../../images/user-default.png'; - img.style.width = '33%'; - img.style.borderRadius = '8px'; - userDiv.appendChild(img); - - const textDiv = document.createElement('div'); - textDiv.style.width = '67%'; - textDiv.style.textAlign = 'left'; - textDiv.innerHTML = `${user.username}

Bio: ${ - user.bio || 'Not Set' - }

`; - userDiv.appendChild(textDiv); - - searchUserResults.appendChild(userDiv); + results.push({ id: doc.id, ...user }); }); + + localStorage.setItem(`movieVerseSearchCache_${searchText}`, JSON.stringify({ results, timestamp: Date.now() })); + + displaySearchResults(results, searchText); } hideSpinner(); } catch (error) { @@ -212,6 +211,41 @@ async function performSearch(searchText) { } } +function displaySearchResults(results, searchText) { + const searchUserResults = document.getElementById('searchUserResults'); + searchUserResults.innerHTML = ''; + + if (results.length === 0) { + searchUserResults.innerHTML = `
No User with Username "${searchText}" found
`; + searchUserResults.style.display = 'block'; + return; + } + + results.forEach(user => { + const userDiv = document.createElement('div'); + userDiv.className = 'user-search-result'; + userDiv.style.cursor = 'pointer'; + userDiv.addEventListener('click', () => loadProfile(user.id)); + + const img = document.createElement('img'); + img.src = user.profileImage || '../../images/user-default.png'; + img.style.width = '33%'; + img.style.borderRadius = '8px'; + userDiv.appendChild(img); + + const textDiv = document.createElement('div'); + textDiv.style.width = '67%'; + textDiv.style.textAlign = 'left'; + textDiv.innerHTML = `${user.username}

Bio: ${ + user.bio || 'Not Set' + }

`; + userDiv.appendChild(textDiv); + + searchUserResults.appendChild(userDiv); + }); + searchUserResults.style.display = 'block'; +} + document.getElementById('container1').addEventListener('click', async () => { const userEmail = localStorage.getItem('currentlyViewingProfile'); @@ -348,9 +382,23 @@ async function loadProfile(userEmail = localStorage.getItem('currentlySignedInMo followUnfollowBtn.style.display = 'none'; } - try { + const cacheKey = `movieVerseProfileCache_${userEmail}`; + const cachedData = localStorage.getItem(cacheKey); + let profile = null; + + if (cachedData) { + const parsedCache = JSON.parse(cachedData); + const cacheAge = Date.now() - parsedCache.timestamp; + + if (cacheAge < 24 * 60 * 60 * 1000) { + profile = parsedCache.profile; + } + } + + if (!profile) { const docSnap = await getDoc(docRef); - let profile = { + + profile = { username: 'N/A', dob: 'N/A', bio: 'N/A', @@ -366,91 +414,46 @@ async function loadProfile(userEmail = localStorage.getItem('currentlySignedInMo if (docSnap.exists()) { profile = { ...profile, ...docSnap.data() }; - const imageUrl = profile.profileImage || '../../images/user-default.png'; - document.getElementById('profileImage').src = imageUrl; - - if ( - userEmail !== localStorage.getItem('currentlySignedInMovieVerseUser') || - !localStorage.getItem('currentlySignedInMovieVerseUser') || - !JSON.parse(localStorage.getItem('isSignedIn')) || - profile.profileImage === '../../images/user-default.png' - ) { - removeProfileImageBtn.style.display = 'none'; - } else { - removeProfileImageBtn.style.display = 'inline'; - } - - document.getElementById('usernameDisplay').innerHTML = `Username: ${profile.username}`; - document.getElementById('dobDisplay').innerHTML = `Date of Birth: ${profile.dob}`; - document.getElementById('bioDisplay').innerHTML = `Bio: ${profile.bio}`; - document.getElementById('favoriteGenresDisplay').innerHTML = `Favorite Genres: ${profile.favoriteGenres}`; - document.getElementById('locationDisplay').innerHTML = `Location: ${profile.location}`; - document.getElementById('favoriteMovieDisplay').innerHTML = `Favorite Movie: ${profile.favoriteMovie}`; - document.getElementById('hobbiesDisplay').innerHTML = `Hobbies: ${profile.hobbies}`; - document.getElementById('favoriteActorDisplay').innerHTML = `Favorite Actor: ${profile.favoriteActor}`; - document.getElementById('favoriteDirectorDisplay').innerHTML = `Favorite Director: ${profile.favoriteDirector}`; - document.getElementById('personalQuoteDisplay').innerHTML = `Personal Quote: ${profile.personalQuote}`; - window.document.title = `${profile.username !== 'N/A' ? profile.username : 'User'}'s Profile - The MovieVerse`; - - if (userEmail === localStorage.getItem('currentlySignedInMovieVerseUser')) { - welcomeMessage.textContent = `Welcome, ${profile.username}!`; - } else { - welcomeMessage.textContent = `Viewing ${profile.username}'s profile`; - } + } - hideSpinner(); + localStorage.setItem(cacheKey, JSON.stringify({ profile, timestamp: Date.now() })); + } - await Promise.all([displayUserList('following', userEmail), displayUserList('followers', userEmail)]); - } else { - const imageUrl = profile.profileImage || '../../images/user-default.png'; - document.getElementById('profileImage').src = imageUrl; - - if ( - userEmail !== localStorage.getItem('currentlySignedInMovieVerseUser') || - !localStorage.getItem('currentlySignedInMovieVerseUser') || - !JSON.parse(localStorage.getItem('isSignedIn')) || - profile.profileImage === '../../images/user-default.png' - ) { - removeProfileImageBtn.style.display = 'none'; - } else { - removeProfileImageBtn.style.display = 'inline'; - } + const imageUrl = profile.profileImage || '../../images/user-default.png'; + document.getElementById('profileImage').src = imageUrl; - document.getElementById('usernameDisplay').innerHTML = `Username: N/A`; - document.getElementById('dobDisplay').innerHTML = `Date of Birth: N/A`; - document.getElementById('bioDisplay').innerHTML = `Bio: N/A`; - document.getElementById('favoriteGenresDisplay').innerHTML = `Favorite Genres: N/A`; - document.getElementById('locationDisplay').innerHTML = `Location: N/A`; - document.getElementById('favoriteMovieDisplay').innerHTML = `Favorite Movie: N/A`; - document.getElementById('hobbiesDisplay').innerHTML = `Hobbies: N/A`; - document.getElementById('favoriteActorDisplay').innerHTML = `Favorite Actor: N/A`; - document.getElementById('favoriteDirectorDisplay').innerHTML = `Favorite Director: N/A`; - document.getElementById('personalQuoteDisplay').innerHTML = `Personal Quote: N/A`; - window.document.title = `${profile.username !== 'N/A' ? profile.username : 'User'}'s Profile - The MovieVerse`; - - if (userEmail === localStorage.getItem('currentlySignedInMovieVerseUser')) { - welcomeMessage.textContent = `Welcome, ${profile.username}!`; - } else { - welcomeMessage.textContent = `Viewing ${profile.username}'s profile`; - } + if ( + userEmail !== localStorage.getItem('currentlySignedInMovieVerseUser') || + !localStorage.getItem('currentlySignedInMovieVerseUser') || + !JSON.parse(localStorage.getItem('isSignedIn')) || + profile.profileImage === '../../images/user-default.png' + ) { + removeProfileImageBtn.style.display = 'none'; + } else { + removeProfileImageBtn.style.display = 'inline'; + } - hideSpinner(); + document.getElementById('usernameDisplay').innerHTML = `Username: ${profile.username}`; + document.getElementById('dobDisplay').innerHTML = `Date of Birth: ${profile.dob}`; + document.getElementById('bioDisplay').innerHTML = `Bio: ${profile.bio}`; + document.getElementById('favoriteGenresDisplay').innerHTML = `Favorite Genres: ${profile.favoriteGenres}`; + document.getElementById('locationDisplay').innerHTML = `Location: ${profile.location}`; + document.getElementById('favoriteMovieDisplay').innerHTML = `Favorite Movie: ${profile.favoriteMovie}`; + document.getElementById('hobbiesDisplay').innerHTML = `Hobbies: ${profile.hobbies}`; + document.getElementById('favoriteActorDisplay').innerHTML = `Favorite Actor: ${profile.favoriteActor}`; + document.getElementById('favoriteDirectorDisplay').innerHTML = `Favorite Director: ${profile.favoriteDirector}`; + document.getElementById('personalQuoteDisplay').innerHTML = `Personal Quote: ${profile.personalQuote}`; + window.document.title = `${profile.username !== 'N/A' ? profile.username : 'User'}'s Profile - The MovieVerse`; + + if (userEmail === localStorage.getItem('currentlySignedInMovieVerseUser')) { + welcomeMessage.textContent = `Welcome, ${profile.username}!`; + } else { + welcomeMessage.textContent = `Viewing ${profile.username}'s profile`; + } - await Promise.all([displayUserList('following', userEmail), displayUserList('followers', userEmail)]); - } - } catch (error) { - if (error.code === 'resource-exhausted') { - const noUserSelected = document.getElementById('profileContainer'); - if (noUserSelected) { - noUserSelected.innerHTML = - "Sorry, the profile feature is currently unavailable as our databases are overloaded. Please try reloading once more, and if that still doesn't work, please try again in a couple hours. For full transparency, we are currently using a database that has a limited number of reads and writes per day due to lack of funding. Thank you for your patience as we work on scaling our services. At the mean time, feel free to use other MovieVerse features!"; - noUserSelected.style.height = '350px'; - noUserSelected.style.display = 'block'; - } - } + hideSpinner(); - document.getElementById('viewMyProfileBtn').disabled = true; - } + await Promise.all([displayUserList('following', userEmail), displayUserList('followers', userEmail)]); } catch (error) { console.error('Error fetching user list: ', error); if (error.code === 'resource-exhausted') { @@ -472,6 +475,9 @@ async function displayUserList(listType, userEmail) { const listRef = collection(db, 'profiles', userEmail, listType); const userListSpan = document.getElementById(`${listType}List`); + const CACHE_KEY = `movieVerseUserListCache_${listType}_${userEmail}`; + const CACHE_EXPIRATION_MS = 24 * 60 * 60 * 1000; // 24 hours + let loadingInterval; function startLoadingAnimation() { let dots = ''; @@ -486,6 +492,46 @@ async function displayUserList(listType, userEmail) { userListSpan.innerHTML = ''; } + function loadFromCache() { + const cachedData = localStorage.getItem(CACHE_KEY); + if (cachedData) { + const parsedCache = JSON.parse(cachedData); + if (Date.now() - parsedCache.timestamp < CACHE_EXPIRATION_MS) { + return parsedCache.data; + } + } + return null; + } + + function saveToCache(data) { + const cacheEntry = { + data: data, + timestamp: Date.now(), + }; + localStorage.setItem(CACHE_KEY, JSON.stringify(cacheEntry)); + } + + const cachedData = loadFromCache(); + if (cachedData) { + userListSpan.innerHTML = ''; + cachedData.forEach(userData => { + const userLink = document.createElement('a'); + userLink.textContent = userData.username; + userLink.href = '#'; + userLink.id = 'userLink'; + userLink.style.cursor = 'pointer'; + userLink.onclick = () => loadProfile(userData.id); + + userListSpan.appendChild(userLink); + userListSpan.appendChild(document.createTextNode(', ')); + }); + + if (userListSpan.lastChild) { + userListSpan.removeChild(userListSpan.lastChild); + } + return; + } + startLoadingAnimation(); try { @@ -495,11 +541,14 @@ async function displayUserList(listType, userEmail) { if (snapshot.empty) { userListSpan.textContent = 'N/A'; } else { + const fetchedData = []; + for (let docSnapshot of snapshot.docs) { const userRef = doc(db, 'profiles', docSnapshot.id); const userSnap = await getDoc(userRef); if (userSnap.exists()) { const userData = userSnap.data(); + fetchedData.push({ id: docSnapshot.id, username: userData.username }); const userLink = document.createElement('a'); userLink.textContent = userData.username; @@ -516,6 +565,8 @@ async function displayUserList(listType, userEmail) { if (userListSpan.lastChild) { userListSpan.removeChild(userListSpan.lastChild); } + + saveToCache(fetchedData); } } catch (error) { console.error('Error fetching user list:', error); @@ -562,6 +613,10 @@ async function saveProfileChanges() { try { await setDoc(profileRef, profile, { merge: true }); console.log('Profile updated successfully.'); + + const cacheKey = `movieVerseProfileCache_${userEmail}`; + localStorage.setItem(cacheKey, JSON.stringify({ profile, timestamp: Date.now() })); + closeModal(); loadProfile(); } catch (error) { @@ -579,6 +634,15 @@ async function removeProfileImage() { await setDoc(doc(db, 'profiles', userEmail), { profileImage: defaultImageUrl }, { merge: true }); document.getElementById('profileImage').src = defaultImageUrl; document.getElementById('removeProfileImage').style.display = 'none'; + + const cacheKey = `movieVerseProfileCache_${userEmail}`; + const cachedProfile = JSON.parse(localStorage.getItem(cacheKey)); + + if (cachedProfile && cachedProfile.profile) { + cachedProfile.profile.profileImage = defaultImageUrl; + cachedProfile.timestamp = Date.now(); + localStorage.setItem(cacheKey, JSON.stringify(cachedProfile)); + } } catch (error) { console.log('Error removing image: ', error); } @@ -605,6 +669,16 @@ async function uploadImage() { document.getElementById('profileImage').src = base64Image; console.log('Image processed and Firestore updated'); + + const cacheKey = `movieVerseProfileCache_${userEmail}`; + const cachedProfile = JSON.parse(localStorage.getItem(cacheKey)); + + if (cachedProfile && cachedProfile.profile) { + cachedProfile.profile.profileImage = base64Image; + cachedProfile.timestamp = Date.now(); + localStorage.setItem(cacheKey, JSON.stringify(cachedProfile)); + } + window.location.reload(); } catch (error) { console.log('Error during image processing:', error);