From 0811c07a28d7f5fa9c09a9259d1a75475f51c044 Mon Sep 17 00:00:00 2001 From: Nunu Miah <nm01312@surrey.ac.uk> Date: Thu, 22 May 2025 20:19:27 +0100 Subject: [PATCH 1/3] Created Social tab to search for users and a public profile view to follow the user and see their watchlist and fixed login to add the user to firestore database if they do not exist --- movie-group-8/src/router/index.js | 14 ++- movie-group-8/src/views/Login.vue | 119 +++++++++++++++--------- movie-group-8/src/views/Social.vue | 100 ++++++++++++++++++++ movie-group-8/src/views/UserProfile.vue | 117 +++++++++++++++++++++++ 4 files changed, 305 insertions(+), 45 deletions(-) create mode 100644 movie-group-8/src/views/Social.vue create mode 100644 movie-group-8/src/views/UserProfile.vue diff --git a/movie-group-8/src/router/index.js b/movie-group-8/src/router/index.js index f4763b4..fcbec63 100644 --- a/movie-group-8/src/router/index.js +++ b/movie-group-8/src/router/index.js @@ -10,7 +10,7 @@ const router = createRouter({ { path: '/', component: () => import('../views/Home.vue') }, - {path: '/top-rated', component: () => import('../views/TopRated.vue'), meta: { requiresAuth: true }}, + { path: '/top-rated', component: () => import('../views/TopRated.vue'), meta: { requiresAuth: true }}, { path: '/register', component: () => import('../views/Register.vue') }, { path: '/login', component: () => import('../views/Login.vue') }, { path: '/recover-account', component: () => import('../views/RecoverAccount.vue') }, @@ -48,7 +48,17 @@ const router = createRouter({ component: () => import('../views/Settings.vue'), meta: { requiresAuth: true }, }, - + { + path: '/social', + name: 'Social', + component: () => import('../views/Social.vue'), + meta: { requiresAuth: true }, + }, + { + path: '/user/:id', + name: 'UserProfile', + component: () => import('../views/UserProfile.vue') + }, ], }); diff --git a/movie-group-8/src/views/Login.vue b/movie-group-8/src/views/Login.vue index eecffe4..45657bb 100644 --- a/movie-group-8/src/views/Login.vue +++ b/movie-group-8/src/views/Login.vue @@ -61,47 +61,80 @@ </div> </div> </div> - </template> +</template> - <script setup> - import { ref } from 'vue'; - import { getAuth, signInWithEmailAndPassword, signInWithPopup, GoogleAuthProvider } from 'firebase/auth'; - import { useRouter } from 'vue-router'; - - const email = ref(''); - const password = ref(''); - const router = useRouter(); - const loading = ref(false); - - const login = async () => { - try { - loading.value = true; - const auth = getAuth(); - await signInWithEmailAndPassword(auth, email.value, password.value); - console.log('Successfully logged in!'); - router.push('/films'); - } catch (error) { - console.error(error); - alert(error.message); - } finally { - loading.value = false; - } - }; - - const signInWithGoogle = async () => { - try { - loading.value = true; - const auth = getAuth(); - const provider = new GoogleAuthProvider(); - await signInWithPopup(auth, provider); - console.log('Successfully signed in with Google!'); - router.push('/films'); - } catch (error) { - console.error(error); - alert(error.message); - } finally { - loading.value = false; - } - }; - </script> - \ No newline at end of file +<script setup> +import { ref } from 'vue'; +import { + getAuth, + signInWithEmailAndPassword, + signInWithPopup, + GoogleAuthProvider +} from 'firebase/auth'; +import { + getFirestore, + doc, + getDoc, + setDoc +} from 'firebase/firestore'; +import { useRouter } from 'vue-router'; + +const email = ref(''); +const password = ref(''); +const router = useRouter(); +const loading = ref(false); + +const db = getFirestore(); + +const createUserIfNotExists = async (user) => { + const userRef = doc(db, 'users', user.uid); + const userSnap = await getDoc(userRef); + + if (!userSnap.exists()) { + await setDoc(userRef, { + uid: user.uid, + displayName: user.displayName || 'Anonymous', + photoURL: user.photoURL || '', + }); + } +}; + +const login = async () => { + try { + loading.value = true; + const auth = getAuth(); + const userCredential = await signInWithEmailAndPassword(auth, email.value, password.value); + const user = userCredential.user; + + await createUserIfNotExists(user); + + console.log('Successfully logged in!'); + router.push('/films'); + } catch (error) { + console.error(error); + alert(error.message); + } finally { + loading.value = false; + } +}; + +const signInWithGoogle = async () => { + try { + loading.value = true; + const auth = getAuth(); + const provider = new GoogleAuthProvider(); + const result = await signInWithPopup(auth, provider); + const user = result.user; + + await createUserIfNotExists(user); + + console.log('Successfully signed in with Google!'); + router.push('/films'); + } catch (error) { + console.error(error); + alert(error.message); + } finally { + loading.value = false; + } +}; +</script> \ No newline at end of file diff --git a/movie-group-8/src/views/Social.vue b/movie-group-8/src/views/Social.vue new file mode 100644 index 0000000..34bbed7 --- /dev/null +++ b/movie-group-8/src/views/Social.vue @@ -0,0 +1,100 @@ +<template> + <div class="min-h-screen bg-gray-100 dark:bg-neutral-900 p-6"> + <h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-6 text-center">Find and Follow Users</h1> + + <div class="max-w-xl mx-auto mb-6"> + <input + type="text" + v-model="searchTerm" + placeholder="Search by display name..." + class="w-full p-3 rounded-lg border border-gray-300 dark:border-neutral-700 dark:bg-neutral-800 dark:text-white" + /> + </div> + + <div class="grid gap-4 max-w-3xl mx-auto"> + <div + v-for="user in filteredUsers" + :key="user.uid" + class="flex items-center justify-between p-4 bg-white dark:bg-neutral-800 border dark:border-neutral-700 rounded-lg" + > + <router-link :to="`/user/${user.uid}`" class="flex items-center gap-4"> + <img + :src="user.photoURL || defaultAvatar" + class="w-12 h-12 rounded-full object-cover" + alt="Avatar" + /> + <span class="text-lg font-semibold text-gray-800 dark:text-white">{{ user.displayName }}</span> + </router-link> + <button + @click="toggleFollow(user.uid)" + class="px-4 py-1.5 text-sm rounded-lg font-medium" + :class="isFollowing(user.uid) + ? 'bg-red-500 text-white hover:bg-red-600' + : 'bg-blue-600 text-white hover:bg-blue-700'" + > + {{ isFollowing(user.uid) ? 'Unfollow' : 'Follow' }} + </button> + </div> + </div> + </div> +</template> + +<script setup> +import { ref, computed, onMounted } from 'vue' +import { getAuth } from 'firebase/auth' +import { + getFirestore, + collection, + getDocs, + setDoc, + deleteDoc, + doc +} from 'firebase/firestore' + +const auth = getAuth() +const db = getFirestore() +const currentUser = auth.currentUser + +const searchTerm = ref('') +const users = ref([]) +const following = ref([]) +const defaultAvatar = 'https://via.placeholder.com/150' + +onMounted(async () => { + await fetchUsers() + await fetchFollowing() +}) + +const fetchUsers = async () => { + const snapshot = await getDocs(collection(db, 'users')) + users.value = snapshot.docs + .map(doc => doc.data()) + .filter(u => u.uid !== currentUser?.uid && !u.hidden) // exclude self and test user +} + +const fetchFollowing = async () => { + const snapshot = await getDocs(collection(db, 'users', currentUser.uid, 'following')) + following.value = snapshot.docs.map(doc => doc.id) +} + +const isFollowing = (uid) => following.value.includes(uid) + +const toggleFollow = async (uid) => { + const followRef = doc(db, 'users', currentUser.uid, 'following', uid) + + if (isFollowing(uid)) { + await deleteDoc(followRef) + following.value = following.value.filter(id => id !== uid) + } else { + await setDoc(followRef, { followedAt: Date.now() }) + following.value.push(uid) + } +} + +const filteredUsers = computed(() => { + if (!searchTerm.value.trim()) return users.value + return users.value.filter(u => + u.displayName.toLowerCase().includes(searchTerm.value.toLowerCase()) + ) +}) +</script> \ No newline at end of file diff --git a/movie-group-8/src/views/UserProfile.vue b/movie-group-8/src/views/UserProfile.vue new file mode 100644 index 0000000..74c0cfc --- /dev/null +++ b/movie-group-8/src/views/UserProfile.vue @@ -0,0 +1,117 @@ +<template> + <div class="min-h-screen bg-gray-100 dark:bg-neutral-900 p-6"> + <div class="text-center mb-10"> + <img + v-if="user.photoURL" + :src="user.photoURL" + alt="Profile" + class="w-24 h-24 mx-auto rounded-full object-cover" + /> + <h1 class="text-2xl font-bold mt-4 text-gray-800 dark:text-white"> + {{ user.displayName || 'Unknown User' }} + </h1> + + <button + v-if="!isOwnProfile" + @click="toggleFollow" + class="mt-3 px-4 py-2 rounded-lg text-white text-sm" + :class="isFollowing ? 'bg-red-500 hover:bg-red-600' : 'bg-blue-600 hover:bg-blue-700'" + > + {{ isFollowing ? 'Unfollow' : 'Follow' }} + </button> + </div> + + <h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4 text-center"> + Watchlist + </h2> + + <div v-if="watchlist.length === 0" class="text-center text-gray-500 dark:text-gray-400"> + No public watchlist found. + </div> + + <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> + <router-link + v-for="movie in watchlist" + :key="movie.id" + :to="`/films/${movie.id}`" + class="block bg-white dark:bg-neutral-800 border dark:border-neutral-700 rounded-lg p-4 hover:opacity-90 transition" + > + <img + v-if="movie.poster_path" + :src="'https://image.tmdb.org/t/p/w342' + movie.poster_path" + alt="Poster" + class="w-full h-60 object-cover rounded mb-2" + /> + <p class="font-semibold text-gray-800 dark:text-white">{{ movie.title }}</p> + <p class="text-sm text-gray-600 dark:text-neutral-400">â {{ movie.vote_average }}</p> + <p class="text-xs text-gray-400 dark:text-neutral-500">{{ movie.status }}</p> + </router-link> + </div> + </div> +</template> + +<script setup> +import { ref, onMounted } from 'vue' +import { useRoute } from 'vue-router' +import { getAuth } from 'firebase/auth' +import { + getFirestore, + doc, + getDoc, + collection, + getDocs, + setDoc, + deleteDoc +} from 'firebase/firestore' + +const route = useRoute() +const db = getFirestore() +const auth = getAuth() + +const profileUserId = route.params.id +const currentUser = auth.currentUser + +const user = ref({}) +const watchlist = ref([]) +const isFollowing = ref(false) +const isOwnProfile = currentUser?.uid === profileUserId + +onMounted(async () => { + await fetchUserProfile() + await fetchWatchlist() + if (!isOwnProfile) await checkIfFollowing() +}) + +const fetchUserProfile = async () => { + const docRef = doc(db, 'users', profileUserId) + const docSnap = await getDoc(docRef) + if (docSnap.exists()) { + user.value = docSnap.data() + } else { + user.value = { displayName: 'User Not Found' } + } +} + +const fetchWatchlist = async () => { + const snapshot = await getDocs(collection(db, 'users', profileUserId, 'watchlist')) + watchlist.value = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })) +} + +const checkIfFollowing = async () => { + const followRef = doc(db, 'users', currentUser.uid, 'following', profileUserId) + const followSnap = await getDoc(followRef) + isFollowing.value = followSnap.exists() +} + +const toggleFollow = async () => { + const followRef = doc(db, 'users', currentUser.uid, 'following', profileUserId) + + if (isFollowing.value) { + await deleteDoc(followRef) + isFollowing.value = false + } else { + await setDoc(followRef, { followedAt: Date.now() }) + isFollowing.value = true + } +} +</script> \ No newline at end of file -- GitLab From e883407ca45ad15e9a40e7bef56a54fa076a430c Mon Sep 17 00:00:00 2001 From: Nunu Miah <nm01312@surrey.ac.uk> Date: Thu, 22 May 2025 21:35:20 +0100 Subject: [PATCH 2/3] Fixed register to add user to database and the navigation bar now includes the social tab, new users now have a default avatar and the profile view has been changed to have 3 tabs to view the users followers, followings and watchlist --- movie-group-8/src/assets/default_avatar.png | Bin 0 -> 39076 bytes movie-group-8/src/components/Navbar.vue | 1 + movie-group-8/src/views/Login.vue | 3 +- movie-group-8/src/views/Profile.vue | 226 +++++++++++++++----- movie-group-8/src/views/Register.vue | 35 ++- 5 files changed, 207 insertions(+), 58 deletions(-) create mode 100644 movie-group-8/src/assets/default_avatar.png diff --git a/movie-group-8/src/assets/default_avatar.png b/movie-group-8/src/assets/default_avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..778e051529c32dc9e845d961a9229d0addaf09ea GIT binary patch literal 39076 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 literal 0 HcmV?d00001 diff --git a/movie-group-8/src/components/Navbar.vue b/movie-group-8/src/components/Navbar.vue index 7abd7a8..1a4d4d9 100644 --- a/movie-group-8/src/components/Navbar.vue +++ b/movie-group-8/src/components/Navbar.vue @@ -87,6 +87,7 @@ const navigation = ref([ { name: 'Films', href: '/films', authRequired: true }, { name: 'Watchlist', href: '/watchlist', authRequired: true }, { name: 'Top Rated', href: '/top-rated', authRequired: true }, + { name: 'Social', href: '/social', authRequired: true }, ]); diff --git a/movie-group-8/src/views/Login.vue b/movie-group-8/src/views/Login.vue index 45657bb..909e2ed 100644 --- a/movie-group-8/src/views/Login.vue +++ b/movie-group-8/src/views/Login.vue @@ -78,6 +78,7 @@ import { setDoc } from 'firebase/firestore'; import { useRouter } from 'vue-router'; +import defaultAvatar from '@/assets/default_avatar.png' const email = ref(''); const password = ref(''); @@ -94,7 +95,7 @@ const createUserIfNotExists = async (user) => { await setDoc(userRef, { uid: user.uid, displayName: user.displayName || 'Anonymous', - photoURL: user.photoURL || '', + photoURL: defaultAvatar }); } }; diff --git a/movie-group-8/src/views/Profile.vue b/movie-group-8/src/views/Profile.vue index 733b4ee..065de78 100644 --- a/movie-group-8/src/views/Profile.vue +++ b/movie-group-8/src/views/Profile.vue @@ -9,7 +9,7 @@ /> <h1 class="text-3xl font-bold text-gray-800 dark:text-white">{{ displayName || 'Anonymous' }}</h1> <p class="text-sm text-gray-600 dark:text-neutral-400">{{ userEmail }}</p> - <p class="mt-2 text-base text-gray-600 dark:text-neutral-400">Movie fan and scene collector.</p> + <p class="mt-2 text-base text-gray-600 dark:text-neutral-400">{{ description }}</p> <button @click="showForm = !showForm" @@ -19,6 +19,7 @@ </button> </div> + <!-- Update Form --> <div v-if="showForm" class="max-w-xl mx-auto mb-12 bg-white dark:bg-neutral-800 p-6 rounded-xl border dark:border-neutral-700"> <form @submit.prevent="handleUpdateProfile" class="grid gap-6"> <div> @@ -26,6 +27,11 @@ <input type="text" v-model="displayName" class="w-full border border-gray-400 rounded-lg p-2 dark:text-white" required /> </div> + <div> + <label class="block text-sm mb-2 dark:text-white">Description</label> + <textarea v-model="description" rows="3" class="w-full border border-gray-400 rounded-lg p-2 dark:text-white" /> + </div> + <div> <label class="block text-sm mb-2 dark:text-white">New Password</label> <input type="password" v-model="newPassword" class="w-full border border-gray-400 rounded-lg p-2 dark:text-white" /> @@ -43,74 +49,114 @@ </form> </div> - <div> - <h2 class="text-2xl font-semibold text-gray-800 dark:text-white mb-6">Your Watchlist</h2> + <!-- Tabs --> + <div class="flex justify-center gap-4 mb-6"> + <button + v-for="tab in ['watchlist', 'followers', 'following']" + :key="tab" + @click="activeTab = tab" + :class="[ + 'px-4 py-2 rounded-lg text-sm font-medium', + activeTab === tab + ? 'bg-blue-600 text-white' + : 'bg-white dark:bg-neutral-700 text-gray-800 dark:text-white border border-gray-300 dark:border-neutral-600' + ]" + > + {{ tab.charAt(0).toUpperCase() + tab.slice(1) }} + </button> + </div> - <div v-if="watchlist.length === 0" class="text-gray-500 dark:text-gray-400 text-sm"> - Your watchlist is empty. + <!-- Tab Content --> + <div v-if="activeTab === 'watchlist'" class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> + <router-link + v-for="movie in watchlist" + :key="movie.id" + :to="`/films/${movie.id}`" + class="block bg-white dark:bg-neutral-800 border dark:border-neutral-700 rounded-lg p-3 hover:opacity-90 transition" + > + <img + v-if="movie.poster_path" + :src="'https://image.tmdb.org/t/p/w342' + movie.poster_path" + alt="Poster" + class="w-full h-60 object-cover rounded mb-3" + /> + <p class="font-semibold text-gray-800 dark:text-white">{{ movie.title }}</p> + <p class="text-sm text-gray-600 dark:text-neutral-400">â {{ movie.vote_average }} — {{ movie.status }}</p> + </router-link> + </div> + + <!-- Followers --> + <div v-else-if="activeTab === 'followers'" class="grid gap-4 max-w-xl mx-auto"> + <div + v-for="follower in followers" + :key="follower.uid" + class="flex items-center justify-between p-4 bg-white dark:bg-neutral-800 border dark:border-neutral-700 rounded-lg" + > + <router-link :to="`/user/${follower.uid}`" class="flex items-center gap-4"> + <img + :src="follower.photoURL || 'https://via.placeholder.com/150'" + alt="Avatar" + class="w-12 h-12 rounded-full object-cover" + /> + <span class="text-lg font-semibold text-gray-800 dark:text-white">{{ follower.displayName }}</span> + </router-link> </div> + </div> - <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> - <div - v-for="movie in watchlist" - :key="movie.id" - class="bg-white dark:bg-neutral-800 border dark:border-neutral-700 rounded-lg p-3 text-left" - > + <!-- Following --> + <div v-else-if="activeTab === 'following'" class="grid gap-4 max-w-xl mx-auto"> + <div + v-for="followed in following" + :key="followed.uid" + class="flex items-center justify-between p-4 bg-white dark:bg-neutral-800 border dark:border-neutral-700 rounded-lg" + > + <router-link :to="`/user/${followed.uid}`" class="flex items-center gap-4"> <img - v-if="movie.poster_path" - :src="'https://image.tmdb.org/t/p/w342' + movie.poster_path" - alt="Poster" - class="w-full h-60 object-cover rounded mb-3" + :src="followed.photoURL || 'https://via.placeholder.com/150'" + alt="Avatar" + class="w-12 h-12 rounded-full object-cover" /> - <p class="font-semibold text-gray-800 dark:text-white">{{ movie.title }}</p> - <p class="text-sm text-gray-600 dark:text-neutral-400">â {{ movie.vote_average }} — {{ movie.status }}</p> - </div> + <span class="text-lg font-semibold text-gray-800 dark:text-white">{{ followed.displayName }}</span> + </router-link> </div> </div> </div> </template> <script setup> -import { ref, computed, onMounted } from 'vue' +import { + collection, + getDocs, + doc, + getDoc, + updateDoc, + getFirestore, +} from 'firebase/firestore' + import { getAuth, updateProfile as firebaseUpdateProfile, - updatePassword, + updatePassword } from 'firebase/auth' -import { - getFirestore, - collection, - getDocs -} from 'firebase/firestore' +import { ref, computed, onMounted } from 'vue' const auth = getAuth() const db = getFirestore() -const user = auth.currentUser - -const loading = ref(false) +const user = ref(null) const userEmail = ref('') const displayName = ref('') -const newPassword = ref('') +const description = ref('') const userPhotoURL = ref('') +const newPassword = ref('') const showForm = ref(false) +const activeTab = ref('watchlist') +const loading = ref(false) const watchlist = ref([]) +const followers = ref([]) +const following = ref([]) -// Load profile info -onMounted(async () => { - if (user) { - userEmail.value = user.email - displayName.value = user.displayName || '' - userPhotoURL.value = user.photoURL || '' - - // Fetch watchlist - const snapshot = await getDocs(collection(db, 'users', user.uid, 'watchlist')) - watchlist.value = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })) - } -}) - -// Password validation const passwordError = computed(() => { if (!newPassword.value) return '' if (newPassword.value.length < 8) return 'Password must be at least 8 characters.' @@ -120,30 +166,104 @@ const passwordError = computed(() => { return '' }) -// Update profile function +const fetchFollowing = async () => { + const followingSnap = await getDocs(collection(db, 'users', user.value.uid, 'following')) + const followedIds = followingSnap.docs.map(doc => doc.id) + + const usersList = [] + + for (const id of followedIds) { + const docSnap = await getDoc(doc(db, 'users', id)) + if (docSnap.exists()) { + const data = docSnap.data() + // Only push if the user has a displayName or isn’t hidden + if (!data.hidden && data.displayName) { + usersList.push({ uid: id, ...data }) + } + } + } + + following.value = usersList +} + +const fetchFollowers = async () => { + if (!user.value) return + + const allUsersSnap = await getDocs(collection(db, 'users')) + const followersList = [] + + for (const docSnap of allUsersSnap.docs) { + const otherUid = docSnap.id + if (otherUid === user.value.uid) continue + + const followDocRef = doc(db, 'users', otherUid, 'following', user.value.uid) + const followDocSnap = await getDoc(followDocRef) + + if (followDocSnap.exists()) { + const otherUserData = docSnap.data() + if (otherUserData.displayName && !otherUserData.hidden) { + followersList.push({ uid: otherUid, ...otherUserData }) + } + } + } + + followers.value = followersList +} + const handleUpdateProfile = async () => { if (passwordError.value) return loading.value = true try { - if (user) { - await firebaseUpdateProfile(user, { - displayName: displayName.value - }) + const authUser = auth.currentUser + if (!authUser) throw new Error('No authenticated user') - if (newPassword.value) { - await updatePassword(user, newPassword.value) - } + // Update the Firebase Auth profile + await firebaseUpdateProfile(authUser, { + displayName: displayName.value + }) - alert('Profile updated successfully!') - showForm.value = false + if (newPassword.value) { + await updatePassword(authUser, newPassword.value) } + + // Update your Firestore user document + const userDocRef = doc(db, 'users', authUser.uid) + await updateDoc(userDocRef, { + displayName: displayName.value, + description: description.value + }) + + alert('Profile updated successfully!') + showForm.value = false + } catch (error) { alert(error.message) } finally { loading.value = false } } + +onMounted(async () => { + const authUser = auth.currentUser + if (!authUser) return + + user.value = authUser + userEmail.value = authUser.email + displayName.value = authUser.displayName || '' + userPhotoURL.value = authUser.photoURL || '' + + const userDoc = await getDoc(doc(db, 'users', user.value.uid)) + if (userDoc.exists()) { + description.value = userDoc.data().description || '' + } + + const snapshot = await getDocs(collection(db, 'users', user.value.uid, 'watchlist')) + watchlist.value = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })) + + await fetchFollowing() + await fetchFollowers() +}) </script> <style scoped> diff --git a/movie-group-8/src/views/Register.vue b/movie-group-8/src/views/Register.vue index 2880887..1f4d102 100644 --- a/movie-group-8/src/views/Register.vue +++ b/movie-group-8/src/views/Register.vue @@ -62,8 +62,18 @@ <script setup> import { ref, computed } from 'vue'; -import { getAuth, createUserWithEmailAndPassword, signInWithPopup, GoogleAuthProvider } from 'firebase/auth'; import { useRouter } from 'vue-router'; +import defaultAvatar from '@/assets/default_avatar.png' + +import { + getAuth, + createUserWithEmailAndPassword, + } from 'firebase/auth'; +import { + getFirestore, + doc, + setDoc +} from 'firebase/firestore'; const email = ref(''); const password = ref(''); @@ -93,9 +103,26 @@ const register = async () => { if (passwordError.value || confirmPasswordError.value) return; loading.value = true; try { - const auth = getAuth(); - await createUserWithEmailAndPassword(auth, email.value, password.value); - router.push('/films'); + const auth = getAuth() + const db = getFirestore() + const userCredential = await createUserWithEmailAndPassword(auth, email.value, password.value) + const user = userCredential.user + + // Set default profile info + await updateProfile(user, { + displayName: 'New User', + photoURL: defaultAvatar + }) + + // Create Firestore document for this user + await setDoc(doc(db, 'users', user.uid), { + displayName: 'New User', + photoURL: defaultAvatar, + description: '', + hidden: false + }) + + router.push('/films') } catch (error) { alert(error.message); } finally { -- GitLab 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 3/3] 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