Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ import Contact from "./pages/Contact";
import Terms from "./pages/Terms";
import Privacy from "./pages/Privacy";

// New dedicated pages
import Recommended from "./pages/Recommended";
import TopTrending from "./pages/TopTrending";
import TopRated from "./pages/TopRated";

// Placeholder pages
function Placeholder({ title }) {
return <div style={{ padding: 40, textAlign: 'center', color: '#888' }}><h1>{title} (Coming Soon)</h1></div>;
Expand Down Expand Up @@ -63,9 +68,9 @@ export default function App() {
<Route path="/terms" element={<Terms />} />
<Route path="/privacy" element={<Privacy />} />
<Route path="/discover" element={<Placeholder title="Discover" />} />
<Route path="/trending" element={<Placeholder title="Trending" />} />
<Route path="/top-rated" element={<Placeholder title="Top Rated" />} />
<Route path="/recommendations" element={<Placeholder title="Recommendations" />} />
<Route path="/trending" element={<TopTrending />} />
<Route path="/top-rated" element={<TopRated />} />
<Route path="/recommendations" element={<ProtectedRoute><Recommended /></ProtectedRoute>} />
<Route path="/profile" element={<ProtectedRoute><UserProfile /></ProtectedRoute>} />
<Route path="/forgot-password" element={ <PublicRoute> <ForgotPassword /></PublicRoute>}/>

Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/ReviewComments.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export default function ReviewComments({ reviewId, comments = [], onUpdate, isAu
{comment.user?.name?.[0]?.toUpperCase() || "?"}
</div>
<div className="flex-1">
<div className="bg-gray-50 dark:bg-gray-800/50 rounded-lg p-3 relative group">
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-3 relative group">
<div className="flex items-center justify-between mb-1">
<span className="font-medium text-gray-900 dark:text-white">
{comment.user?.name || "Anonymous"}
Expand Down Expand Up @@ -151,7 +151,7 @@ export default function ReviewComments({ reviewId, comments = [], onUpdate, isAu
<textarea
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm resize-none focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full p-2 border border-gray-300 dark:border-gray-700 rounded bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm resize-none focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={2}
autoFocus
/>
Expand Down Expand Up @@ -179,7 +179,7 @@ export default function ReviewComments({ reviewId, comments = [], onUpdate, isAu
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="Add a comment..."
className="flex-1 min-w-0 rounded-lg bg-gray-50 dark:bg-gray-800/50 border border-gray-200/5 dark:border-white/10 px-4 py-2 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500/50"
className="flex-1 min-w-0 rounded-lg bg-gray-50 dark:bg-gray-900 border border-gray-200/5 dark:border-gray-700 px-4 py-2 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500/50"
/>
<button
type="submit"
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/pages/Landing.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ export default function Landing() {
{isLoggedIn() && recommended.length > 0 && (
<RowCarousel
title="Recommended for you"
to="/search?recommended=1"
to="/recommendations"
items={recommended}
loading={loading.recommended}
onCardClick={(m) => navigate(`/movies/${m.id}`)}
Expand All @@ -304,15 +304,15 @@ export default function Landing() {

<RowCarousel
title="Trending Now"
to="/search?sort_by=popularity"
to="/trending"
items={trending}
loading={loading.trending}
onCardClick={(m) => navigate(`/movies/${m.id}`)}
/>

<RowCarousel
title="Top Rated"
to="/search?sort_by=rating"
to="/top-rated"
items={topRated}
loading={loading.topRated}
onCardClick={(m) => navigate(`/movies/${m.id}`)}
Expand Down
150 changes: 150 additions & 0 deletions frontend/src/pages/Recommended.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { motion } from "framer-motion";
import api from "../api/api";
import { isLoggedIn } from "../utils/auth";

const img = {
poster: (path) => path ? `https://image.tmdb.org/t/p/w500${path}` : '/images/no-image-300x450.png',
};

const ratingOf = (m) => {
const r = m?.rating ?? m?.vote_average;
const n = parseFloat(r);
return !isNaN(n) ? n.toFixed(1) : "N/A";
};

const yearOf = (m) => (m?.release_date ? m.release_date.slice(0, 4) : "—");

function MovieCard({ movie, onClick }) {
return (
<motion.button
onClick={() => onClick?.(movie)}
className="group text-left rounded-xl border border-gray-200/50 dark:border-white/10 bg-white/60 dark:bg-gray-800/40 hover:bg-white/80 dark:hover:bg-gray-800/60 hover:border-gray-300 dark:hover:border-white/20 hover:shadow-lg transition-all duration-200 overflow-hidden"
whileHover={{ y: -2 }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<div className="aspect-[2/3] overflow-hidden rounded-t-xl">
<img
src={img.poster(movie.poster_path)}
alt={movie.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
<div className="p-4">
<h3 className="font-semibold text-gray-900 dark:text-white line-clamp-2 mb-1">{movie.title}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">{yearOf(movie)}</p>
<div className="flex items-center justify-between">
<div className="text-xs text-yellow-600 dark:text-yellow-400 font-medium">
⭐ {ratingOf(movie)}
</div>
</div>
</div>
</motion.button>
);
}

function SkeletonCard() {
return (
<div className="rounded-xl border border-gray-200 dark:border-white/10 bg-white/60 dark:bg-gray-800/40 overflow-hidden animate-pulse">
<div className="aspect-[2/3] bg-gray-200 dark:bg-gray-700"></div>
<div className="p-4">
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded mb-2"></div>
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-2/3 mb-2"></div>
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-1/3"></div>
</div>
</div>
);
}

export default function Recommended() {
const navigate = useNavigate();
const [movies, setMovies] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
if (!isLoggedIn()) {
navigate('/login');
return;
}

const fetchRecommended = async () => {
try {
setLoading(true);
const response = await api.get('/recommended');
setMovies(response.data || []);
} catch (err) {
console.error('Error fetching recommendations:', err);
setError('Failed to load recommendations');
} finally {
setLoading(false);
}
};

fetchRecommended();
}, [navigate]);

if (!isLoggedIn()) {
return null; // Will redirect above
}

if (error) {
return (
<div className="min-h-screen bg-white dark:bg-black dark:text-gray-200 pt-24">
<div className="max-w-7xl mx-auto px-6 py-8">
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">Recommended for You</h1>
<p className="text-red-500">{error}</p>
</div>
</div>
</div>
);
}

return (
<div className="min-h-screen bg-white dark:bg-black dark:text-gray-200 pt-24">
<div className="max-w-7xl mx-auto px-6 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">Recommended for You</h1>
<p className="text-gray-600 dark:text-gray-400">
Personalized movie recommendations based on your viewing history and preferences
</p>
</div>

{loading ? (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-6">
{Array.from({ length: 18 }).map((_, i) => (
<SkeletonCard key={i} />
))}
</div>
) : movies.length === 0 ? (
<div className="text-center py-16">
<div className="mb-4 text-gray-400 text-6xl">🎬</div>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">No Recommendations Yet</h3>
<p className="text-gray-600 dark:text-gray-400 mb-6">
Start rating and watching movies to get personalized recommendations!
</p>
<button
onClick={() => navigate('/search')}
className="px-6 py-3 bg-yellow-500 text-black font-semibold rounded-lg hover:bg-yellow-600 transition-colors"
>
Explore Movies
</button>
</div>
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-6">
{movies.map((movie) => (
<MovieCard
key={movie.id}
movie={movie}
onClick={(m) => navigate(`/movies/${m.id}`)}
/>
))}
</div>
)}
</div>
</div>
);
}
28 changes: 14 additions & 14 deletions frontend/src/pages/ReviewDetails.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,19 +115,19 @@ const ReviewDetails = () => {

const renderStars = rating =>
Array.from({ length: 5 }, (_, i) =>
i < rating ? <FaStar key={i} className="text-yellow-400" /> : <FaRegStar key={i} className="text-gray-400" />
i < rating ? <FaStar key={i} className="text-yellow-400" /> : <FaRegStar key={i} className="text-gray-400 dark:text-gray-500" />
);

const formatDate = dateString =>
new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });


if (loading) return (
<div className="min-h-screen bg-gray-50 dark:bg-black flex justify-center items-center pt-24">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-red-600"></div>
</div>
);


if (error) return <div className="min-h-screen bg-gray-50 dark:bg-black flex flex-col items-center justify-center text-red-400 pt-24">{error}</div>;
if (!review) return <div className="min-h-screen bg-gray-50 dark:bg-black flex items-center justify-center text-gray-900 dark:text-white pt-24">No review found</div>;

Expand All @@ -137,17 +137,17 @@ const ReviewDetails = () => {
const moviePoster = review.film?.poster_path ? `https://image.tmdb.org/t/p/w500${review.film.poster_path}` : null;

return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 pt-24">
<div className="min-h-screen bg-gray-50 dark:bg-black pt-24">
{/* Hero */}
<div className="relative h-96 bg-gray-200 dark:bg-gray-800">
<div className="relative h-96 bg-gray-200 dark:bg-gray-900">
{movieBackdrop && (
<div
className="absolute inset-0 bg-cover bg-center"
style={{ backgroundImage: `linear-gradient(to bottom, rgba(0,0,0,0.4), rgba(17,24,39,0.9)), url(${movieBackdrop})` }}
></div>
)}
<div className="absolute bottom-0 left-0 right-0 p-8 max-w-6xl mx-auto flex flex-col lg:flex-row gap-6">
{moviePoster && <img src={moviePoster} alt={review.film.title} className="w-48 h-72 object-cover rounded-xl shadow-2xl border-4 border-white" />}
{moviePoster && <img src={moviePoster} alt={review.film.title} className="w-48 h-72 object-cover rounded-xl shadow-2xl border-4 border-white dark:border-gray-800" />}
<div className="flex-1 text-white">
<h1 className="text-4xl font-bold">{review.film?.title || 'Movie Review'}</h1>
<div className="flex items-center gap-6 mt-2 text-gray-300">
Expand All @@ -164,7 +164,7 @@ const ReviewDetails = () => {

{/* Review Box */}
<div className="max-w-6xl mx-auto p-8">
<div className="bg-white dark:bg-gray-800 rounded-2xl p-6 shadow-xl">
<div className="bg-white dark:bg-gray-900/50 rounded-2xl p-6 shadow-2xl border border-gray-200/20 dark:border-gray-800/50 backdrop-blur-sm">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex items-center gap-4">
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white text-xl font-bold">
Expand All @@ -179,10 +179,10 @@ const ReviewDetails = () => {
{/* Action Buttons for Author Only */}
{isAuthor && (
<div className="flex items-center gap-2 flex-wrap">
<button onClick={handleEdit} className="flex items-center gap-2 px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg">
<button onClick={handleEdit} className="flex items-center gap-2 px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors shadow-lg">
<FaEdit /> Edit
</button>
<button onClick={handleDelete} className="flex items-center gap-2 px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg">
<button onClick={handleDelete} className="flex items-center gap-2 px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg transition-colors shadow-lg">
<FaTrash /> Delete
</button>
</div>
Expand All @@ -192,7 +192,7 @@ const ReviewDetails = () => {
{isLoggedIn() && (
<button
onClick={handleLike}
className={`flex items-center gap-2 px-4 py-2 rounded-lg ${isLiked ? 'bg-red-500 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'}`}
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors ${isLiked ? 'bg-red-500 hover:bg-red-600 text-white' : 'bg-gray-200/50 dark:bg-gray-700/50 hover:bg-gray-300/50 dark:hover:bg-gray-600/50 text-gray-700 dark:text-gray-300 border border-gray-300/20 dark:border-gray-600/30'}`}
>
{isLiked ? <FaHeart /> : <FaRegHeart />} {likesCount}
</button>
Expand All @@ -207,20 +207,20 @@ const ReviewDetails = () => {
value={editContent}
onChange={e => setEditContent(e.target.value)}
rows={6}
className="w-full p-4 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white resize-none"
className="w-full p-4 border border-gray-300/30 dark:border-gray-600/40 rounded-lg bg-white/80 dark:bg-gray-800/60 text-gray-900 dark:text-white resize-none focus:outline-none focus:ring-2 focus:ring-blue-500/50 backdrop-blur-sm"
/>
<div className="flex gap-3">
<button onClick={handleSaveEdit} className="px-6 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg">Save</button>
<button onClick={handleCancelEdit} className="px-6 py-2 bg-gray-500 hover:bg-gray-600 text-white rounded-lg">Cancel</button>
<button onClick={handleSaveEdit} className="px-6 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors shadow-lg">Save</button>
<button onClick={handleCancelEdit} className="px-6 py-2 bg-gray-500 hover:bg-gray-600 text-white rounded-lg transition-colors shadow-lg">Cancel</button>
</div>
</div>
) : (
<p className="text-gray-700 dark:text-gray-300 text-lg whitespace-pre-wrap">{review.content || 'No review content available.'}</p>
<p className="text-gray-700 dark:text-gray-300 text-lg leading-relaxed whitespace-pre-wrap">{review.content || 'No review content available.'}</p>
)}
</div>

{/* Comments */}
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
<div className="mt-8 pt-6 border-t border-gray-200/30 dark:border-gray-700/50">
<h4 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">Comments</h4>
{commentsLoading ? (
<p className="text-gray-600 dark:text-gray-400 text-center py-4">Loading comments...</p>
Expand Down
Loading