diff --git a/movie-group-8/package-lock.json b/movie-group-8/package-lock.json index c6e9575c2f7e1aca5b9ab83b9e720cd18bd6e052..776cb26126436ac733829e82ef1a9429176c4a92 100644 --- a/movie-group-8/package-lock.json +++ b/movie-group-8/package-lock.json @@ -1852,6 +1852,20 @@ "node": ">=0.8.0" } }, + "node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/firebase": { "version": "11.4.0", "resolved": "https://registry.npmjs.org/firebase/-/firebase-11.4.0.tgz", @@ -2200,6 +2214,18 @@ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -2359,6 +2385,22 @@ "node": ">=6" } }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -2370,13 +2412,17 @@ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" }, "node_modules/vite": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.2.tgz", - "integrity": "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==", + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", + "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", + "license": "MIT", "dependencies": { "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" diff --git a/movie-group-8/src/components/TopRatedMovies.vue b/movie-group-8/src/components/TopRatedMovies.vue index 8b5bfc579781e6ae9467420581cd36848010a234..89845c5502db60f794bd399f34cd1fa56da71a8b 100644 --- a/movie-group-8/src/components/TopRatedMovies.vue +++ b/movie-group-8/src/components/TopRatedMovies.vue @@ -1,5 +1,6 @@ <script setup> import { ref, onMounted, watch } from 'vue' +import addToWatchlist from '@/views/Watchlist.vue' const year = ref(2000) const genre = ref('') @@ -58,6 +59,7 @@ watch([year, genre], async () => { /> <p class="title">{{ movie.title }}</p> <p class="rating">â {{ movie.vote_average }}</p> + <button @click="addToWatchlist(movie)">Add to Watchlist</button> </div> </div> </template> diff --git a/movie-group-8/src/components/Watchlist.vue b/movie-group-8/src/components/Watchlist.vue new file mode 100644 index 0000000000000000000000000000000000000000..f4f4aadaed9e7001aa2a16e99f1cc00984980767 --- /dev/null +++ b/movie-group-8/src/components/Watchlist.vue @@ -0,0 +1,74 @@ +<template> + <div class="watchlist"> + <h1>Your Watchlist</h1> + + <div class="filters"> + <select v-model="filter"> + <option value="">All</option> + <option value="planned">Plan To Watch</option> + <option value="watched">Watched</option> + </select> + </div> + + <div class="movie-grid"> + <div + v-for="movie in filteredWatchlist" + :key="movie.id" + class="movie-card" + > + <img :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> + <p class="status">📌 {{ movie.status }}</p> + </div> + </div> + </div> +</template> + +<script setup> + import { ref, computed, onMounted } from 'vue' + import { getFirestore, collection, getDocs } from 'firebase/firestore' + import { getAuth } from 'firebase/auth' + + const db = getFirestore() + const auth = getAuth() + + const watchlist = ref([]) + const filter = ref('') + + const fetchWatchlist = async () => { + const user = auth.currentUser + if (!user) return + + const snapshot = await getDocs(collection(db, 'users', user.uid, 'watchlist')) + watchlist.value = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })) + } + + const filteredWatchlist = computed(() => { + if (!filter.value) return watchlist.value + return watchlist.value.filter(movie => movie.status === filter.value) + }) + + onMounted(() => { + fetchWatchlist() + }) +</script> + +<style scoped> + /* Reuse your styles or tweak as needed */ + .watchlist { + padding: 2rem; + text-align: center; + } + + .filters { + margin-bottom: 1rem; + } + + .movie-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 1.2rem; + } +</style> + \ No newline at end of file diff --git a/movie-group-8/src/views/Profile.vue b/movie-group-8/src/views/Profile.vue index 3314b81e266a166a519fa0ee6c5461b23a8be0c0..7e01bc735dbae92af82aaa50e9e4bf31980eaab9 100644 --- a/movie-group-8/src/views/Profile.vue +++ b/movie-group-8/src/views/Profile.vue @@ -1 +1,103 @@ -<template>Profile</template> \ No newline at end of file +<template> + <div class="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-neutral-900"> + <div class="mt-7 bg-white border border-gray-200 rounded-xl shadow-2xs dark:bg-neutral-900 dark:border-neutral-700 min-w-96"> + <div class="p-4 sm:p-7"> + <div class="text-center"> + <h1 class="block text-2xl font-bold text-gray-800 dark:text-white">Profile</h1> + <p class="mt-2 text-sm text-gray-600 dark:text-neutral-400"> + Manage your account information + </p> + </div> + + <div class="mt-5"> + <form @submit.prevent="updateProfile"> + <div class="grid gap-y-4"> + <div> + <label class="block text-sm mb-2 dark:text-white">Email</label> + <input type="email" :value="userEmail" disabled class="w-full border border-gray-400 rounded-lg p-2 bg-gray-100 dark:bg-neutral-800 dark:text-white" /> + </div> + + <div> + <label class="block text-sm mb-2 dark:text-white">Display Name</label> + <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">New Password</label> + <input type="password" v-model="newPassword" class="w-full border border-gray-400 rounded-lg p-2 dark:text-white" /> + <p v-if="newPassword && passwordError" class="text-red-500 text-sm mt-1">{{ passwordError }}</p> + </div> + + <button + type="submit" + :disabled="loading || !!passwordError" + class="w-full py-3 bg-blue-600 text-white rounded-lg flex justify-center items-center gap-2 hover:bg-blue-700 transition duration-300 disabled:opacity-50 disabled:pointer-events-none" + > + <span v-if="!loading">Update Profile</span> + <span v-else>Saving...</span> + </button> + </div> + </form> + </div> + </div> + </div> + </div> +</template> + +<script setup> + import { ref, computed, onMounted } from 'vue'; + import { getAuth, updateProfile, updatePassword } from 'firebase/auth'; + + const auth = getAuth(); + const user = auth.currentUser; + + const loading = ref(false); + const userEmail = ref(''); + const displayName = ref(''); + const newPassword = ref(''); + + // Load current profile info + onMounted(() => { + if (user) { + userEmail.value = user.email; + displayName.value = user.displayName || ''; + } + }); + + // Password validation + const passwordError = computed(() => { + if (!newPassword.value) return ''; + if (newPassword.value.length < 8) return "Password must be at least 8 characters."; + if (!/[A-Z]/.test(newPassword.value)) return "Must include at least 1 uppercase letter."; + if (!/[0-9]/.test(newPassword.value)) return "Must include at least 1 number."; + if (!/[@$!%*?&]/.test(newPassword.value)) return "Must include at least 1 special character (@$!%*?&)."; + return ''; + }); + + // Update function + const updateProfile = async () => { + if (passwordError.value) return; + + loading.value = true; + try { + if (user) { + // Update display name + await updateProfile(user, { + displayName: displayName.value, + }); + + // Update password if provided + if (newPassword.value) { + await updatePassword(user, newPassword.value); + } + + alert('Profile updated successfully!'); + } + } catch (error) { + alert(error.message); + } finally { + loading.value = false; + } + }; +</script> + \ No newline at end of file diff --git a/movie-group-8/src/views/Watchlist.vue b/movie-group-8/src/views/Watchlist.vue index 881fa1732ebc44f101dee618914366c413913c24..58cf86606685c77a69557491d6f210d3ba2ca52d 100644 --- a/movie-group-8/src/views/Watchlist.vue +++ b/movie-group-8/src/views/Watchlist.vue @@ -1 +1,26 @@ -<template>Watchlist</template> \ No newline at end of file +<template> + <Watchlist /> +</template> + +<script setup> +import Watchlist from '@/components/Watchlist.vue' + +import { getFirestore, doc, setDoc } from 'firebase/firestore' +import { getAuth } from 'firebase/auth' + +const addToWatchlist = async (movie) => { + const user = getAuth().currentUser + if (!user) return alert('You need to log in.') + + 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' + }) + + alert('Movie added to watchlist!') +} +</script> \ No newline at end of file