From 8bc6c0b32cbdd68f3901b769cdab8def33f0725a Mon Sep 17 00:00:00 2001 From: sc02613 <sc02613@surrey.ac.uk> Date: Thu, 22 May 2025 00:47:01 +0100 Subject: [PATCH] The paths and views for review page added --- movie-group-8/src/router/index.js | 9 + movie-group-8/src/views/FilmDetails.vue | 339 ++++++++++++------------ movie-group-8/src/views/ReviewPage.vue | 230 ++++++++++++++++ 3 files changed, 415 insertions(+), 163 deletions(-) create mode 100644 movie-group-8/src/views/ReviewPage.vue diff --git a/movie-group-8/src/router/index.js b/movie-group-8/src/router/index.js index 8cad4e4..f4763b4 100644 --- a/movie-group-8/src/router/index.js +++ b/movie-group-8/src/router/index.js @@ -2,6 +2,7 @@ import { getAuth, onAuthStateChanged } from 'firebase/auth'; import { createRouter, createWebHistory } from 'vue-router'; import Films from '@/views/Films.vue' import FilmDetails from '@/views/FilmDetails.vue' +import ReviewPage from '@/views/ReviewPage.vue' const router = createRouter({ history: createWebHistory(), @@ -25,6 +26,13 @@ const router = createRouter({ component: FilmDetails, }, + { + path: '/films/:id/review', + name: 'ReviewFilm', + component: ReviewPage, + props: true, + }, + { path: '/watchlist', component: () => import('../views/WatchlistView.vue'), @@ -40,6 +48,7 @@ const router = createRouter({ component: () => import('../views/Settings.vue'), meta: { requiresAuth: true }, }, + ], }); diff --git a/movie-group-8/src/views/FilmDetails.vue b/movie-group-8/src/views/FilmDetails.vue index 0e8eda8..8194f5c 100644 --- a/movie-group-8/src/views/FilmDetails.vue +++ b/movie-group-8/src/views/FilmDetails.vue @@ -1,170 +1,183 @@ <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="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> - </div> - <button @click="addToWatchlist(movie)">Add to Watchlist</button> + <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="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> </div> + + <button + @click="addToWatchlist(movie)" + class="mt-4 px-4 py-2 bg-green-600 rounded text-white hover:bg-green-700" + > + Add to Watchlist + </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" + > + Review this Film + </RouterLink> </div> </div> - </template> - - <script setup> - import { ref, onMounted } from 'vue' - import { useRoute } from 'vue-router' - import { addToWatchlist } from '@/composables/useWatchlist.js' - - const route = useRoute() - const movie = ref(null) - const trailerUrl = ref('') - const loading = ref(true) - const error = ref('') - - function formatDate(dateString) { - if (!dateString) return 'Unknown' - return new Date(dateString).toLocaleDateString(undefined, { - year: 'numeric', - month: 'long', - day: 'numeric' - }) - } - - async function fetchMovieDetails(id) { - try { - const res = await fetch( - `${import.meta.env.VITE_TMDB_BASE}/movie/${id}?api_key=${import.meta.env.VITE_TMDB_API_KEY}` - ) - if (!res.ok) throw new Error('Failed to fetch movie details') - movie.value = await res.json() - } catch (err) { - error.value = err.message - } - } - - async function fetchMovieVideos(id) { - try { - const res = await fetch( - `${import.meta.env.VITE_TMDB_BASE}/movie/${id}/videos?api_key=${import.meta.env.VITE_TMDB_API_KEY}` - ) - if (!res.ok) throw new Error('Failed to fetch movie videos') - const data = await res.json() - const trailer = data.results.find(v => v.type === 'Trailer' && v.site === 'YouTube') - if (trailer) { - trailerUrl.value = `https://www.youtube.com/embed/${trailer.key}` - } - } catch { - // ignore trailer errors - } - } - - onMounted(async () => { - const id = route.params.id - await fetchMovieDetails(id) - await fetchMovieVideos(id) - loading.value = false + </div> +</template> + +<script setup> +import { ref, onMounted } from 'vue' +import { useRoute } from 'vue-router' +import { addToWatchlist } from '@/composables/useWatchlist.js' + +const route = useRoute() +const movie = ref(null) +const trailerUrl = ref('') +const loading = ref(true) +const error = ref('') + +function formatDate(dateString) { + if (!dateString) return 'Unknown' + return new Date(dateString).toLocaleDateString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric' }) - </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; +} + +async function fetchMovieDetails(id) { + try { + const res = await fetch( + `${import.meta.env.VITE_TMDB_BASE}/movie/${id}?api_key=${import.meta.env.VITE_TMDB_API_KEY}` + ) + if (!res.ok) throw new Error('Failed to fetch movie details') + movie.value = await res.json() + } catch (err) { + error.value = err.message } - - .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; +} + +async function fetchMovieVideos(id) { + try { + const res = await fetch( + `${import.meta.env.VITE_TMDB_BASE}/movie/${id}/videos?api_key=${import.meta.env.VITE_TMDB_API_KEY}` + ) + if (!res.ok) throw new Error('Failed to fetch movie videos') + const data = await res.json() + const trailer = data.results.find(v => v.type === 'Trailer' && v.site === 'YouTube') + if (trailer) { + trailerUrl.value = `https://www.youtube.com/embed/${trailer.key}` + } + } catch { + // ignore trailer errors } - </style> \ No newline at end of file +} + +onMounted(async () => { + const id = route.params.id + await fetchMovieDetails(id) + await fetchMovieVideos(id) + 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/ReviewPage.vue b/movie-group-8/src/views/ReviewPage.vue new file mode 100644 index 0000000..3f130f9 --- /dev/null +++ b/movie-group-8/src/views/ReviewPage.vue @@ -0,0 +1,230 @@ +<template> + <div class="review-page"> + <!-- Back --> + <button class="back-button" @click="$router.back()">↠Back</button> + + <!-- Loading / Error states --> + <div v-if="loading" class="loading">Loading movie...</div> + <div v-else-if="error" class="error">Error: {{ error }}</div> + + <!-- Main content --> + <div v-else class="content"> + <!-- Poster & Title --> + <div class="header"> + <img + v-if="movie.poster_path" + :src="`https://image.tmdb.org/t/p/w300${movie.poster_path}`" + :alt="movie.title + ' poster'" + class="poster" + /> + <h1 class="title">{{ movie.title }}</h1> + </div> + + <!-- Star Rating --> + <div class="rating"> + <span + v-for="star in 5" + :key="star" + class="star" + :class="{ filled: star <= userRating }" + @click="setRating(star)" + > + ★ + </span> + <span class="rating-value">{{ userRating }} / 5</span> + </div> + + <!-- Favourite Toggle --> + <button + class="favourite-btn" + :class="{ fav: isFavourite }" + @click="toggleFavourite" + > + {{ isFavourite ? '★ Favourite' : '☆ Add to Favourites' }} + </button> + + <!-- Review Text --> + <textarea + v-model="reviewText" + placeholder="Write your review…" + class="review-text" + rows="6" + ></textarea> + + <!-- Submit --> + <button class="submit-btn" @click="submitReview"> + Submit Review + </button> + </div> + </div> + </template> + + <script setup> + import { ref, onMounted } from 'vue' + import { useRoute, useRouter } from 'vue-router' + + const route = useRoute() + const router = useRouter() + + // State + const movie = ref(null) + const loading = ref(true) + const error = ref('') + const userRating = ref(0) + const isFavourite = ref(false) + const reviewText = ref('') + + // Fetch movie details + async function fetchMovie() { + try { + const id = route.params.id + const res = await fetch( + `${import.meta.env.VITE_TMDB_BASE}/movie/${id}?api_key=${import.meta.env.VITE_TMDB_API_KEY}` + ) + if (!res.ok) throw new Error('Failed to load movie') + movie.value = await res.json() + } catch (err) { + error.value = err.message + } finally { + loading.value = false + } + } + + // Rating handlers + function setRating(star) { + userRating.value = star + } + + // Favourite toggle + function toggleFavourite() { + isFavourite.value = !isFavourite.value + } + + // Submit handler (stub — wire up to Firestore or your backend) + function submitReview() { + console.log({ + movieId: route.params.id, + rating: userRating.value, + favourite: isFavourite.value, + review: reviewText.value, + }) + // e.g. use Firestore: + // const db = getFirestore(); + // setDoc(doc(db, 'reviews', `${user.uid}_${movieId}`), { ... }) + router.back() + } + + onMounted(fetchMovie) + </script> + + <style scoped> + .review-page { + background: #121212; + color: #fff; + min-height: 100vh; + padding: 2rem; + box-sizing: border-box; + } + + .back-button { + background: transparent; + border: none; + color: #fff; + font-size: 1rem; + } + + .loading, + .error { + text-align: center; + margin: 2rem 0; + } + + .content { + max-width: 600px; + margin: 2rem auto; + background: #1e1e1e; + padding: 2rem; + border-radius: 8px; + } + + .header { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1.5rem; + } + + .poster { + width: 100px; + border-radius: 4px; + } + + .title { + font-size: 1.75rem; + margin: 0; + } + + .rating { + display: flex; + align-items: center; + margin-bottom: 1rem; + } + + .star { + font-size: 2rem; + cursor: pointer; + transition: transform 0.1s; + margin-right: 0.25rem; + color: #555; + } + .star.filled { + color: #f5c518; + } + .star:hover { + transform: scale(1.2); + } + + .rating-value { + margin-left: 0.5rem; + color: #bbb; + } + + .favourite-btn { + background: #333; + border: none; + color: #bbb; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + margin-bottom: 1rem; + } + .favourite-btn.fav { + background: #d32f2f; + color: #fff; + } + + .review-text { + width: 100%; + padding: 0.75rem; + border: none; + border-radius: 4px; + background: #2a2a2a; + color: #fff; + margin-bottom: 1rem; + resize: vertical; + } + + .submit-btn { + background: #1976d2; + border: none; + color: #fff; + padding: 0.75rem 1.5rem; + border-radius: 4px; + cursor: pointer; + width: 100%; + } + .submit-btn:hover { + background: #1565c0; + } + </style> + \ No newline at end of file -- GitLab