From 5a1bee3ec3f724d0878036279b68c4d23b74737d Mon Sep 17 00:00:00 2001
From: Nunu Miah <nm01312@surrey.ac.uk>
Date: Thu, 22 May 2025 22:55:38 +0100
Subject: [PATCH] Added remove from watchlist button to watchlist view,
 switched the film details page to have a new toggle button from the
 watchlistbutton component that checks to see if its already in the database
 and some other minor changes in the navbar

---
 movie-group-8/src/components/Navbar.vue       |  3 +-
 .../src/components/TopRatedMovies.vue         | 75 ++++++++++---------
 movie-group-8/src/components/Watchlist.vue    | 18 ++++-
 .../src/components/WatchlistButton.vue        | 37 +++++++++
 movie-group-8/src/composables/useWatchlist.js | 53 ++++++++++---
 movie-group-8/src/views/FilmDetails.vue       | 23 ++++--
 movie-group-8/src/views/Login.vue             | 12 +--
 movie-group-8/src/views/Social.vue            |  3 +-
 movie-group-8/src/views/WatchlistView.vue     | 47 ++++++++----
 9 files changed, 194 insertions(+), 77 deletions(-)
 create mode 100644 movie-group-8/src/components/WatchlistButton.vue

diff --git a/movie-group-8/src/components/Navbar.vue b/movie-group-8/src/components/Navbar.vue
index 1a4d4d9..64d7e57 100644
--- a/movie-group-8/src/components/Navbar.vue
+++ b/movie-group-8/src/components/Navbar.vue
@@ -74,6 +74,7 @@ import { ref, computed, onMounted } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import { getAuth, onAuthStateChanged, signOut } from 'firebase/auth';
 import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue';
+import defaultAvatar from '@/assets/default_avatar.png'
 
 // Firebase Authentication instance
 const auth = getAuth(); 
@@ -110,7 +111,7 @@ onMounted(() => {
     if(user && user.photoURL) {
       userPhotoURL.value = user.photoURL;
     } else {
-      userPhotoURL.value = 'https://static.vecteezy.com/system/resources/previews/009/292/244/non_2x/default-avatar-icon-of-social-media-user-vector.jpg';
+      userPhotoURL.value = defaultAvatar;
     }
   });
 });
diff --git a/movie-group-8/src/components/TopRatedMovies.vue b/movie-group-8/src/components/TopRatedMovies.vue
index 5df1ea2..090e315 100644
--- a/movie-group-8/src/components/TopRatedMovies.vue
+++ b/movie-group-8/src/components/TopRatedMovies.vue
@@ -1,6 +1,5 @@
 <script setup>
 import { ref, onMounted, watch } from 'vue'
-import { addToWatchlist } from '@/composables/useWatchlist.js'
 
 const year = ref(2000)
 const genre = ref('')
