diff --git a/movie-group-8/src/router/index.js b/movie-group-8/src/router/index.js index f4763b4c1ce260ab69a24d77e4ab2956d5b63c49..fcbec63700fcb695109d7a419820f479345ec6d5 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 eecffe4823239b2c0feac6450cd4b1938ed3d129..45657bb635a34b46de98dd7a312c7ada47d52328 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 0000000000000000000000000000000000000000..34bbed7727d78d84a2346b30ac4577da3cf4de18 --- /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 0000000000000000000000000000000000000000..74c0cfc15940553b93dad02694bb0a5e0f82de7d --- /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