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 Binary files /dev/null and b/movie-group-8/src/assets/default_avatar.png differ diff --git a/movie-group-8/src/components/Navbar.vue b/movie-group-8/src/components/Navbar.vue index 7abd7a88907732e7e5cb61faa5c7cbbd8d27570d..1a4d4d9870dd8801a56825986cc2e54bb6a5bd59 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 45657bb635a34b46de98dd7a312c7ada47d52328..909e2eded2b886a31ceaf700bb41b2eb850b9064 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 733b4eefd0e59443c462995e23715c1aa7fa71d1..065de78bfe03ba517b7fdb499bcce3d8981ec898 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 2880887871e12186c4e92abcc2b2d1f83fff625f..1f4d10208d7c4e78d2394a6ef467358234f9f5f4 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 {