@@ -28,41 +27,45 @@ watch([year, genre], async () => {
 </script>
 
 <template>
-    <div class="nyt-header">
-      <h1>The Movies We've Loved Since 2000</h1>
-      <p>
-        Explore top-rated films using the filters below — by genre and year — and rediscover hidden gems.
-      </p>
-    </div>
-  
-    <div class="filters">
-      <select v-model="genre">
-        <option value="">Pick a genre ...</option>
-        <option v-for="g in genres" :key="g.id" :value="g.id">{{ g.name }}</option>
-      </select>
-  
-      <select v-model="year">
-        <option v-for="y in Array.from({length: 25}, (_, i) => 2000 + i)" :key="y" :value="y">
-          {{ y }}
-        </option>
-      </select>
-    </div>
-  
-    <h2 class="section-title">Our favorite movies from {{ year }}</h2>
-  
-    <div class="movie-grid">
-      <div v-for="movie in movies" :key="movie.id" class="movie-card">
-        <img
-          v-if="movie.poster_path"
-          :src="'https://image.tmdb.org/t/p/w342' + movie.poster_path"
-          alt="poster"
-        />
-        <p class="title">{{ movie.title }}</p>
-        <p class="rating">⭐ {{ movie.vote_average }}</p>
-        <button @click="addToWatchlist(movie)">Add to Watchlist</button>
-      </div>
-    </div>
-  </template>
+  <div class="nyt-header">
+    <h1>The Movies We've Loved Since 2000</h1>
+    <p>
+      Explore top-rated films using the filters below — by genre and year — and rediscover hidden gems.
+    </p>
+  </div>
+
+  <div class="filters">
+    <select v-model="genre">
+      <option value="">Pick a genre ...</option>
+      <option v-for="g in genres" :key="g.id" :value="g.id">{{ g.name }}</option>
+    </select>
+
+    <select v-model="year">
+      <option v-for="y in Array.from({length: 25}, (_, i) => 2000 + i)" :key="y" :value="y">
+        {{ y }}
+      </option>
+    </select>
+  </div>
+
+  <h2 class="section-title">Our favorite movies from {{ year }}</h2>
+
+  <div class="movie-grid">
+    <router-link
+      v-for="movie in movies"
+      :key="movie.id"
+      :to="`/films/${movie.id}`"
+      class="movie-card block hover:opacity-90 transition"
+    >
+      <img
+        v-if="movie.poster_path"
+        :src="'https://image.tmdb.org/t/p/w342' + movie.poster_path"
+        alt="poster"
+      />
+      <p class="title">{{ movie.title }}</p>
+      <p class="rating">⭐ {{ movie.vote_average }}</p>
+    </router-link>
+  </div>
+</template>
   
 <style scoped>
 .nyt-header {
diff --git a/movie-group-8/src/components/Watchlist.vue b/movie-group-8/src/components/Watchlist.vue
index 968bb92..7240822 100644
--- a/movie-group-8/src/components/Watchlist.vue
+++ b/movie-group-8/src/components/Watchlist.vue
@@ -20,6 +20,12 @@
             <p class="title">{{ movie.title }}</p>
             <p class="rating">⭐ {{ movie.vote_average }}</p>
             <p class="status">📌 {{ movie.status }}</p>
+            <button
+                @click="removeFromWatchlist(movie)"
+                class="mt-2 px-3 py-1 bg-red-600 text-white rounded hover:bg-red-700 transition"
+                >
+                Remove
+            </button>
             </div>
         </div>
     </div>
@@ -27,7 +33,7 @@
   
 <script setup>
     import { ref, computed, onMounted } from 'vue'
-    import { getFirestore, collection, getDocs } from 'firebase/firestore'
+    import { getFirestore, collection, getDocs, doc, deleteDoc } from 'firebase/firestore'
     import { getAuth } from 'firebase/auth'
   
     const db = getFirestore()
@@ -43,6 +49,16 @@
         const snapshot = await getDocs(collection(db, 'users', user.uid, 'watchlist'))
         watchlist.value = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }))
     }
