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