diff --git a/movie-group-8/src/components/Navbar.vue b/movie-group-8/src/components/Navbar.vue index 64d7e5700bc6baec7c66c76e9357bbc787de6f9e..ea83fa1aa0b41228417ff9b2171d64bbefc33573 100644 --- a/movie-group-8/src/components/Navbar.vue +++ b/movie-group-8/src/components/Navbar.vue @@ -11,6 +11,7 @@ <div class="absolute left-1/2 transform -translate-x-1/2 w-full max-w-md"> <input v-model="searchQuery" + @keyup.enter="handleSearch" type="text" placeholder="Search..." class="w-full px-4 py-2 pr-10 rounded-full bg-neutral-700 text-white placeholder-gray-400 focus:ring-2 focus:ring-white focus:outline-none" @@ -83,13 +84,12 @@ const route = useRoute(); const router = useRouter(); const isLoggedIn = ref(false); const userPhotoURL = ref(''); +const searchQuery = ref(''); 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 }, - ]); // Compute the current route to match against the navigation links @@ -127,4 +127,10 @@ const handleSignOut = () => { console.error("Error signing out:", error); }); }; + +function handleSearch() { + if (searchQuery.value.trim()) { + router.push({ path: '/films', query: { q: searchQuery.value.trim() } }); + } +} </script> diff --git a/movie-group-8/src/components/WatchlistButton.vue b/movie-group-8/src/components/WatchlistButton.vue index 91458232887237079c738f99224113fc42242682..7e68baada3c94af92dcf9ced1496881420471486 100644 --- a/movie-group-8/src/components/WatchlistButton.vue +++ b/movie-group-8/src/components/WatchlistButton.vue @@ -4,8 +4,8 @@ @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'" + ? 'bg-red-600 hover:bg-red-700 border border-white' + : 'bg-neutral-700 hover:bg-neutral-800 border border-white'" > {{ isInWatchlist ? 'Remove from Watchlist' : 'Add to Watchlist' }} </button> diff --git a/movie-group-8/src/stores/WatchlistStore.js b/movie-group-8/src/stores/WatchlistStore.js new file mode 100644 index 0000000000000000000000000000000000000000..b7cc82583eaabf3af1422f5c24b982991e0844e0 --- /dev/null +++ b/movie-group-8/src/stores/WatchlistStore.js @@ -0,0 +1,25 @@ +import { reactive } from 'vue'; + +export const WatchlistStore = reactive({ + watchlist: JSON.parse(localStorage.getItem('watchlist') || '[]'), + + add(movie) { + if (!this.watchlist.find(m => m.id === movie.id)) { + this.watchlist.push(movie); + this.save(); + } + }, + + remove(movie) { + this.watchlist = this.watchlist.filter(m => m.id !== movie.id); + this.save(); + }, + + isInWatchlist(movie) { + return this.watchlist.some(m => m.id === movie.id); + }, + + save() { + localStorage.setItem('watchlist', JSON.stringify(this.watchlist)); + } +}); diff --git a/movie-group-8/src/views/FilmDetails.vue b/movie-group-8/src/views/FilmDetails.vue index ebc1be66bed09a4459ba2f28e3a0edf377d69d5b..c3efd77e48c6cf4b1e4a565348fa537e5097503e 100644 --- a/movie-group-8/src/views/FilmDetails.vue +++ b/movie-group-8/src/views/FilmDetails.vue @@ -1,69 +1,94 @@ <template> - <div class="film-details"> - <button class="back-button" @click="$router.back()">↠Back</button> - - <div v-if="loading" class="loading">Loading movie...</div> - <div v-else-if="error" class="error">Error: {{ error }}</div> - - <div v-else class="details-container"> - <div class="poster-container"> - <img - v-if="movie.poster_path" - :src="`https://image.tmdb.org/t/p/w500${movie.poster_path}`" - :alt="`${movie.title} poster`" - class="poster" - /> - </div> + <div class="min-h-screen bg-neutral-900 p-6"> + <div class="max-w-[85rem] mx-auto"> + <button class="text-neutral-300 mb-4 hover:underline" @click="$router.back()">↠Back</button> + + <div v-if="loading" class="text-center text-neutral-400 text-lg">Loading movie...</div> + <div v-else-if="error" class="text-center text-red-500 text-lg">Error: {{ error }}</div> + + <div v-else class="bg-neutral-800 rounded-xl shadow-md p-6 md:p-10 space-y-10"> + <!-- Poster & Trailer Card Layout --> + <div class="flex flex-col lg:flex-row gap-6"> + <!-- Poster --> + <div class="w-full lg:w-1/3 flex justify-center items-start"> + <img + v-if="movie.poster_path" + :src="`https://image.tmdb.org/t/p/w500${movie.poster_path}`" + :alt="`${movie.title} poster`" + class="rounded-xl max-h-[473px] object-contain bg-neutral-700 p-2" + /> + </div> + + <!-- Trailer --> + <div class="w-full lg:w-2/3"> + <div + v-if="trailerUrl" + class="aspect-video w-full rounded-xl overflow-hidden bg-black" + > + <iframe + :src="trailerUrl" + class="w-full h-full" + frameborder="0" + allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" + allowfullscreen + ></iframe> + </div> + </div> + </div> - <div class="info-container"> - <h1 class="title">{{ movie.title }}</h1> - <p class="release-date">{{ formatDate(movie.release_date) }}</p> - <p class="overview">{{ movie.overview }}</p> - - <div v-if="trailerUrl" class="trailer"> - <h2>Trailer</h2> - <iframe - :src="trailerUrl" - frameborder="0" - allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" - allowfullscreen - ></iframe> + + <!-- Title and Watchlist --> + <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> + <h1 class="text-3xl font-bold text-neutral-200">{{ movie.title }}</h1> + <button + type="button" + @click="toggleWatchlist" + class="px-5 py-2 rounded-xl font-medium transition duration-300" + :class="isInWatchlist + ? 'bg-red-600 hover:bg-red-700' + : 'bg-green-600 hover:bg-green-700'" + > + {{ isInWatchlist ? 'Remove from Watchlist' : 'Add to Watchlist' }} + </button> </div> - <button - type="button" - @click="toggleWatchlist" - class="mt-4 px-4 py-2 rounded text-white 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> + <!-- Overview --> + <div> + <p class="text-neutral-400 text-sm mb-1">Released: {{ formatDate(movie.release_date) }}</p> + <p class="text-neutral-300 leading-relaxed">{{ movie.overview }}</p> + </div> + <!-- Review Button --> <RouterLink :to="{ name: 'ReviewFilm', params: { id: movie.id } }" - class="mt-2 inline-block px-4 py-2 bg-blue-600 rounded text-white hover:bg-blue-700 text-center" + class="inline-block bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition duration-300" > Review this Film </RouterLink> - <div class="mt-6 reviews-section"> - <h2 class="text-xl mb-2 text-white">User Reviews</h2> - <div v-if="reviewsLoading" class="text-gray-400">Loading reviews…</div> - <div v-else-if="!reviews.length" class="text-gray-400">No reviews yet.</div> + + <!-- Reviews --> + <div> + <h2 class="text-2xl font-semibold text-neutral-200 mb-4">User Reviews</h2> + <div v-if="reviewsLoading" class="text-neutral-400">Loading reviews…</div> + <div v-else-if="!reviews.length" class="text-neutral-500">No reviews yet.</div> <ul v-else class="space-y-4"> - <li v-for="(r, i) in reviews" :key="i" class="bg-neutral-800 p-4 rounded"> + <li + v-for="(r, i) in reviews" + :key="i" + class="bg-neutral-700 p-4 rounded-lg" + > <div class="flex items-center mb-2"> <img - v-if="r.authorPFP" - :src="r.authorPFP" + v-if="r.authorPFP" + :src="r.authorPFP" alt="" - class="h-8 w-8 rounded-full mr-2"/> - <span class="font-semibold text-white">{{ r.authorName }}</span> - <span class="text-gray-400 ml-2">{{ r.rating }}★</span> - </div> - <p class="text-gray-200">{{ r.text }}</p> - </li> + class="h-8 w-8 rounded-full mr-2" + /> + <span class="font-semibold text-neutral-100">{{ r.authorName }}</span> + <span class="text-yellow-400 ml-2">{{ r.rating }}★</span> + </div> + <p class="text-neutral-300">{{ r.text }}</p> + </li> </ul> </div> </div> @@ -71,13 +96,14 @@ </div> </template> + <script setup> import { ref, onMounted } from 'vue' import { useRoute } from 'vue-router' import { useWatchlist } from '@/composables/useWatchlist.js' import { getAuth } from 'firebase/auth' import { getFirestore, doc, getDoc } from 'firebase/firestore' -import { getAllReviews } from '@/composables/useReviews.js' +import { getAllReviews } from '@/composables/useReviews.js' const route = useRoute() const movie = ref(null) @@ -146,81 +172,3 @@ onMounted(async () => { loading.value = false }) </script> - -<style scoped> -.film-details { - background-color: #121212; - color: #ffffff; - min-height: 100vh; - padding: 2rem; - box-sizing: border-box; -} - -.back-button { - color: #ffffff; - background: transparent; - margin-bottom: 1rem; - font-size: 1rem; -} - -.loading, -.error { - color: #ffffff; - text-align: center; - margin: 2rem 0; -} - -.details-container { - display: flex; - flex-wrap: wrap; - gap: 2rem; - background-color: #1e1e1e; - padding: 2rem; - border-radius: 8px; - max-width: 900px; - margin: 2rem auto 0; -} - -.poster-container { - flex: 0 0 300px; -} - -.poster { - width: 100%; - border-radius: 4px; -} - -.info-container { - flex: 1; - display: flex; - flex-direction: column; -} - -.title { - font-size: 2rem; - margin: 0 0 0.5rem; -} - -.release-date { - color: #bbbbbb; - margin-bottom: 1rem; -} - -.overview { - color: #dddddd; - line-height: 1.6; - margin-bottom: 1.5rem; -} - -.trailer h2 { - margin-bottom: 0.5rem; - color: #ffffff; -} - -.trailer iframe { - width: 100%; - height: 300px; - border-radius: 8px; - border: none; -} -</style> diff --git a/movie-group-8/src/views/Films.vue b/movie-group-8/src/views/Films.vue index 2343d26e7e79942a734e33569c7a9d2b783a2e5a..05b38809ea0761ad318f2f634ff36f8cc770a2f1 100644 --- a/movie-group-8/src/views/Films.vue +++ b/movie-group-8/src/views/Films.vue @@ -1,107 +1,148 @@ <template> - <div class="films-container"> - <!-- Centered search bar pushed down slightly --> - <div class="search-wrapper"> - <input - v-model="term" - @keyup.enter="onSearch" - placeholder="Search movies…" - class="search-input" - /> - </div> - - <div v-if="loading" class="loading">Loading…</div> - <div v-else-if="error" class="error">Error: {{ error.message }}</div> + <div class="bg-neutral-900 min-h-screen py-10 px-6 sm:px-10"> + <div class="max-w-[85rem] mx-auto mt-12"> + <!-- Title and Filters --> + <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-10"> + <h2 class="text-3xl font-semibold text-white"> + {{ computedTitle }} + </h2> + <div class="flex flex-col sm:flex-row sm:items-center gap-4"> + <select v-model="genre" @change="fetchFiltered" class="px-4 py-2 rounded-lg border border-neutral-700 bg-neutral-800 text-white"> + <option value="">All Genres</option> + <option v-for="g in genres" :key="g.id" :value="g.id">{{ g.name }}</option> + </select> + <select v-model.number="year" @change="fetchFiltered" class="px-4 py-2 rounded-lg border border-neutral-700 bg-neutral-800 text-white"> + <option value="">All Years</option> + <option v-for="y in years" :key="y" :value="y">{{ y }}</option> + </select> + </div> + </div> - <!-- Movie grid --> - <MovieList :movies="results" /> + <!-- Movie Cards --> + <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4"> + <div + v-for="movie in results" + :key="movie.id" + @click="goToMovie(movie.id)" + class="group flex flex-col h-full bg-neutral-800 hover:bg-neutral-700 cursor-pointer border border-gray-200 shadow-2xs rounded-xl dark:border-neutral-700 dark:shadow-neutral-700/70 text-sm transition-colors duration-150" + > + <div class="rounded-t-xl overflow-hidden"> + <img v-if="movie.poster_path" :src="imgUrl(movie.poster_path)" alt="" class="w-full object-contain rounded-t-xl"/> + </div> + <div class="p-3 flex flex-col flex-grow"> + <h3 class="text-base font-semibold text-gray-200 dark:hover:text-white"> + {{ movie.title }} + </h3> + <p class="mt-2 text-gray-400 dark:text-neutral-400 line-clamp-3"> + {{ movie.overview || 'No description available.' }} + </p> + </div> + <div class="mt-auto flex justify-between items-center border-t border-gray-200 dark:border-neutral-700 px-3 py-2"> + <span class="text-xs font-semibold text-blue-400"> + â {{ movie.vote_average.toFixed(1) }} + </span> + <WatchlistButton :movie="movie" @click.stop /> + </div> + </div> + </div> + </div> </div> </template> <script setup> -import { ref, onMounted } from 'vue' +import { ref, onMounted, computed, watch } from 'vue' +import { useRoute, useRouter } from 'vue-router' import axios from 'axios' -import MovieList from '../components/MovieList.vue' +import WatchlistButton from '@/components/WatchlistButton.vue' const API_KEY = import.meta.env.VITE_TMDB_API_KEY -const BASE = import.meta.env.VITE_TMDB_BASE +const BASE = import.meta.env.VITE_TMDB_BASE -const term = ref('') +const route = useRoute() +const router = useRouter() + +const term = ref(route.query.q || '') const results = ref([]) +const genres = ref([]) +const genre = ref('') +const year = ref('') const loading = ref(false) -const error = ref(null) - -async function search(query) { - loading.value = true - error.value = null - try { - const { data } = await axios.get(`${BASE}/search/movie`, { - params: { api_key: API_KEY, query } - }) - results.value = data.results - } catch (e) { - error.value = e - } finally { - loading.value = false - } +const error = ref(null) + +const years = Array.from({ length: 25 }, (_, i) => 2000 + i) + +function imgUrl(path) { + return `https://image.tmdb.org/t/p/w342${path}` } -async function fetchPopular() { - loading.value = true - error.value = null - try { - const { data } = await axios.get(`${BASE}/movie/popular`, { - params: { api_key: API_KEY } - }) - results.value = data.results - } catch (e) { - error.value = e - } finally { - loading.value = false - } +async function fetchGenres() { + const { data } = await axios.get(`${BASE}/genre/movie/list`, { + params: { api_key: API_KEY }, + }) + genres.value = data.genres } -function onSearch() { - if (term.value.trim()) { - search(term.value) - } else { - fetchPopular() - } +async function fetchTopRated() { + const { data } = await axios.get(`${BASE}/movie/top_rated`, { + params: { api_key: API_KEY }, + }) + results.value = data.results } -onMounted(fetchPopular) -</script> +async function fetchFiltered() { + let url = `${BASE}/discover/movie` + const params = { + api_key: API_KEY, + sort_by: 'vote_average.desc', + 'vote_count.gte': 100, + with_genres: genre.value || undefined, + primary_release_year: year.value || undefined, + } -<style scoped> -.films-container { - padding-top: 5rem; /* adjust to nav height + spacing */ - display: flex; - flex-direction: column; - align-items: center; - width: 100%; + const { data } = await axios.get(url, { params }) + results.value = data.results } -.search-wrapper { - width: 100%; - max-width: 500px; - margin-bottom: 1.5rem; - display: flex; - justify-content: center; - color: white; +async function onSearch() { + if (!term.value) return fetchTopRated() + const { data } = await axios.get(`${BASE}/search/movie`, { + params: { api_key: API_KEY, query: term.value }, + }) + results.value = data.results } -.search-input { - width: 100%; - padding: 0.75rem 1rem; - font-size: 1.1rem; - border: 1px solid #ccc; - border-radius: 4px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +watch(() => route.query.q, async (newQuery) => { + term.value = newQuery || '' + await onSearch() +}) + +function goToMovie(id) { + router.push(`/films/${id}`) } -.loading, -.error { - margin: 1rem 0; - font-weight: bold; +const computedTitle = computed(() => { + const genreName = genres.value.find(g => g.id === Number(genre.value))?.name + if (genreName && year.value) return `Top Rated ${genreName} ${year.value}` + if (genreName) return `Top Rated ${genreName}` + if (year.value) return `Top Rated ${year.value}` + return 'Top Rated' +}) + +onMounted(async () => { + await fetchGenres() + if (term.value) { + await onSearch() + } else { + await fetchTopRated() + } +}) +</script> + +<style scoped> +.line-clamp-3 { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; } </style> diff --git a/movie-group-8/src/views/Home.vue b/movie-group-8/src/views/Home.vue index 04089a56d0655779a1cbdd2d067b22e84be646be..1a9c377eb4f21196b953e02fe6d1182681642b63 100644 --- a/movie-group-8/src/views/Home.vue +++ b/movie-group-8/src/views/Home.vue @@ -1,5 +1,5 @@ <template> - <div class="h-full bg-neutral-900 p-6"> + <div class="min-h-screen bg-neutral-900 p-6"> <div class="max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto dark"> <div class="relative p-6 md:p-16"> <div class="relative z-10 lg:grid lg:grid-cols-12 lg:gap-16 lg:items-center"> diff --git a/movie-group-8/src/views/Profile.vue b/movie-group-8/src/views/Profile.vue index 8e3a4b8caefddcb6f16a23faac61bd18ba50dec5..82b407c6e273574d57f0ec052e24a81383069817 100644 --- a/movie-group-8/src/views/Profile.vue +++ b/movie-group-8/src/views/Profile.vue @@ -1,22 +1,21 @@ <template> <div class="min-h-screen bg-gray-100 dark:bg-neutral-900 text-center px-4 py-8 pt-20"> - <div class="text-center mb-10"> + <div class="bg-neutral-800 border border-neutral-700 text-center max-w-2xl mx-auto p-6 rounded-2xl shadow mb-10"> <img v-if="userPhotoURL" :src="userPhotoURL" alt="Profile Picture" - class="w-28 h-28 mx-auto rounded-full object-cover mb-4" + class="w-24 h-24 mx-auto rounded-full object-cover mb-4 border-2 border-white" /> - <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">{{ description }}</p> - - <button - @click="showForm = !showForm" - class="mt-4 px-5 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition" + <h1 class="text-2xl font-bold text-white">{{ displayName || 'Anonymous' }}</h1> + <p class="text-sm text-neutral-400">{{ userEmail }}</p> + <p class="mt-2 text-base text-neutral-400">{{ description }}</p> + <router-link + to="/settings" + class="mt-4 inline-block px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition" > - {{ showForm ? 'Close Update Form' : 'Update Profile' }} - </button> + Edit Profile + </router-link> </div> <!-- Update Form --> @@ -56,10 +55,10 @@ :key="tab" @click="activeTab = tab" :class="[ - 'px-4 py-2 rounded-lg text-sm font-medium', + 'px-5 py-2 rounded-full text-sm font-medium transition', 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' + ? 'bg-white text-black dark:bg-white dark:text-black' + : 'bg-neutral-700 text-white hover:bg-neutral-600' ]" > {{ tab.charAt(0).toUpperCase() + tab.slice(1) }} @@ -67,21 +66,39 @@ </div> <!-- Tab Content --> - <div v-if="activeTab === 'watchlist'" class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> + <div v-if="activeTab === 'watchlist'" class="grid gap-4 sm:grid-cols-2 lg:grid-cols-5 xl:grid-cols-5"> <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" + class="rounded-xl overflow-hidden border border-neutral-700 bg-neutral-800 shadow hover:brightness-110 transition flex flex-col" > - <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> + <div class="rounded-t-xl overflow-hidden"> + <img + v-if="movie.poster_path" + :src="'https://image.tmdb.org/t/p/w342' + movie.poster_path" + alt="Poster" + class="w-full object-contain" + /> + </div> + + <div class="p-3 flex flex-col grow"> + <p class="text-base font-semibold text-white">{{ movie.title }}</p> + <p class="mt-1 text-sm text-neutral-400 line-clamp-3"> + {{ movie.overview }} + </p> + + <div class="mt-auto flex justify-between items-center border-t border-neutral-700 pt-2 mt-3"> + <span class="text-sm font-semibold text-blue-400"> + â {{ movie.vote_average.toFixed(1) }} + </span> + <span + class="px-3 py-1 text-xs rounded-md bg-neutral-700 text-white border border-neutral-600" + > + {{ movie.status === 'planned' ? 'Planned' : 'Watched' }} + </span> + </div> + </div> </router-link> </div> diff --git a/movie-group-8/src/views/Settings.vue b/movie-group-8/src/views/Settings.vue index 4c5d407983bd7e51dd0c6b32680470d99bd2029a..3056533ebd24813d46047059c97038d1e0c773b4 100644 --- a/movie-group-8/src/views/Settings.vue +++ b/movie-group-8/src/views/Settings.vue @@ -1 +1,163 @@ -<template>Settings</template> \ No newline at end of file +<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">Edit Profile</h1> + + <div class="max-w-xl mx-auto space-y-6"> + <!-- Profile Info --> + <div class="space-y-4"> + <div> + <label class="block text-sm font-medium mb-1 text-gray-700 dark:text-white">Display Name</label> + <input + type="text" + v-model="displayName" + class="w-full p-3 rounded-lg border border-gray-300 dark:border-neutral-700 dark:bg-neutral-800 dark:text-white" + /> + </div> + + <div> + <label class="block text-sm font-medium mb-1 text-gray-700 dark:text-white">Description</label> + <textarea + v-model="description" + rows="3" + class="w-full p-3 rounded-lg border border-gray-300 dark:border-neutral-700 dark:bg-neutral-800 dark:text-white" + ></textarea> + </div> + + <div class="flex items-center gap-2"> + <input + id="hidden" + type="checkbox" + v-model="hidden" + class="form-checkbox h-5 w-5 text-blue-600 dark:bg-neutral-800 dark:border-neutral-700" + /> + <label for="hidden" class="text-sm text-gray-700 dark:text-white">Hide profile from others</label> + </div> + + <button + @click="handleProfileUpdate" + class="w-full py-3 text-white rounded-lg font-medium bg-blue-600 hover:bg-blue-700 transition" + > + Save Changes + </button> + </div> + + <!-- Change Password --> + <div class="border-t border-gray-300 dark:border-neutral-700 pt-6"> + <h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Change Password</h2> + + <div v-if="isGoogleUser" class="text-gray-600 dark:text-neutral-400 italic"> + Your account is linked with Google. Password changes are managed through your Google account. + </div> + + <div v-else class="space-y-4"> + <div> + <label class="block text-sm font-medium mb-1 text-gray-700 dark:text-white">New Password</label> + <input + type="password" + v-model="newPassword" + class="w-full p-3 rounded-lg border border-gray-300 dark:border-neutral-700 dark:bg-neutral-800 dark:text-white" + /> + </div> + + <div> + <label class="block text-sm font-medium mb-1 text-gray-700 dark:text-white">Confirm New Password</label> + <input + type="password" + v-model="confirmPassword" + class="w-full p-3 rounded-lg border border-gray-300 dark:border-neutral-700 dark:bg-neutral-800 dark:text-white" + /> + </div> + + <button + @click="handleChangePassword" + :disabled="!newPassword || !confirmPassword || newPassword !== confirmPassword" + class="w-full py-3 text-white rounded-lg font-medium bg-blue-600 hover:bg-blue-700 transition disabled:opacity-50 disabled:pointer-events-none" + > + Update Password + </button> + </div> + </div> + </div> + </div> + </template> + + <script setup> + import { ref, onMounted } from 'vue' + import { getAuth, updateProfile as firebaseUpdateProfile, updatePassword } from 'firebase/auth' + import { getFirestore, doc, getDoc, setDoc } from 'firebase/firestore' + + const auth = getAuth() + const db = getFirestore() + + const displayName = ref('') + const description = ref('') + const hidden = ref(false) + + const newPassword = ref('') + const confirmPassword = ref('') + + const isGoogleUser = ref(false) + + onMounted(async () => { + const user = auth.currentUser + if (user) { + displayName.value = user.displayName || '' + const docRef = doc(db, 'users', user.uid) + const docSnap = await getDoc(docRef) + if (docSnap.exists()) { + const data = docSnap.data() + description.value = data.description || '' + hidden.value = data.hidden || false + } + + // Detect if user signed in with Google + isGoogleUser.value = user.providerData.some( + (provider) => provider.providerId === 'google.com' + ) + } + }) + + const handleProfileUpdate = async () => { + const user = auth.currentUser + if (user) { + try { + await firebaseUpdateProfile(user, { + displayName: displayName.value + }) + + await setDoc(doc(db, 'users', user.uid), { + displayName: displayName.value, + description: description.value, + hidden: hidden.value + }) + + alert('Profile updated successfully!') + } catch (error) { + console.error('Error updating profile:', error) + alert('Failed to update profile.') + } + } + } + + const handleChangePassword = async () => { + const user = auth.currentUser + if (isGoogleUser.value) { + alert('Password changes are not allowed for Google accounts. Please change your password via your Google account settings.') + return + } + if (user && newPassword.value === confirmPassword.value) { + try { + await updatePassword(user, newPassword.value) + alert('Password updated successfully!') + newPassword.value = '' + confirmPassword.value = '' + } catch (error) { + console.error('Error updating password:', error) + alert('Failed to update password.') + } + } else { + alert("Passwords don't match.") + } + } + </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 index e3b650ebab1cd98051851e8b453fffa22cf2fe62..4f27064b33ef8556084dbb04cd19fef2e8c508fe 100644 --- a/movie-group-8/src/views/UserProfile.vue +++ b/movie-group-8/src/views/UserProfile.vue @@ -1,6 +1,6 @@ <template> <div class="min-h-screen bg-gray-100 dark:bg-neutral-900 p-6"> - <div class="text-center mb-10"> + <div class="text-center mt-16 mb-10"> <img v-if="user.photoURL" :src="user.photoURL"