+
+    const removeFromWatchlist = async (movie) => {
+        const user = auth.currentUser
+        const movieRef = doc(db, 'users', user.uid, 'watchlist', String(movie.id))
+        await deleteDoc(movieRef)
+
+        // remove locally
+        watchlist.value = watchlist.value.filter(m => m.id !== movie.id)
+        alert(`Removed "${movie.title}" from your watchlist.`)
+    }
   
     const filteredWatchlist = computed(() => {
         if (!filter.value) return watchlist.value
diff --git a/movie-group-8/src/components/WatchlistButton.vue b/movie-group-8/src/components/WatchlistButton.vue
new file mode 100644
index 0000000..9145823
--- /dev/null
+++ b/movie-group-8/src/components/WatchlistButton.vue
@@ -0,0 +1,37 @@
+<template>
+  <button
+    type="button"
+    @click="toggleWatchlist"
+    class="mt-2 px-3 py-1 rounded text-white text-sm transition"
+    :class="isInWatchlist
+      ? 'bg-red-600 hover:bg-red-700'
+      : 'bg-green-600 hover:bg-green-700'"
+  >
+    {{ isInWatchlist ? 'Remove from Watchlist' : 'Add to Watchlist' }}
+  </button>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+import { useWatchlist } from '@/composables/useWatchlist.js'
+
+const props = defineProps({
+  movie: {
+    type: Object,
+    required: true
+  }
+})
+
+// We need a ref for the ID and a ref for the data
+const movieId = ref(props.movie.id)
+const movieData = ref(props.movie)
+
+// Pull in the composable
+const { isInWatchlist, toggleWatchlist } = useWatchlist(movieId, movieData)
+
+// If parent updates the prop, keep our ref in sync
+watch(() => props.movie, m => {
+  movieId.value = m.id
+  movieData.value = m
+})
+</script>
\ No newline at end of file
diff --git a/movie-group-8/src/composables/useWatchlist.js b/movie-group-8/src/composables/useWatchlist.js
index 7de0359..7dc09f0 100644
--- a/movie-group-8/src/composables/useWatchlist.js
+++ b/movie-group-8/src/composables/useWatchlist.js
@@ -1,18 +1,47 @@
-import { getFirestore, doc, setDoc } from 'firebase/firestore'
+import { ref, watchEffect } from 'vue'
 import { getAuth } from 'firebase/auth'
+import { getFirestore, doc, getDoc, setDoc, deleteDoc } from 'firebase/firestore'
 
-export const addToWatchlist = async (movie) => {
-  const user = getAuth().currentUser
-  if (!user) return alert('You need to log in.')
-
+export function useWatchlist(movieId, movieData) {
+  const auth = getAuth()
   const db = getFirestore()
-  const movieRef = doc(db, 'users', user.uid, 'watchlist', String(movie.id))
-  await setDoc(movieRef, {
-    title: movie.title,
-    poster_path: movie.poster_path,
-    vote_average: movie.vote_average,
-    status: 'planned'
+  const isInWatchlist = ref(false)
+
+  // Check on load & whenever movieId or user changes
+  watchEffect(async (onInvalidate) => {
+    const user = auth.currentUser
+    if (!user.value ?? user) {
+      isInWatchlist.value = false
+      return
+    }
+    const ref = doc(db, 'users', user.uid, 'watchlist', String(movieId.value ?? movieId))
+    const snap = await getDoc(ref)
+    isInWatchlist.value = snap.exists()
   })
 
-  alert('Movie added to watchlist!')
+  // Toggle function
+  const toggleWatchlist = async () => {
+    const user = auth.currentUser
+    if (!user) {
+      return alert('You need to log in.')
+    }
+    const ref = doc(db, 'users', user.uid, 'watchlist', String(movieId.value ?? movieId))
+
+    if (isInWatchlist.value) {
+      await deleteDoc(ref)
+      isInWatchlist.value = false
+      alert('Removed from watchlist')
+    } else {
+      await setDoc(ref, {
+        title: movieData.value.title,
+        poster_path: movieData.value.poster_path,
+        vote_average: movieData.value.vote_average,
+        status: 'planned'
+      })
+      isInWatchlist.value = true
+      alert('Added to watchlist')
+    }
+  }
+
+  return { isInWatchlist, toggleWatchlist }
 }
\ No newline at end of file
diff --git a/movie-group-8/src/views/FilmDetails.vue b/movie-group-8/src/views/FilmDetails.vue
index 8194f5c..fa1a9d8 100644
--- a/movie-group-8/src/views/FilmDetails.vue
+++ b/movie-group-8/src/views/FilmDetails.vue
@@ -30,12 +30,7 @@
           ></iframe>
         </div>
 
-        <button
-          @click="addToWatchlist(movie)"
-          class="mt-4 px-4 py-2 bg-green-600 rounded text-white hover:bg-green-700"
-        >
-          Add to Watchlist
-        </button>
+        <WatchlistButton :movie="movie" class="mt-4" />
 
         <RouterLink
           :to="{ name: 'ReviewFilm', params: { id: movie.id } }"
@@ -51,13 +46,18 @@
 <script setup>
 import { ref, onMounted } from 'vue'
 import { useRoute } from 'vue-router'
-import { addToWatchlist } from '@/composables/useWatchlist.js'
+import { useWatchlist } from '@/composables/useWatchlist.js'
+import WatchlistButton from '@/components/WatchlistButton.vue'
+import { getAuth } from 'firebase/auth'
+import { getFirestore, doc, getDoc } from 'firebase/firestore'
 
 const route = useRoute()
 const movie = ref(null)
 const trailerUrl = ref('')
 const loading = ref(true)
 const error = ref('')
+const movieId = ref(route.params.id)
+const { isInWatchlist } = useWatchlist(movieId, movie)
 
 function formatDate(dateString) {
   if (!dateString) return 'Unknown'
@@ -100,6 +100,15 @@ onMounted(async () => {
   const id = route.params.id
   await fetchMovieDetails(id)
   await fetchMovieVideos(id)
+
+  const user = getAuth().currentUser
+  if (user) {
+    const db = getFirestore()
+    const watchRef = doc(db, 'users', user.uid, 'watchlist', String(id))
+    const snap = await getDoc(watchRef)
+    isInWatchlist.value = snap.exists()
+  }
+
   loading.value = false
 })
 </script>
diff --git a/movie-group-8/src/views/Login.vue b/movie-group-8/src/views/Login.vue
index 909e2ed..0b539b1 100644
--- a/movie-group-8/src/views/Login.vue
+++ b/movie-group-8/src/views/Login.vue
@@ -88,17 +88,19 @@ const loading = ref(false);
 const db = getFirestore();
 
 const createUserIfNotExists = async (user) => {
-  const userRef = doc(db, 'users', user.uid);
-  const userSnap = await getDoc(userRef);
+  const userRef = doc(db, 'users', user.uid)
+  const userSnap = await getDoc(userRef)
 
   if (!userSnap.exists()) {
+    const photoURLToSave = user.photoURL || defaultAvatar
+
     await setDoc(userRef, {
       uid: user.uid,
       displayName: user.displayName || 'Anonymous',
-      photoURL: defaultAvatar
-    });
+      photoURL: photoURLToSave
+    })
   }
-};
+}
 
 const login = async () => {
   try {
diff --git a/movie-group-8/src/views/Social.vue b/movie-group-8/src/views/Social.vue
index 34bbed7..ed3b9b1 100644
--- a/movie-group-8/src/views/Social.vue
+++ b/movie-group-8/src/views/Social.vue
@@ -19,7 +19,7 @@
       >
         <router-link :to="`/user/${user.uid}`" class="flex items-center gap-4">
           <img
-            :src="user.photoURL || defaultAvatar"
+            :src="user.photoURL"
             class="w-12 h-12 rounded-full object-cover"
             alt="Avatar"
           />
@@ -58,7 +58,6 @@ const currentUser = auth.currentUser
 const searchTerm = ref('')
 const users = ref([])
 const following = ref([])
-const defaultAvatar = 'https://via.placeholder.com/150'
 
 onMounted(async () => {
   await fetchUsers()
diff --git a/movie-group-8/src/views/WatchlistView.vue b/movie-group-8/src/views/WatchlistView.vue
index 4ce8909..071b02e 100644
--- a/movie-group-8/src/views/WatchlistView.vue
+++ b/movie-group-8/src/views/WatchlistView.vue
@@ -10,12 +10,18 @@
           No movies planned yet.
         </div>
         <div class="grid gap-4">
-          <MovieCard
-            v-for="movie in plannedMovies"
-            :key="movie.id"
-            :movie="movie"
-            @status-changed="updateMovieStatus"
-          />
+          <div v-for="movie in plannedMovies" :key="movie.id" class="relative bg-white dark:bg-neutral-800 p-4 rounded">
+            <MovieCard
+              :movie="movie"
+              @status-changed="updateMovieStatus"
+            />
+            <button
+              @click="removeFromWatchlist(movie.id)"
+              class="absolute top-2 right-2 px-2 py-1 bg-red-600 text-white text-xs rounded hover:bg-red-700 transition"
+            >
+              Remove
+            </button>
+          </div>
         </div>
       </div>
 
@@ -26,12 +32,18 @@
           No watched movies yet.
         </div>
         <div class="grid gap-4">
-          <MovieCard
-            v-for="movie in watchedMovies"
-            :key="movie.id"
-            :movie="movie"
-            @status-changed="updateMovieStatus"
-          />
+          <div v-for="movie in watchedMovies" :key="movie.id" class="relative bg-white dark:bg-neutral-800 p-4 rounded">
+            <MovieCard
+              :movie="movie"
+              @status-changed="updateMovieStatus"
+            />
+            <button
+              @click="removeFromWatchlist(movie.id)"
+              class="absolute top-2 right-2 px-2 py-1 bg-red-600 text-white text-xs rounded hover:bg-red-700 transition"
+            >
+              Remove
+            </button>
+          </div>
         </div>
       </div>
     </div>
@@ -41,7 +53,7 @@
 <script setup>
 import { ref, onMounted } from 'vue'
 import { getAuth } from 'firebase/auth'
-import { getFirestore, collection, getDocs, doc, updateDoc } from 'firebase/firestore'
+import { getFirestore, collection, getDocs, doc, updateDoc, deleteDoc } from 'firebase/firestore'
 import MovieCard from '@/components/MovieCard.vue'
 
 const auth = getAuth()
@@ -60,6 +72,15 @@ const fetchWatchlist = async () => {
   watchedMovies.value = all.filter(m => m.status === 'watched')
 }
 
+const removeFromWatchlist = async (movieId) => {
+  const user = auth.currentUser
+  if (!user) return
+
+  const movieRef = doc(db, 'users', user.uid, 'watchlist', movieId)
+  await deleteDoc(movieRef)
+  await fetchWatchlist()
+}
+
 const updateMovieStatus = async (movieId, newStatus) => {
   const user = auth.currentUser
   if (!user) return
-- 
GitLab