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] 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