Skip to content
Snippets Groups Projects
Commit 08d1a3a1 authored by Tejas's avatar Tejas
Browse files

Films Page Overhauled, Implemented Search, Fixed Filters.

parent cd543adc
Branches feature/set_on_creation
No related tags found
1 merge request!6Merge EC into Main
......@@ -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>
......@@ -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>
......
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));
}
});
<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>
<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">
......
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