Skip to content
Snippets Groups Projects
Commit b16f2665 authored by ABHI N's avatar ABHI N
Browse files

Added movie details page

parents
No related branches found
No related tags found
2 merge requests!5Push to Main,!2Merging Top-Rated to Test
Showing with 732 additions and 0 deletions
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
import { initializeApp } from "firebase/app";
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyDiAEv_XsHelEiuvJsPDOEBZRcsr4Mg4R4",
authDomain: "movie-group-8.firebaseapp.com",
projectId: "movie-group-8",
storageBucket: "movie-group-8.firebasestorage.app",
messagingSenderId: "767837078763",
appId: "1:767837078763:web:e64ea7235d421e46852aae"
};
initializeApp(firebaseConfig);
const app = createApp(App);
app.use(router);
app.mount("#app");
import { getAuth, onAuthStateChanged } from 'firebase/auth';
import { createRouter, createWebHistory } from 'vue-router';
import Films from '@/views/Films.vue'
import FilmDetails from '@/views/FilmDetails.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: () => import('../views/Home.vue') },
{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') },
{
path: '/films',
name: 'Films',
component: Films,
meta: { requiresAuth: true }
},
{
path: '/films/:id',
name: 'FilmDetails',
component: FilmDetails,
},
{
path: '/watchlist',
component: () => import('../views/Watchlist.vue'),
meta: { requiresAuth: true },
},
{
path: '/profile',
component: () => import('../views/Profile.vue'),
meta: { requiresAuth: true },
},
{
path: '/settings',
component: () => import('../views/Settings.vue'),
meta: { requiresAuth: true },
},
],
});
const getCurrentUser = () => {
return new Promise((resolve, reject) => {
const removeListener = onAuthStateChanged(
getAuth(),
user => {
removeListener();
resolve(user);
},
reject);
});
};
router.beforeEach(async (to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
if (await getCurrentUser()) {
next();
} else {
alert('You must be logged in to see this page');
next("/");
}
} else {
next();
}
});
export default router;
\ No newline at end of file
@import "tailwindcss";
\ No newline at end of file
<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>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
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
})
</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>
\ No newline at end of file
<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>
<!-- Movie grid -->
<MovieList :movies="results" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
import MovieList from '../components/MovieList.vue'
const API_KEY = import.meta.env.VITE_TMDB_API_KEY
const BASE = import.meta.env.VITE_TMDB_BASE
const term = ref('')
const results = 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
}
}
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
}
}
function onSearch() {
if (term.value.trim()) {
search(term.value)
} else {
fetchPopular()
}
}
onMounted(fetchPopular)
</script>
<style scoped>
.films-container {
padding-top: 6rem; /* adjust to nav height + spacing */
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.search-wrapper {
width: 100%;
max-width: 500px;
margin-bottom: 1.5rem;
display: flex;
justify-content: center;
}
.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);
}
.loading,
.error {
margin: 1rem 0;
font-weight: bold;
}
</style>
\ No newline at end of file
<template>
<div class="h-full 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">
<div class="mb-10 lg:mb-0 lg:col-span-6 lg:col-start-8 lg:order-2">
<h2 class="text-2xl text-neutral-200 font-bold sm:text-3xl">
Welcome to SceneIt! Your new portal for all your movie needs.
</h2>
<nav class="grid gap-4 mt-5 md:mt-10" aria-label="Tabs" role="tablist" aria-orientation="vertical">
<button @click="setTab('list')" :class="{ 'bg-neutral-700 shadow-md': activeTab === 'list' }" class="text-start hover:bg-neutral-700 focus:bg-neutral-700 p-4 md:p-5 rounded-xl">
<span class="flex gap-x-6">
<svg class="shrink-0 mt-2 size-6 md:size-7 text-blue-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5.5A3.5 3.5 0 0 1 8.5 2H12v7H8.5A3.5 3.5 0 0 1 5 5.5z"/><path d="M12 2h3.5a3.5 3.5 0 1 1 0 7H12V2z"/><path d="M12 12.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 1 1-7 0z"/><path d="M5 19.5A3.5 3.5 0 0 1 8.5 16H12v3.5a3.5 3.5 0 1 1-7 0z"/><path d="M5 12.5A3.5 3.5 0 0 1 8.5 9H12v7H8.5A3.5 3.5 0 0 1 5 12.5z"/></svg>
<span class="grow">
<span class="block text-lg font-semibold text-blue-500">Movie Lists</span>
<span class="block mt-1 text-neutral-200">Organize your favorites, track what you've seen, and never forget a great film again!</span>
</span>
</span>
</button>
<button @click="setTab('reviews')" :class="{ 'bg-neutral-700 shadow-md': activeTab === 'reviews' }" class="text-start hover:bg-neutral-700 focus:bg-neutral-700 p-4 md:p-5 rounded-xl">
<span class="flex gap-x-6">
<svg class="shrink-0 mt-2 size-6 md:size-7 text-blue-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/><path d="M5 3v4"/><path d="M19 17v4"/><path d="M3 5h4"/><path d="M17 19h4"/></svg>
<span class="grow">
<span class="block text-lg font-semibold text-blue-500">Movie Reviews</span>
<span class="block mt-1 text-neutral-200">Read reviews from fellow movie lovers or share your own.</span>
</span>
</span>
</button>
<button @click="setTab('tmdb')" :class="{ 'bg-neutral-700 shadow-md': activeTab === 'tmdb' }" class="text-start hover:bg-neutral-700 focus:bg-neutral-700 p-4 md:p-5 rounded-xl">
<span class="flex gap-x-6">
<img class="h-10 w-auto" src="../assets/TMDB.svg" alt="TMDb Logo" />
<span class="grow">
<span class="block text-lg font-semibold text-blue-500">Powered by TMDB</span>
<span class="block mt-1 text-neutral-200">Data provided by The Movie Database.</span>
</span>
</span>
</button>
</nav>
</div>
<div class="lg:col-span-6">
<div class="relative">
<img v-if="activeTab === 'list'" class="shadow-xl shadow-gray-900/20 rounded-xl object-cover w-[560px] h-[720px]" src="../assets/home_poster.png" alt="Movie Lists">
<img v-if="activeTab === 'reviews'" class="shadow-xl shadow-gray-900/20 rounded-xl object-cover w-[560px] h-[720px]" src="../assets/review_poster.png" alt="Movie Reviews">
<img v-if="activeTab === 'tmdb'" class="shadow-xl shadow-gray-900/20 rounded-xl object-cover w-[560px] h-[720px]" src="../assets/tmdb_poster.png" alt="Powered by TMDB">
</div>
</div>
</div>
<div class="absolute inset-0 bg-neutral-800 rounded-xl"></div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
activeTab: 'list',
tabs: ['list', 'reviews', 'tmdb'],
};
},
methods: {
setTab(tab) {
this.activeTab = tab;
}
},
mounted() {
setInterval(() => {
const currentIndex = this.tabs.indexOf(this.activeTab);
const nextIndex = (currentIndex + 1) % this.tabs.length;
this.activeTab = this.tabs[nextIndex];
}, 5000);
}
};
</script>
\ 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">Login</h1>
<p class="mt-2 text-sm text-gray-600 dark:text-neutral-400">
Don't have an account?
<a class="text-blue-600 decoration-2 hover:underline focus:outline-hidden focus:underline font-medium dark:text-blue-500" href="/register">
Sign up here
</a>
</p>
</div>
<div class="mt-5">
<button @click="signInWithGoogle" type="button" class="w-full py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-2xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 dark:bg-neutral-900 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-800">
<svg class="w-4 h-auto" width="46" height="47" viewBox="0 0 46 47" fill="none">
<path d="M46 24.0287C46 22.09 45.8533 20.68 45.5013 19.2112H23.4694V27.9356H36.4069C36.1429 30.1094 34.7347 33.37 31.5957 35.5731L31.5663 35.8669L38.5191 41.2719L38.9885 41.3306C43.4477 37.2181 46 31.1669 46 24.0287Z" fill="#4285F4"/>
<path d="M23.4694 47C29.8061 47 35.1161 44.9144 39.0179 41.3012L31.625 35.5437C29.6301 36.9244 26.9898 37.8937 23.4987 37.8937C17.2793 37.8937 12.0281 33.7812 10.1505 28.1412L9.88649 28.1706L2.61097 33.7812L2.52296 34.0456C6.36608 41.7125 14.287 47 23.4694 47Z" fill="#34A853"/>
<path d="M10.1212 28.1413C9.62245 26.6725 9.32908 25.1156 9.32908 23.5C9.32908 21.8844 9.62245 20.3275 10.0918 18.8588V18.5356L2.75765 12.8369L2.52296 12.9544C0.909439 16.1269 0 19.7106 0 23.5C0 27.2894 0.909439 30.8731 2.49362 34.0456L10.1212 28.1413Z" fill="#FBBC05"/>
<path d="M23.4694 9.07688C27.8699 9.07688 30.8622 10.9863 32.5344 12.5725L39.1645 6.11C35.0867 2.32063 29.8061 0 23.4694 0C14.287 0 6.36607 5.2875 2.49362 12.9544L10.0918 18.8588C11.9987 13.1894 17.25 9.07688 23.4694 9.07688Z" fill="#EB4335"/>
</svg>
Sign in with Google
</button>
<div class="py-3 flex items-center text-xs text-gray-400 uppercase before:flex-1 before:border-t before:border-gray-200 before:me-6 after:flex-1 after:border-t after:border-gray-200 after:ms-6 dark:text-neutral-500 dark:before:border-neutral-600 dark:after:border-neutral-600">Or</div>
<form @submit.prevent="login">
<div class="grid gap-y-4">
<div>
<label class="block text-sm mb-2 dark:text-white">Email Address</label>
<input type="email" v-model="email" class="w-full border border-gray-400 rounded-lg p-2 dark:text-white" required />
</div>
<div>
<div class="flex justify-between items-center mb-2">
<label class="block text-sm dark:text-white">Password</label>
<a class="text-sm text-blue-600 decoration-2 hover:underline focus:outline-hidden focus:underline font-medium dark:text-blue-500" href="/recover-account">
Forgot password?
</a>
</div>
<input type="password" v-model="password" class="w-full border border-gray-400 rounded-lg p-2 dark:text-white" required />
</div>
<button
type="submit"
@click="login"
:disabled="loading"
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"
>
<svg v-if="loading" class="w-5 h-5 animate-spin text-white" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" stroke-linecap="round" class="opacity-25"/>
<path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="4" stroke-linecap="round" class="opacity-75"/>
</svg>
<span v-if="!loading">Sign in</span>
<span v-else>Loading...</span>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</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
<template>Profile</template>
\ No newline at end of file
<template>Recover Account</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">Register</h1>
<p class="mt-2 text-sm text-gray-600 dark:text-neutral-400">
Already have an account?
<a class="text-blue-600 decoration-2 hover:underline font-medium dark:text-blue-500" href="/login">
Sign in here
</a>
</p>
</div>
<div class="mt-5">
<button @click="signInWithGoogle" type="button" class="w-full py-3 px-4 flex justify-center items-center gap-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-2xs hover:bg-gray-50 dark:bg-neutral-900 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-800">
<svg class="w-4 h-auto" width="46" height="47" viewBox="0 0 46 47" fill="none">
<path d="M46 24.0287C46 22.09 45.8533 20.68 45.5013 19.2112H23.4694V27.9356H36.4069C36.1429 30.1094 34.7347 33.37 31.5957 35.5731L31.5663 35.8669L38.5191 41.2719L38.9885 41.3306C43.4477 37.2181 46 31.1669 46 24.0287Z" fill="#4285F4"/>
<path d="M23.4694 47C29.8061 47 35.1161 44.9144 39.0179 41.3012L31.625 35.5437C29.6301 36.9244 26.9898 37.8937 23.4987 37.8937C17.2793 37.8937 12.0281 33.7812 10.1505 28.1412L9.88649 28.1706L2.61097 33.7812L2.52296 34.0456C6.36608 41.7125 14.287 47 23.4694 47Z" fill="#34A853"/>
<path d="M10.1212 28.1413C9.62245 26.6725 9.32908 25.1156 9.32908 23.5C9.32908 21.8844 9.62245 20.3275 10.0918 18.8588V18.5356L2.75765 12.8369L2.52296 12.9544C0.909439 16.1269 0 19.7106 0 23.5C0 27.2894 0.909439 30.8731 2.49362 34.0456L10.1212 28.1413Z" fill="#FBBC05"/>
<path d="M23.4694 9.07688C27.8699 9.07688 30.8622 10.9863 32.5344 12.5725L39.1645 6.11C35.0867 2.32063 29.8061 0 23.4694 0C14.287 0 6.36607 5.2875 2.49362 12.9544L10.0918 18.8588C11.9987 13.1894 17.25 9.07688 23.4694 9.07688Z" fill="#EB4335"/>
</svg>
Sign up with Google
</button>
<div class="py-3 flex items-center text-xs text-gray-400 uppercase before:flex-1 before:border-t before:border-gray-200 after:flex-1 after:border-t after:border-gray-200 dark:text-neutral-500 dark:before:border-neutral-600 dark:after:border-neutral-600">Or</div>
<form @submit.prevent="register">
<div class="grid gap-y-4">
<div>
<label class="block text-sm mb-2 dark:text-white">Email address</label>
<input type="email" v-model="email" 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">Password</label>
<input type="password" v-model="password" class="w-full border border-gray-400 rounded-lg p-2 dark:text-white" required />
<p v-if="password && passwordError" class="text-red-500 text-sm mt-1">{{ passwordError }}</p>
</div>
<div>
<label class="block text-sm mb-2 dark:text-white">Confirm Password</label>
<input type="password" v-model="confirmPassword" class="w-full border border-gray-400 rounded-lg p-2 dark:text-white" required />
<p v-if="confirmPassword && confirmPasswordError" class="text-red-500 text-sm mt-1">{{ confirmPasswordError }}</p>
</div>
<button
type="submit"
:disabled="loading || !!passwordError || !!confirmPasswordError"
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">Sign up</span>
<span v-else>Loading...</span>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { getAuth, createUserWithEmailAndPassword, signInWithPopup, GoogleAuthProvider } from 'firebase/auth';
import { useRouter } from 'vue-router';
const email = ref('');
const password = ref('');
const confirmPassword = ref('');
const router = useRouter();
const loading = ref(false);
// Password validation
const passwordError = computed(() => {
if (password.value.length < 8) return "Password must be at least 8 characters.";
if (!/[A-Z]/.test(password.value)) return "Must include at least 1 uppercase letter.";
if (!/[0-9]/.test(password.value)) return "Must include at least 1 number.";
if (!/[@$!%*?&]/.test(password.value)) return "Must include at least 1 special character (@$!%*?&).";
return "";
});
// Confirm password validation
const confirmPasswordError = computed(() => {
if (confirmPassword.value && confirmPassword.value !== password.value) {
return "Passwords do not match.";
}
return "";
});
// Register function
const register = async () => {
if (passwordError.value || confirmPasswordError.value) return;
loading.value = true;
try {
const auth = getAuth();
await createUserWithEmailAndPassword(auth, email.value, password.value);
router.push('/films');
} catch (error) {
alert(error.message);
} finally {
loading.value = false;
}
};
</script>
<template>Settings</template>
\ No newline at end of file
<script setup>
import TopRatedMovies from '@/components/TopRatedMovies.vue'
</script>
<template>
<TopRatedMovies />
<template>
<div class="nyt-header">
<h1>The Movies We've Loved Since 2000</h1>
<p>
Explore top-rated films using the filters below — by genre and year — and rediscover hidden gems.
</p>
</div>
<div class="filters">
<select v-model="genre">
<option value="">Pick a genre ...</option>
<option v-for="g in genres" :key="g.id" :value="g.id">{{ g.name }}</option>
</select>
<select v-model="year">
<option v-for="y in Array.from({length: 25}, (_, i) => 2000 + i)" :key="y" :value="y">
{{ y }}
</option>
</select>
</div>
<h2 class="section-title">Our favorite movies from {{ year }}</h2>
<div class="movie-grid">
<div v-for="movie in movies" :key="movie.id" class="movie-card">
<img
v-if="movie.poster_path"
: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>
</div>
</div>
</template>
</template>
<template>Watchlist</template>
\ No newline at end of file
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(),
tailwindcss()
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment