Merge version_2 into main #3

Merged
bender merged 14 commits from version_2 into main 2026-03-12 06:27:36 +00:00
14 changed files with 2237 additions and 4 deletions

9
.env.example Normal file
View File

@@ -0,0 +1,9 @@
# TMDB API Configuration
NEXT_PUBLIC_TMDB_API_KEY=your_tmdb_api_key_here
# Instructions:
# 1. Go to https://www.themoviedb.org/settings/api
# 2. Sign up for a free account
# 3. Request an API key
# 4. Copy your API key and paste it in the value above
# 5. Rename this file to .env.local

151
src/app/directors/page.tsx Normal file
View File

@@ -0,0 +1,151 @@
"use client"
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarLayoutFloatingOverlay from '@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay';
import FeatureCardTwentySeven from '@/components/sections/feature/FeatureCardTwentySeven';
import FooterLogoEmphasis from '@/components/sections/footer/FooterLogoEmphasis';
import { useState } from 'react';
import { Search } from 'lucide-react';
export default function DirectorsPage() {
const [searchQuery, setSearchQuery] = useState("");
const directors = [
{
id: "d1", title: "Christopher Nolan", descriptions: [
"Renowned for mind-bending narratives", "Master of complex storytelling", "Director of The Dark Knight Trilogy"
],
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/epic-sci-fi-movie-scene-with-futuristic--1773296470285-239f9df4.png?_wi=2", imageAlt: "Christopher Nolan"
},
{
id: "d2", title: "James Cameron", descriptions: [
"Pioneer of visual effects", "Director of Avatar and Titanic", "Visionary filmmaker and innovator"
],
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-adventure-film-with-dyn-1773296470265-3fcf3dca.png?_wi=2", imageAlt: "James Cameron"
},
{
id: "d3", title: "Peter Jackson", descriptions: [
"Creator of The Lord of the Rings", "Master of epic storytelling", "Groundbreaking visual direction"
],
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-fantasy-epic-with-magic-1773296470410-db7e6b1b.png?_wi=2", imageAlt: "Peter Jackson"
},
{
id: "d4", title: "Greta Gerwig", descriptions: [
"Director of Barbie and Lady Bird", "Master of character-driven stories", "Contemporary cinema innovator"
],
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-drama-film-with-emotion-1773296469620-6aeae782.png?_wi=2", imageAlt: "Greta Gerwig"
},
{
id: "d5", title: "Jordan Peele", descriptions: [
"Master of psychological horror", "Social commentary through cinema", "Director of Get Out and Us"
],
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-horror-film-with-dark-o-1773296470517-4a437840.png?_wi=2", imageAlt: "Jordan Peele"
},
{
id: "d6", title: "Denis Villeneuve", descriptions: [
"Director of Dune and Blade Runner 2049", "Master of visual storytelling", "Epic filmmaker and visionary"
],
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-comedy-film-with-vibran-1773296471649-19824f8f.png?_wi=2", imageAlt: "Denis Villeneuve"
}
];
const filteredDirectors = directors.filter(director =>
director.title.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<ThemeProvider
defaultButtonVariant="hover-bubble"
defaultTextAnimation="background-highlight"
borderRadius="rounded"
contentWidth="small"
sizing="largeSizeMediumTitles"
background="noiseDiagonalGradient"
cardStyle="glass-depth"
primaryButtonStyle="radial-glow"
secondaryButtonStyle="layered"
headingFontWeight="medium"
>
<div id="nav" data-section="nav">
<NavbarLayoutFloatingOverlay
brandName="CinemaFlow"
navItems={[
{ name: "Home", id: "/" },
{ name: "Movies", id: "/movies" },
{ name: "Series", id: "/series" },
{ name: "Directors", id: "/directors" },
{ name: "Recommended", id: "/" },
{ name: "Search", id: "/search" }
]}
button={{ text: "Start Watching", href: "/search" }}
/>
</div>
<div className="fixed top-20 left-1/2 transform -translate-x-1/2 w-full max-w-md z-40 px-4">
<div className="flex items-center gap-2 px-4 py-2 rounded-lg bg-white/10 backdrop-blur-md border border-white/20">
<Search className="w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Search directors..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="bg-transparent outline-none text-sm w-full text-white placeholder-gray-400"
/>
</div>
</div>
<div id="directors" data-section="directors" className="pt-20">
<FeatureCardTwentySeven
title="Browse Directors"
description="Explore the works of acclaimed filmmakers and visionary directors."
tag="Directors"
features={filteredDirectors}
gridVariant="three-columns-all-equal-width"
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
/>
</div>
<div id="footer" data-section="footer">
<FooterLogoEmphasis
logoText="CinemaFlow"
columns={[
{
items: [
{ label: "Home", href: "/" },
{ label: "Movies", href: "/movies" },
{ label: "Series", href: "/series" },
{ label: "Directors", href: "/directors" }
]
},
{
items: [
{ label: "About Us", href: "/#about" },
{ label: "Testimonials", href: "/#testimonials" },
{ label: "Support", href: "https://support.example.com" },
{ label: "Blog", href: "https://blog.example.com" }
]
},
{
items: [
{ label: "Privacy Policy", href: "#" },
{ label: "Terms of Service", href: "#" },
{ label: "Cookie Policy", href: "#" },
{ label: "Contact", href: "https://contact.example.com" }
]
},
{
items: [
{ label: "Twitter", href: "https://twitter.com" },
{ label: "Facebook", href: "https://facebook.com" },
{ label: "Instagram", href: "https://instagram.com" },
{ label: "LinkedIn", href: "https://linkedin.com" }
]
}
]}
/>
</div>
</ThemeProvider>
);
}

218
src/app/movie/[id]/page.tsx Normal file
View File

@@ -0,0 +1,218 @@
"use client"
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarLayoutFloatingOverlay from '@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay';
import MediaAbout from '@/components/sections/about/MediaAbout';
import ProductCardTwo from '@/components/sections/product/ProductCardTwo';
import TestimonialCardThirteen from '@/components/sections/testimonial/TestimonialCardThirteen';
import FooterLogoEmphasis from '@/components/sections/footer/FooterLogoEmphasis';
import { Sparkles, Play, Star } from 'lucide-react';
interface MovieDetailProps {
params: {
id: string;
};
}
export default function MovieDetailPage({ params }: MovieDetailProps) {
// Mock movie data - in a real app, this would come from an API
const movie = {
id: params.id,
title: "The Midnight Phantom", poster: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-cover-poster-for-thriller-film-wit-1773296469771-f4246bdd.png", rating: 5,
reviewCount: "2.5M", genre: "Thriller", director: "Christopher Nolan", description: "A thrilling mystery unfolds as a detective investigates a series of supernatural occurrences in a moonlit city. With stunning cinematography and mind-bending plot twists, this film will keep you on the edge of your seat until the very last frame.", cast: [
{ id: "1", name: "Leonardo DiCaprio", role: "Lead Role", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/portrait-photo-of-a-satisfied-user-enjoy-1773296469712-88954449.png" },
{ id: "2", name: "Emma Stone", role: "Supporting Role", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/welcoming-professional-portrait-of-a-con-1773296470246-3476db0d.png" },
{ id: "3", name: "Tom Hardy", role: "Antagonist", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/professional-headshot-of-satisfied-strea-1773296469147-15a5bce5.png" },
{ id: "4", name: "Saoirse Ronan", role: "Supporting Role", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/engaging-professional-portrait-of-happy--1773296469063-a4fb9206.png" },
{ id: "5", name: "Mark Ruffalo", role: "Supporting Role", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/professional-portrait-for-customer-testi-1773296469525-32033b29.png" },
{ id: "6", name: "Timothée Chalamet", role: "Mysterious Character", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/professional-portrait-of-a-happy-streami-1773296469542-6f84fd6a.png" }
],
userReviews: [
{
id: "1", name: "James Chen", handle: "@jamesfilm", testimonial: "Absolutely phenomenal! One of the best thrillers I've seen in years. The cinematography is breathtaking!", rating: 5,
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/professional-portrait-of-a-happy-streami-1773296469542-6f84fd6a.png"
},
{
id: "2", name: "Rachel Williams", handle: "@rachelwatches", testimonial: "An absolute masterpiece. The plot twists had me completely shocked. Highly recommend!", rating: 5,
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/welcoming-professional-portrait-of-a-con-1773296470246-3476db0d.png"
},
{
id: "3", name: "Michael Torres", handle: "@michaelmovies", testimonial: "Stunning visuals combined with an intricate plot. This is cinema at its finest.", rating: 5,
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/professional-headshot-of-satisfied-strea-1773296469147-15a5bce5.png"
},
{
id: "4", name: "Sophie Martin", handle: "@sophiecinema", testimonial: "A thought-provoking thriller that keeps you guessing until the end. Perfect!", rating: 5,
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/engaging-professional-portrait-of-happy--1773296469063-a4fb9206.png"
},
{
id: "5", name: "David Kim", handle: "@davidflix", testimonial: "One of those rare films that stays with you long after it ends. Brilliant storytelling!", rating: 5,
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/professional-portrait-for-customer-testi-1773296469525-32033b29.png"
},
{
id: "6", name: "Alice Johnson", handle: "@alicereviews", testimonial: "Exceptional performances from the entire cast. This deserves all the accolades!", rating: 5,
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/portrait-photo-of-a-satisfied-user-enjoy-1773296469712-88954449.png"
}
],
similarMovies: [
{
id: "1", brand: "CinemaFlow", name: "Crimson Horizon", price: "Stream Now", rating: 5,
reviewCount: "1.8M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-adventure-film-with-dyn-1773296470265-3fcf3dca.png?_wi=1", imageAlt: "Crimson Horizon"
},
{
id: "2", brand: "CinemaFlow", name: "Shadows of Tomorrow", price: "Stream Now", rating: 5,
reviewCount: "1.4M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-horror-film-with-dark-o-1773296470517-4a437840.png?_wi=1", imageAlt: "Shadows of Tomorrow"
},
{
id: "3", brand: "CinemaFlow", name: "The Last Echo", price: "Stream Now", rating: 5,
reviewCount: "2.7M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/award-winning-movie-poster-with-sophisti-1773296468875-acd67598.png", imageAlt: "The Last Echo"
},
{
id: "4", brand: "CinemaFlow", name: "Digital Pulse", price: "Stream Now", rating: 5,
reviewCount: "3.2M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/popular-streaming-movie-poster-with-mode-1773296469731-aada3dd2.png", imageAlt: "Digital Pulse"
},
{
id: "5", brand: "CinemaFlow", name: "Nova Rising", price: "Stream Now", rating: 5,
reviewCount: "2.9M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/blockbuster-movie-poster-with-high-impac-1773296470191-ed07f0e0.png", imageAlt: "Nova Rising"
},
{
id: "6", brand: "CinemaFlow", name: "Infinite Paths", price: "Stream Now", rating: 5,
reviewCount: "2.4M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-latest-release-with-con-1773296469326-313f5c19.png", imageAlt: "Infinite Paths"
}
]
};
return (
<ThemeProvider
defaultButtonVariant="hover-bubble"
defaultTextAnimation="background-highlight"
borderRadius="rounded"
contentWidth="small"
sizing="largeSizeMediumTitles"
background="noiseDiagonalGradient"
cardStyle="glass-depth"
primaryButtonStyle="radial-glow"
secondaryButtonStyle="layered"
headingFontWeight="medium"
>
<div id="nav" data-section="nav">
<NavbarLayoutFloatingOverlay
brandName="CinemaFlow"
navItems={[
{ name: "Home", id: "/" },
{ name: "Movies", id: "/" },
{ name: "Trending", id: "/" },
{ name: "Genres", id: "/" },
{ name: "Browse", id: "/" }
]}
button={{ text: "Start Watching", href: "/" }}
/>
</div>
<div id="poster" data-section="poster">
<MediaAbout
tag="Movie Details"
title={movie.title}
description={movie.description}
subdescription={`Director: ${movie.director} | Genre: ${movie.genre} | Rating: ${movie.rating}⭐ (${movie.reviewCount} reviews)`}
icon={Sparkles}
imageSrc={movie.poster}
imageAlt={`${movie.title} poster`}
mediaAnimation="slide-up"
useInvertedBackground={false}
buttons={[
{ text: "Play Now", href: "https://example.com/play" },
{ text: "Add to Watchlist", href: "https://example.com/watchlist" }
]}
/>
</div>
<div id="cast" data-section="cast">
<ProductCardTwo
title="Cast"
description="Meet the talented actors bringing this story to life"
tag="Starring"
products={movie.cast.map((actor) => ({
id: actor.id,
brand: "Actor", name: actor.name,
price: actor.role,
rating: 5,
reviewCount: "", imageSrc: actor.imageSrc,
imageAlt: actor.name
}))}
gridVariant="three-columns-all-equal-width"
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
carouselMode="buttons"
/>
</div>
<div id="reviews" data-section="reviews">
<TestimonialCardThirteen
title="Audience Reviews"
description="What viewers are saying about this amazing film"
tag="User Ratings"
testimonials={movie.userReviews}
showRating={true}
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
/>
</div>
<div id="similar" data-section="similar">
<ProductCardTwo
title="Similar Movies"
description="You might also enjoy watching these recommendations"
tag="Recommended"
products={movie.similarMovies}
gridVariant="three-columns-all-equal-width"
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
carouselMode="buttons"
/>
</div>
<div id="footer" data-section="footer">
<FooterLogoEmphasis
logoText="CinemaFlow"
columns={[
{
items: [
{ label: "Home", href: "/" },
{ label: "Movies", href: "/" },
{ label: "Trending", href: "/" },
{ label: "Genres", href: "/" }
]
},
{
items: [
{ label: "About Us", href: "/" },
{ label: "Support", href: "https://support.example.com" },
{ label: "Blog", href: "https://blog.example.com" },
{ label: "Contact", href: "https://contact.example.com" }
]
},
{
items: [
{ label: "Privacy Policy", href: "#" },
{ label: "Terms of Service", href: "#" },
{ label: "Cookie Policy", href: "#" },
{ label: "Report Issue", href: "#" }
]
},
{
items: [
{ label: "Twitter", href: "https://twitter.com" },
{ label: "Facebook", href: "https://facebook.com" },
{ label: "Instagram", href: "https://instagram.com" },
{ label: "YouTube", href: "https://youtube.com" }
]
}
]}
/>
</div>
</ThemeProvider>
);
}

140
src/app/movies/page.tsx Normal file
View File

@@ -0,0 +1,140 @@
"use client"
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarLayoutFloatingOverlay from '@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay';
import ProductCardTwo from '@/components/sections/product/ProductCardTwo';
import FooterLogoEmphasis from '@/components/sections/footer/FooterLogoEmphasis';
import { useState } from 'react';
import { Search } from 'lucide-react';
export default function MoviesPage() {
const [searchQuery, setSearchQuery] = useState("");
const movies = [
{
id: "1", brand: "CinemaFlow", name: "The Midnight Phantom", price: "Stream Now", rating: 5,
reviewCount: "2.5M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-cover-poster-for-thriller-film-wit-1773296469771-f4246bdd.png", imageAlt: "The Midnight Phantom thriller poster"
},
{
id: "2", brand: "CinemaFlow", name: "Crimson Horizon", price: "Stream Now", rating: 5,
reviewCount: "1.8M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-adventure-film-with-dyn-1773296470265-3fcf3dca.png?_wi=1", imageAlt: "Crimson Horizon adventure poster"
},
{
id: "3", brand: "CinemaFlow", name: "The Enchanted Realm", price: "Stream Now", rating: 5,
reviewCount: "1.6M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-fantasy-epic-with-magic-1773296470410-db7e6b1b.png?_wi=1", imageAlt: "The Enchanted Realm fantasy poster"
},
{
id: "4", brand: "CinemaFlow", name: "Shadows of Tomorrow", price: "Stream Now", rating: 5,
reviewCount: "1.4M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-horror-film-with-dark-o-1773296470517-4a437840.png?_wi=1", imageAlt: "Shadows of Tomorrow horror poster"
},
{
id: "5", brand: "CinemaFlow", name: "Laughs Unleashed", price: "Stream Now", rating: 5,
reviewCount: "1.2M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-comedy-film-with-vibran-1773296471649-19824f8f.png?_wi=1", imageAlt: "Laughs Unleashed comedy poster"
},
{
id: "6", brand: "CinemaFlow", name: "Hearts in Motion", price: "Stream Now", rating: 5,
reviewCount: "1.9M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-drama-film-with-emotion-1773296469620-6aeae782.png?_wi=1", imageAlt: "Hearts in Motion drama poster"
},
];
const filteredMovies = movies.filter(movie =>
movie.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<ThemeProvider
defaultButtonVariant="hover-bubble"
defaultTextAnimation="background-highlight"
borderRadius="rounded"
contentWidth="small"
sizing="largeSizeMediumTitles"
background="noiseDiagonalGradient"
cardStyle="glass-depth"
primaryButtonStyle="radial-glow"
secondaryButtonStyle="layered"
headingFontWeight="medium"
>
<div id="nav" data-section="nav">
<NavbarLayoutFloatingOverlay
brandName="CinemaFlow"
navItems={[
{ name: "Home", id: "/" },
{ name: "Movies", id: "/movies" },
{ name: "Series", id: "/series" },
{ name: "Directors", id: "/directors" },
{ name: "Recommended", id: "/" },
{ name: "Search", id: "/search" }
]}
button={{ text: "Start Watching", href: "/search" }}
/>
</div>
<div className="fixed top-20 left-1/2 transform -translate-x-1/2 w-full max-w-md z-40 px-4">
<div className="flex items-center gap-2 px-4 py-2 rounded-lg bg-white/10 backdrop-blur-md border border-white/20">
<Search className="w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Search movies..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="bg-transparent outline-none text-sm w-full text-white placeholder-gray-400"
/>
</div>
</div>
<div id="movies" data-section="movies" className="pt-20">
<ProductCardTwo
title="All Movies"
description="Browse our complete collection of movies across all genres."
tag="Movies"
products={filteredMovies}
gridVariant="three-columns-all-equal-width"
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
carouselMode="buttons"
/>
</div>
<div id="footer" data-section="footer">
<FooterLogoEmphasis
logoText="CinemaFlow"
columns={[
{
items: [
{ label: "Home", href: "/" },
{ label: "Movies", href: "/movies" },
{ label: "Series", href: "/series" },
{ label: "Directors", href: "/directors" }
]
},
{
items: [
{ label: "About Us", href: "/#about" },
{ label: "Testimonials", href: "/#testimonials" },
{ label: "Support", href: "https://support.example.com" },
{ label: "Blog", href: "https://blog.example.com" }
]
},
{
items: [
{ label: "Privacy Policy", href: "#" },
{ label: "Terms of Service", href: "#" },
{ label: "Cookie Policy", href: "#" },
{ label: "Contact", href: "https://contact.example.com" }
]
},
{
items: [
{ label: "Twitter", href: "https://twitter.com" },
{ label: "Facebook", href: "https://facebook.com" },
{ label: "Instagram", href: "https://instagram.com" },
{ label: "LinkedIn", href: "https://linkedin.com" }
]
}
]}
/>
</div>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,210 @@
"use client"
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarLayoutFloatingOverlay from '@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay';
import ProductCardTwo from '@/components/sections/product/ProductCardTwo';
import ContactCTA from '@/components/sections/contact/ContactCTA';
import FooterLogoEmphasis from '@/components/sections/footer/FooterLogoEmphasis';
import { Brain } from 'lucide-react';
export default function RecommendedPage() {
return (
<ThemeProvider
defaultButtonVariant="hover-bubble"
defaultTextAnimation="background-highlight"
borderRadius="rounded"
contentWidth="small"
sizing="largeSizeMediumTitles"
background="noiseDiagonalGradient"
cardStyle="glass-depth"
primaryButtonStyle="radial-glow"
secondaryButtonStyle="layered"
headingFontWeight="medium"
>
<div id="nav" data-section="nav">
<NavbarLayoutFloatingOverlay
brandName="CinemaFlow"
navItems={[
{ name: "Home", id: "/" },
{ name: "Movies", id: "/" },
{ name: "Trending", id: "/" },
{ name: "Genres", id: "/" },
{ name: "Browse", id: "/" }
]}
button={{ text: "Start Watching", href: "#contact" }}
/>
</div>
<div id="recommendations" data-section="recommendations" style={{ paddingTop: "80px" }}>
<ProductCardTwo
title="AI-Powered Movie Recommendations"
description="Discover films handpicked for you by our advanced recommendation engine. Based on your viewing history, preferences, and similar user patterns."
tag="Personalized For You"
products={[
{
id: "1", brand: "CinemaFlow AI", name: "The Quantum Paradox", price: "Watch Now", rating: 5,
reviewCount: "1.2M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/epic-sci-fi-movie-scene-with-futuristic--1773296470285-239f9df4.png", imageAlt: "The Quantum Paradox sci-fi film"
},
{
id: "2", brand: "CinemaFlow AI", name: "Echoes of Tomorrow", price: "Watch Now", rating: 5,
reviewCount: "980K", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/cinematic-blockbuster-movie-poster-featu-1773296471359-3ce253bd.png?_wi=1", imageAlt: "Echoes of Tomorrow drama film"
},
{
id: "3", brand: "CinemaFlow AI", name: "Silent Reckoning", price: "Watch Now", rating: 5,
reviewCount: "1.1M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-thriller-film-wit-1773296469771-f4246bdd.png", imageAlt: "Silent Reckoning thriller film"
},
{
id: "4", brand: "CinemaFlow AI", name: "Crimson Dreams", price: "Watch Now", rating: 5,
reviewCount: "875K", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-adventure-film-with-dyn-1773296470265-3fcf3dca.png?_wi=1", imageAlt: "Crimson Dreams adventure film"
},
{
id: "5", brand: "CinemaFlow AI", name: "The Last Horizon", price: "Watch Now", rating: 5,
reviewCount: "1.05M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-fantasy-epic-with-magic-1773296470410-db7e6b1b.png?_wi=1", imageAlt: "The Last Horizon epic fantasy film"
},
{
id: "6", brand: "CinemaFlow AI", name: "Midnight Reflections", price: "Watch Now", rating: 5,
reviewCount: "920K", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-horror-film-with-dark-o-1773296470517-4a437840.png?_wi=1", imageAlt: "Midnight Reflections mystery film"
}
]}
gridVariant="three-columns-all-equal-width"
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
carouselMode="buttons"
/>
</div>
<div id="similar-movies" data-section="similar-movies">
<ProductCardTwo
title="Because You Watched 'Inception'"
description="Similar films based on your recent viewing. Discover more mind-bending sci-fi and psychological thrillers you'll love."
tag="Smart Suggestions"
products={[
{
id: "1", brand: "CinemaFlow AI", name: "Dimensional Breach", price: "Watch Now", rating: 5,
reviewCount: "1.35M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/epic-sci-fi-movie-scene-with-futuristic--1773296470285-239f9df4.png?_wi=1", imageAlt: "Dimensional Breach sci-fi film"
},
{
id: "2", brand: "CinemaFlow AI", name: "Fractured Reality", price: "Watch Now", rating: 5,
reviewCount: "1.28M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/award-winning-movie-poster-with-sophisti-1773296468875-acd67598.png", imageAlt: "Fractured Reality psychological thriller"
},
{
id: "3", brand: "CinemaFlow AI", name: "Neural Convergence", price: "Watch Now", rating: 5,
reviewCount: "1.15M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/popular-streaming-movie-poster-with-mode-1773296469731-aada3dd2.png", imageAlt: "Neural Convergence sci-fi thriller"
},
{
id: "4", brand: "CinemaFlow AI", name: "Temporal Echoes", price: "Watch Now", rating: 5,
reviewCount: "1.42M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/blockbuster-movie-poster-with-high-impac-1773296470191-ed07f0e0.png", imageAlt: "Temporal Echoes sci-fi epic"
},
{
id: "5", brand: "CinemaFlow AI", name: "Consciousness Unbound", price: "Watch Now", rating: 5,
reviewCount: "998K", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-latest-release-with-con-1773296469326-313f5c19.png", imageAlt: "Consciousness Unbound sci-fi drama"
},
{
id: "6", brand: "CinemaFlow AI", name: "Existential Duality", price: "Watch Now", rating: 5,
reviewCount: "1.07M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-viral-hit-with-engaging-1773296471572-95de22d2.png", imageAlt: "Existential Duality philosophical thriller"
}
]}
gridVariant="three-columns-all-equal-width"
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
carouselMode="buttons"
/>
</div>
<div id="trending-recommendations" data-section="trending-recommendations">
<ProductCardTwo
title="Trending This Week Among Your Preferences"
description="What viewers with similar tastes are watching right now. Curated by our AI engine to match your entertainment style."
tag="Community Picks"
products={[
{
id: "1", brand: "CinemaFlow AI", name: "Stellar Uprising", price: "Watch Now", rating: 5,
reviewCount: "2.8M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-cult-classic-with-timel-1773296470317-863fed8f.png", imageAlt: "Stellar Uprising sci-fi adventure"
},
{
id: "2", brand: "CinemaFlow AI", name: "The Infinite Compass", price: "Watch Now", rating: 5,
reviewCount: "2.5M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/cinematic-blockbuster-movie-poster-featu-1773296471359-3ce253bd.png?_wi=2", imageAlt: "The Infinite Compass epic film"
},
{
id: "3", brand: "CinemaFlow AI", name: "Veiled Mysteries", price: "Watch Now", rating: 5,
reviewCount: "2.3M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-thriller-film-wit-1773296469771-f4246bdd.png?_wi=1", imageAlt: "Veiled Mysteries mystery thriller"
},
{
id: "4", brand: "CinemaFlow AI", name: "Neon Prophecy", price: "Watch Now", rating: 5,
reviewCount: "2.6M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/popular-streaming-movie-poster-with-mode-1773296469731-aada3dd2.png?_wi=1", imageAlt: "Neon Prophecy cyberpunk film"
},
{
id: "5", brand: "CinemaFlow AI", name: "Resonance Protocol", price: "Watch Now", rating: 5,
reviewCount: "2.4M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/blockbuster-movie-poster-with-high-impac-1773296470191-ed07f0e0.png?_wi=1", imageAlt: "Resonance Protocol sci-fi action"
},
{
id: "6", brand: "CinemaFlow AI", name: "Beyond the Veil", price: "Watch Now", rating: 5,
reviewCount: "2.2M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/award-winning-movie-poster-with-sophisti-1773296468875-acd67598.png?_wi=1", imageAlt: "Beyond the Veil fantasy drama"
}
]}
gridVariant="three-columns-all-equal-width"
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
carouselMode="buttons"
/>
</div>
<div id="contact" data-section="contact">
<ContactCTA
tag="Save Your Favorites"
title="Create Personalized Watchlists"
description="Save these recommendations to your personal watchlist and let our AI continue learning your preferences. Enjoy unlimited streaming with personalized suggestions every day."
buttons={[
{ text: "Create Watchlist", href: "https://example.com/watchlist" },
{ text: "View More", href: "/" }
]}
background={{ variant: "plain" }}
useInvertedBackground={false}
/>
</div>
<div id="footer" data-section="footer">
<FooterLogoEmphasis
logoText="CinemaFlow"
columns={[
{
items: [
{ label: "Home", href: "/" },
{ label: "Directors", href: "/directors" },
{ label: "Recommended", href: "/recommended" },
{ label: "Browse", href: "/" }
]
},
{
items: [
{ label: "About Us", href: "/" },
{ label: "Featured Films", href: "/" },
{ label: "Support", href: "https://support.example.com" },
{ label: "Blog", href: "https://blog.example.com" }
]
},
{
items: [
{ label: "Privacy Policy", href: "#" },
{ label: "Terms of Service", href: "#" },
{ label: "Cookie Policy", href: "#" },
{ label: "Contact", href: "https://contact.example.com" }
]
},
{
items: [
{ label: "Twitter", href: "https://twitter.com" },
{ label: "Facebook", href: "https://facebook.com" },
{ label: "Instagram", href: "https://instagram.com" },
{ label: "LinkedIn", href: "https://linkedin.com" }
]
}
]}
/>
</div>
</ThemeProvider>
);
}

140
src/app/series/page.tsx Normal file
View File

@@ -0,0 +1,140 @@
"use client"
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarLayoutFloatingOverlay from '@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay';
import ProductCardTwo from '@/components/sections/product/ProductCardTwo';
import FooterLogoEmphasis from '@/components/sections/footer/FooterLogoEmphasis';
import { useState } from 'react';
import { Search } from 'lucide-react';
export default function SeriesPage() {
const [searchQuery, setSearchQuery] = useState("");
const series = [
{
id: "s1", brand: "CinemaFlow", name: "Breaking Bad", price: "Stream Now", rating: 5,
reviewCount: "5M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-cover-poster-for-thriller-film-wit-1773296469771-f4246bdd.png", imageAlt: "Breaking Bad series poster"
},
{
id: "s2", brand: "CinemaFlow", name: "Stranger Things", price: "Stream Now", rating: 5,
reviewCount: "4.5M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/epic-sci-fi-movie-scene-with-futuristic--1773296470285-239f9df4.png?_wi=1", imageAlt: "Stranger Things series poster"
},
{
id: "s3", brand: "CinemaFlow", name: "The Crown", price: "Stream Now", rating: 5,
reviewCount: "3.8M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-drama-film-with-emotion-1773296469620-6aeae782.png?_wi=1", imageAlt: "The Crown series poster"
},
{
id: "s4", brand: "CinemaFlow", name: "Game of Thrones", price: "Stream Now", rating: 5,
reviewCount: "4.2M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-fantasy-epic-with-magic-1773296470410-db7e6b1b.png?_wi=1", imageAlt: "Game of Thrones series poster"
},
{
id: "s5", brand: "CinemaFlow", name: "The Office", price: "Stream Now", rating: 5,
reviewCount: "3.5M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-comedy-film-with-vibran-1773296471649-19824f8f.png?_wi=1", imageAlt: "The Office series poster"
},
{
id: "s6", brand: "CinemaFlow", name: "Dark", price: "Stream Now", rating: 5,
reviewCount: "2.9M", imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/movie-poster-for-horror-film-with-dark-o-1773296470517-4a437840.png?_wi=1", imageAlt: "Dark series poster"
},
];
const filteredSeries = series.filter(show =>
show.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<ThemeProvider
defaultButtonVariant="hover-bubble"
defaultTextAnimation="background-highlight"
borderRadius="rounded"
contentWidth="small"
sizing="largeSizeMediumTitles"
background="noiseDiagonalGradient"
cardStyle="glass-depth"
primaryButtonStyle="radial-glow"
secondaryButtonStyle="layered"
headingFontWeight="medium"
>
<div id="nav" data-section="nav">
<NavbarLayoutFloatingOverlay
brandName="CinemaFlow"
navItems={[
{ name: "Home", id: "/" },
{ name: "Movies", id: "/movies" },
{ name: "Series", id: "/series" },
{ name: "Directors", id: "/directors" },
{ name: "Recommended", id: "/" },
{ name: "Search", id: "/search" }
]}
button={{ text: "Start Watching", href: "/search" }}
/>
</div>
<div className="fixed top-20 left-1/2 transform -translate-x-1/2 w-full max-w-md z-40 px-4">
<div className="flex items-center gap-2 px-4 py-2 rounded-lg bg-white/10 backdrop-blur-md border border-white/20">
<Search className="w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Search TV series..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="bg-transparent outline-none text-sm w-full text-white placeholder-gray-400"
/>
</div>
</div>
<div id="series" data-section="series" className="pt-20">
<ProductCardTwo
title="All TV Series"
description="Browse our complete collection of TV series and shows."
tag="TV Series"
products={filteredSeries}
gridVariant="three-columns-all-equal-width"
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
carouselMode="buttons"
/>
</div>
<div id="footer" data-section="footer">
<FooterLogoEmphasis
logoText="CinemaFlow"
columns={[
{
items: [
{ label: "Home", href: "/" },
{ label: "Movies", href: "/movies" },
{ label: "Series", href: "/series" },
{ label: "Directors", href: "/directors" }
]
},
{
items: [
{ label: "About Us", href: "/#about" },
{ label: "Testimonials", href: "/#testimonials" },
{ label: "Support", href: "https://support.example.com" },
{ label: "Blog", href: "https://blog.example.com" }
]
},
{
items: [
{ label: "Privacy Policy", href: "#" },
{ label: "Terms of Service", href: "#" },
{ label: "Cookie Policy", href: "#" },
{ label: "Contact", href: "https://contact.example.com" }
]
},
{
items: [
{ label: "Twitter", href: "https://twitter.com" },
{ label: "Facebook", href: "https://facebook.com" },
{ label: "Instagram", href: "https://instagram.com" },
{ label: "LinkedIn", href: "https://linkedin.com" }
]
}
]}
/>
</div>
</ThemeProvider>
);
}

View File

@@ -13,12 +13,12 @@
--background: #0a0a0a;
--card: #1a1a1a;
--foreground: #f5f5f5;
--primary-cta: #dfff1c;
--primary-cta: #9acd32;
--primary-cta-text: #0a0a0a;
--secondary-cta: #1a1a1a;
--secondary-cta: #0a0a0a;
--secondary-cta-text: #ffffff;
--accent: #8b9a1b;
--background-accent: #5d6b00;
--accent: #9acd32;
--background-accent: #4a5f1a;
/* text sizing - set by ThemeProvider */
/* --text-2xs: clamp(0.465rem, 0.62vw, 0.62rem);

328
src/app/watch/page.tsx Normal file
View File

@@ -0,0 +1,328 @@
"use client"
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarLayoutFloatingOverlay from '@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay';
import { useState } from 'react';
import { Play, Pause, Volume2, VolumeX, Maximize, Settings, ChevronLeft, ChevronRight } from 'lucide-react';
interface Episode {
id: string;
number: number;
title: string;
duration: string;
thumbnail?: string;
}
interface Season {
id: string;
number: number;
episodes: Episode[];
}
export default function WatchPage() {
const [isPlaying, setIsPlaying] = useState(false);
const [isMuted, setIsMuted] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const [showSubtitles, setShowSubtitles] = useState(true);
const [currentSeasonIndex, setCurrentSeasonIndex] = useState(0);
const [currentEpisodeIndex, setCurrentEpisodeIndex] = useState(0);
const [showSettings, setShowSettings] = useState(false);
const seasons: Season[] = [
{
id: "season-1", number: 1,
episodes: [
{ id: "ep-1", number: 1, title: "The Beginning", duration: "48:32" },
{ id: "ep-2", number: 2, title: "Rising Action", duration: "52:15" },
{ id: "ep-3", number: 3, title: "Turning Point", duration: "45:48" },
{ id: "ep-4", number: 4, title: "Climax", duration: "54:22" },
{ id: "ep-5", number: 5, title: "Resolution", duration: "49:11" },
{ id: "ep-6", number: 6, title: "New Dawn", duration: "50:33" }
]
},
{
id: "season-2", number: 2,
episodes: [
{ id: "ep-7", number: 1, title: "Aftermath", duration: "51:20" },
{ id: "ep-8", number: 2, title: "Consequences", duration: "47:15" },
{ id: "ep-9", number: 3, title: "Redemption", duration: "53:48" },
{ id: "ep-10", number: 4, title: "Betrayal", duration: "49:22" },
{ id: "ep-11", number: 5, title: "Truth Revealed", duration: "52:11" },
{ id: "ep-12", number: 6, title: "Final Stand", duration: "56:33" }
]
},
{
id: "season-3", number: 3,
episodes: [
{ id: "ep-13", number: 1, title: "New Horizons", duration: "50:20" },
{ id: "ep-14", number: 2, title: "Unexpected", duration: "48:15" },
{ id: "ep-15", number: 3, title: "Alliance", duration: "52:48" },
{ id: "ep-16", number: 4, title: "Crisis", duration: "51:22" },
{ id: "ep-17", number: 5, title: "Sacrifice", duration: "54:11" },
{ id: "ep-18", number: 6, title: "Eternity", duration: "58:33" }
]
}
];
const currentSeason = seasons[currentSeasonIndex];
const currentEpisode = currentSeason.episodes[currentEpisodeIndex];
const handleNextEpisode = () => {
if (currentEpisodeIndex < currentSeason.episodes.length - 1) {
setCurrentEpisodeIndex(currentEpisodeIndex + 1);
setIsPlaying(true);
} else if (currentSeasonIndex < seasons.length - 1) {
setCurrentSeasonIndex(currentSeasonIndex + 1);
setCurrentEpisodeIndex(0);
setIsPlaying(true);
}
};
const handlePreviousEpisode = () => {
if (currentEpisodeIndex > 0) {
setCurrentEpisodeIndex(currentEpisodeIndex - 1);
setIsPlaying(true);
} else if (currentSeasonIndex > 0) {
setCurrentSeasonIndex(currentSeasonIndex - 1);
setCurrentEpisodeIndex(seasons[currentSeasonIndex - 1].episodes.length - 1);
setIsPlaying(true);
}
};
return (
<ThemeProvider
defaultButtonVariant="hover-bubble"
defaultTextAnimation="background-highlight"
borderRadius="rounded"
contentWidth="small"
sizing="largeSizeMediumTitles"
background="noiseDiagonalGradient"
cardStyle="glass-depth"
primaryButtonStyle="radial-glow"
secondaryButtonStyle="layered"
headingFontWeight="medium"
>
<div id="nav" data-section="nav">
<NavbarLayoutFloatingOverlay
brandName="CinemaFlow"
navItems={[
{ name: "Home", id: "/" },
{ name: "Browse", id: "/#featured" },
{ name: "Trending", id: "/#trending" },
{ name: "Genres", id: "/#categories" },
{ name: "Account", id: "/#contact" }
]}
button={{ text: "Back to Home", href: "/" }}
/>
</div>
<div className="w-full h-screen bg-background flex flex-col">
{/* Video Player Section */}
<div id="player" className={`w-full ${ isFullscreen ? 'h-screen' : 'h-96 md:h-screen' } bg-black relative overflow-hidden`}>
<div className="w-full h-full flex items-center justify-center bg-gradient-to-b from-black via-black to-black">
{/* Video Placeholder */}
<video
className="w-full h-full object-cover"
poster="https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3Api8Eq5FlSZ0Yx3pymHlMVvJV5/cinematic-blockbuster-movie-poster-featu-1773296471359-3ce253bd.png?_wi=1"
/>
{/* Subtitles Overlay */}
{showSubtitles && (
<div className="absolute bottom-24 left-0 right-0 flex justify-center pointer-events-none">
<div className="bg-black bg-opacity-70 px-4 py-2 rounded text-foreground text-center max-w-2xl">
<p className="text-sm md:text-base">This is a subtitle example for the current scene...</p>
</div>
</div>
)}
{/* Player Controls */}
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black via-black/50 to-transparent pt-12 pb-4 px-4 md:px-6">
{/* Progress Bar */}
<div className="w-full mb-4">
<div className="w-full h-1 bg-foreground/20 rounded-full hover:h-2 transition-all cursor-pointer group">
<div className="h-full bg-primary-cta rounded-full" style={{ width: '35%' }}></div>
</div>
<div className="flex justify-between text-xs text-foreground/70 mt-1">
<span>35:42</span>
<span>1:42:30</span>
</div>
</div>
{/* Control Buttons */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 md:gap-4">
<button
onClick={() => setIsPlaying(!isPlaying)}
className="p-2 hover:bg-foreground/10 rounded-full transition-colors"
aria-label={isPlaying ? "Pause" : "Play"}
>
{isPlaying ? (
<Pause className="w-5 h-5 md:w-6 md:h-6 text-foreground" />
) : (
<Play className="w-5 h-5 md:w-6 md:h-6 text-foreground" />
)}
</button>
<button
onClick={() => handlePreviousEpisode()}
className="p-2 hover:bg-foreground/10 rounded-full transition-colors hidden md:block"
aria-label="Previous episode"
>
<ChevronLeft className="w-5 h-5 md:w-6 md:h-6 text-foreground" />
</button>
<button
onClick={() => handleNextEpisode()}
className="p-2 hover:bg-foreground/10 rounded-full transition-colors hidden md:block"
aria-label="Next episode"
>
<ChevronRight className="w-5 h-5 md:w-6 md:h-6 text-foreground" />
</button>
<div className="hidden md:flex items-center gap-2">
<button
onClick={() => setIsMuted(!isMuted)}
className="p-2 hover:bg-foreground/10 rounded-full transition-colors"
aria-label={isMuted ? "Unmute" : "Mute"}
>
{isMuted ? (
<VolumeX className="w-5 h-5 text-foreground" />
) : (
<Volume2 className="w-5 h-5 text-foreground" />
)}
</button>
<div className="w-20 h-1 bg-foreground/20 rounded-full">
<div className="h-full bg-foreground w-3/4 rounded-full"></div>
</div>
</div>
</div>
<div className="flex items-center gap-2 md:gap-4">
<button
onClick={() => setShowSubtitles(!showSubtitles)}
className="px-3 py-1 text-sm bg-foreground/10 hover:bg-foreground/20 rounded transition-colors text-foreground hidden md:block"
aria-label={showSubtitles ? "Hide subtitles" : "Show subtitles"}
>
{showSubtitles ? 'CC' : 'CC'}
</button>
<button
onClick={() => setShowSettings(!showSettings)}
className="p-2 hover:bg-foreground/10 rounded-full transition-colors relative"
aria-label="Settings"
>
<Settings className="w-5 h-5 md:w-6 md:h-6 text-foreground" />
{showSettings && (
<div className="absolute bottom-full right-0 mb-2 bg-card border border-foreground/20 rounded-lg shadow-lg p-2 w-48 z-50">
<div className="space-y-2 text-sm text-foreground">
<button className="w-full text-left px-3 py-2 hover:bg-foreground/10 rounded">Speed: 1x</button>
<button className="w-full text-left px-3 py-2 hover:bg-foreground/10 rounded">Quality: Auto</button>
<button className="w-full text-left px-3 py-2 hover:bg-foreground/10 rounded">Audio Track: English</button>
</div>
</div>
)}
</button>
<button
onClick={() => setIsFullscreen(!isFullscreen)}
className="p-2 hover:bg-foreground/10 rounded-full transition-colors"
aria-label="Fullscreen"
>
<Maximize className="w-5 h-5 md:w-6 md:h-6 text-foreground" />
</button>
</div>
</div>
</div>
</div>
</div>
{/* Content Section */}
<div className="flex-1 overflow-y-auto bg-background">
<div className="max-w-6xl mx-auto px-4 md:px-6 py-6 md:py-8">
{/* Episode Info */}
<div className="mb-8">
<h1 className="text-2xl md:text-4xl font-bold text-foreground mb-2">
Season {currentSeason.number}, Episode {currentEpisode.number}: {currentEpisode.title}
</h1>
<p className="text-foreground/70">Duration: {currentEpisode.duration}</p>
</div>
{/* Navigation Controls */}
<div className="flex flex-col md:flex-row gap-4 mb-8">
<button
onClick={handlePreviousEpisode}
disabled={currentSeasonIndex === 0 && currentEpisodeIndex === 0}
className="flex items-center gap-2 px-4 py-2 bg-secondary-cta hover:bg-secondary-cta/80 disabled:opacity-50 disabled:cursor-not-allowed text-foreground rounded-lg transition-colors"
>
<ChevronLeft className="w-5 h-5" />
Previous
</button>
<button
onClick={handleNextEpisode}
disabled={currentSeasonIndex === seasons.length - 1 && currentEpisodeIndex === currentSeason.episodes.length - 1}
className="flex items-center gap-2 px-4 py-2 bg-primary-cta hover:bg-primary-cta/80 disabled:opacity-50 disabled:cursor-not-allowed text-background rounded-lg transition-colors"
>
Next
<ChevronRight className="w-5 h-5" />
</button>
</div>
{/* Season Selector */}
<div className="mb-8">
<h2 className="text-xl font-semibold text-foreground mb-4">Select Season</h2>
<div className="flex gap-3 overflow-x-auto pb-2">
{seasons.map((season, index) => (
<button
key={season.id}
onClick={() => {
setCurrentSeasonIndex(index);
setCurrentEpisodeIndex(0);
}}
className={`px-4 py-2 rounded-lg font-medium transition-colors whitespace-nowrap ${
currentSeasonIndex === index
? 'bg-primary-cta text-background'
: 'bg-card text-foreground hover:bg-card/80 border border-foreground/20'
}`}
>
Season {season.number}
</button>
))}
</div>
</div>
{/* Episode List */}
<div>
<h2 className="text-xl font-semibold text-foreground mb-4">Episodes</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{currentSeason.episodes.map((episode, index) => (
<button
key={episode.id}
onClick={() => setCurrentEpisodeIndex(index)}
className={`p-4 rounded-lg text-left transition-all border ${
currentEpisodeIndex === index
? 'bg-primary-cta/20 border-primary-cta'
: 'bg-card border-foreground/10 hover:border-foreground/30'
}`}
>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-12 h-12 bg-foreground/10 rounded flex items-center justify-center text-foreground font-semibold">
{episode.number}
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-foreground truncate">{episode.title}</h3>
<p className="text-sm text-foreground/70">{episode.duration}</p>
</div>
{currentEpisodeIndex === index && (
<Play className="w-5 h-5 text-primary-cta flex-shrink-0" />
)}
</div>
</button>
))}
</div>
</div>
</div>
</div>
</div>
</ThemeProvider>
);
}

341
src/lib/api/tmdb.ts Normal file
View File

@@ -0,0 +1,341 @@
// TMDB API Service with Caching
import {
TMDBMovie,
TMDBTVSeries,
TMDBGenre,
TMDBPerson,
TMDBMovieDetails,
TMDBTVSeriesDetails,
TMDBResponse,
CachedData,
} from '@/lib/types/tmdb';
const TMDB_API_BASE = 'https://api.themoviedb.org/3';
const TMDB_API_KEY = process.env.NEXT_PUBLIC_TMDB_API_KEY || '';
// In-memory cache with TTL (Time To Live)
class TMDBCache {
private cache: Map<string, CachedData<any>> = new Map();
private readonly DEFAULT_TTL = 5 * 60 * 1000; // 5 minutes
set<T>(key: string, data: T, ttl: number = this.DEFAULT_TTL): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl,
});
}
get<T>(key: string): T | null {
const cached = this.cache.get(key);
if (!cached) return null;
const isExpired = Date.now() - cached.timestamp > cached.ttl;
if (isExpired) {
this.cache.delete(key);
return null;
}
return cached.data as T;
}
clear(): void {
this.cache.clear();
}
delete(key: string): void {
this.cache.delete(key);
}
}
const cache = new TMDBCache();
// Fetch wrapper with error handling
async function fetchTMDB<T>(
endpoint: string,
params: Record<string, string | number> = {}
): Promise<T> {
const url = new URL(`${TMDB_API_BASE}${endpoint}`);
url.searchParams.append('api_key', TMDB_API_KEY);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, String(value));
});
const response = await fetch(url.toString(), {
next: { revalidate: 300 }, // ISR: revalidate every 5 minutes
});
if (!response.ok) {
throw new Error(`TMDB API error: ${response.status} ${response.statusText}`);
}
return response.json();
}
// Movie API methods
export const tmdbMoviesAPI = {
// Get popular movies
async getPopular(page: number = 1): Promise<TMDBResponse<TMDBMovie>> {
const cacheKey = `popular_movies_${page}`;
const cached = cache.get<TMDBResponse<TMDBMovie>>(cacheKey);
if (cached) return cached;
const data = await fetchTMDB<TMDBResponse<TMDBMovie>>('/movie/popular', {
page,
});
cache.set(cacheKey, data);
return data;
},
// Get trending movies
async getTrending(
timeWindow: 'day' | 'week' = 'week',
page: number = 1
): Promise<TMDBResponse<TMDBMovie>> {
const cacheKey = `trending_movies_${timeWindow}_${page}`;
const cached = cache.get<TMDBResponse<TMDBMovie>>(cacheKey);
if (cached) return cached;
const data = await fetchTMDB<TMDBResponse<TMDBMovie>>(
`/trending/movie/${timeWindow}`,
{ page }
);
cache.set(cacheKey, data);
return data;
},
// Get top rated movies
async getTopRated(page: number = 1): Promise<TMDBResponse<TMDBMovie>> {
const cacheKey = `top_rated_movies_${page}`;
const cached = cache.get<TMDBResponse<TMDBMovie>>(cacheKey);
if (cached) return cached;
const data = await fetchTMDB<TMDBResponse<TMDBMovie>>('/movie/top_rated', {
page,
});
cache.set(cacheKey, data);
return data;
},
// Get movies by genre
async getByGenre(
genreId: number,
page: number = 1
): Promise<TMDBResponse<TMDBMovie>> {
const cacheKey = `movies_genre_${genreId}_${page}`;
const cached = cache.get<TMDBResponse<TMDBMovie>>(cacheKey);
if (cached) return cached;
const data = await fetchTMDB<TMDBResponse<TMDBMovie>>('/discover/movie', {
with_genres: genreId,
page,
});
cache.set(cacheKey, data);
return data;
},
// Get movie details
async getDetails(movieId: number): Promise<TMDBMovieDetails> {
const cacheKey = `movie_details_${movieId}`;
const cached = cache.get<TMDBMovieDetails>(cacheKey);
if (cached) return cached;
const data = await fetchTMDB<TMDBMovieDetails>(`/movie/${movieId}`, {
append_to_response: 'credits',
});
cache.set(cacheKey, data, 10 * 60 * 1000); // 10 minutes TTL
return data;
},
// Search movies
async search(
query: string,
page: number = 1
): Promise<TMDBResponse<TMDBMovie>> {
const cacheKey = `search_movies_${query}_${page}`;
const cached = cache.get<TMDBResponse<TMDBMovie>>(cacheKey);
if (cached) return cached;
const data = await fetchTMDB<TMDBResponse<TMDBMovie>>('/search/movie', {
query,
page,
});
cache.set(cacheKey, data);
return data;
},
};
// TV Series API methods
export const tmdbTVAPI = {
// Get popular TV series
async getPopular(page: number = 1): Promise<TMDBResponse<TMDBTVSeries>> {
const cacheKey = `popular_tv_${page}`;
const cached = cache.get<TMDBResponse<TMDBTVSeries>>(cacheKey);
if (cached) return cached;
const data = await fetchTMDB<TMDBResponse<TMDBTVSeries>>('/tv/popular', {
page,
});
cache.set(cacheKey, data);
return data;
},
// Get trending TV series
async getTrending(
timeWindow: 'day' | 'week' = 'week',
page: number = 1
): Promise<TMDBResponse<TMDBTVSeries>> {
const cacheKey = `trending_tv_${timeWindow}_${page}`;
const cached = cache.get<TMDBResponse<TMDBTVSeries>>(cacheKey);
if (cached) return cached;
const data = await fetchTMDB<TMDBResponse<TMDBTVSeries>>(
`/trending/tv/${timeWindow}`,
{ page }
);
cache.set(cacheKey, data);
return data;
},
// Get top rated TV series
async getTopRated(page: number = 1): Promise<TMDBResponse<TMDBTVSeries>> {
const cacheKey = `top_rated_tv_${page}`;
const cached = cache.get<TMDBResponse<TMDBTVSeries>>(cacheKey);
if (cached) return cached;
const data = await fetchTMDB<TMDBResponse<TMDBTVSeries>>('/tv/top_rated', {
page,
});
cache.set(cacheKey, data);
return data;
},
// Get TV series by genre
async getByGenre(
genreId: number,
page: number = 1
): Promise<TMDBResponse<TMDBTVSeries>> {
const cacheKey = `tv_genre_${genreId}_${page}`;
const cached = cache.get<TMDBResponse<TMDBTVSeries>>(cacheKey);
if (cached) return cached;
const data = await fetchTMDB<TMDBResponse<TMDBTVSeries>>('/discover/tv', {
with_genres: genreId,
page,
});
cache.set(cacheKey, data);
return data;
},
// Get TV series details
async getDetails(seriesId: number): Promise<TMDBTVSeriesDetails> {
const cacheKey = `tv_details_${seriesId}`;
const cached = cache.get<TMDBTVSeriesDetails>(cacheKey);
if (cached) return cached;
const data = await fetchTMDB<TMDBTVSeriesDetails>(`/tv/${seriesId}`, {
append_to_response: 'credits',
});
cache.set(cacheKey, data, 10 * 60 * 1000); // 10 minutes TTL
return data;
},
// Search TV series
async search(
query: string,
page: number = 1
): Promise<TMDBResponse<TMDBTVSeries>> {
const cacheKey = `search_tv_${query}_${page}`;
const cached = cache.get<TMDBResponse<TMDBTVSeries>>(cacheKey);
if (cached) return cached;
const data = await fetchTMDB<TMDBResponse<TMDBTVSeries>>('/search/tv', {
query,
page,
});
cache.set(cacheKey, data);
return data;
},
};
// People API methods (Actors and Directors)
export const tmdbPeopleAPI = {
// Search people
async search(
query: string,
page: number = 1
): Promise<TMDBResponse<TMDBPerson>> {
const cacheKey = `search_people_${query}_${page}`;
const cached = cache.get<TMDBResponse<TMDBPerson>>(cacheKey);
if (cached) return cached;
const data = await fetchTMDB<TMDBResponse<TMDBPerson>>('/search/person', {
query,
page,
});
cache.set(cacheKey, data);
return data;
},
// Get popular people
async getPopular(page: number = 1): Promise<TMDBResponse<TMDBPerson>> {
const cacheKey = `popular_people_${page}`;
const cached = cache.get<TMDBResponse<TMDBPerson>>(cacheKey);
if (cached) return cached;
const data = await fetchTMDB<TMDBResponse<TMDBPerson>>(
'/person/popular',
{ page }
);
cache.set(cacheKey, data);
return data;
},
// Get person details
async getDetails(personId: number): Promise<TMDBPerson> {
const cacheKey = `person_details_${personId}`;
const cached = cache.get<TMDBPerson>(cacheKey);
if (cached) return cached;
const data = await fetchTMDB<TMDBPerson>(`/person/${personId}`, {
append_to_response: 'combined_credits',
});
cache.set(cacheKey, data, 10 * 60 * 1000); // 10 minutes TTL
return data;
},
};
// Genres API methods
export const tmdbGenresAPI = {
// Get all movie genres
async getMovieGenres(): Promise<{ genres: TMDBGenre[] }> {
const cacheKey = 'movie_genres';
const cached = cache.get<{ genres: TMDBGenre[] }>(cacheKey);
if (cached) return cached;
const data = await fetchTMDB<{ genres: TMDBGenre[] }>(
'/genre/movie/list'
);
cache.set(cacheKey, data, 60 * 60 * 1000); // 1 hour TTL
return data;
},
// Get all TV genres
async getTVGenres(): Promise<{ genres: TMDBGenre[] }> {
const cacheKey = 'tv_genres';
const cached = cache.get<{ genres: TMDBGenre[] }>(cacheKey);
if (cached) return cached;
const data = await fetchTMDB<{ genres: TMDBGenre[] }>('/genre/tv/list');
cache.set(cacheKey, data, 60 * 60 * 1000); // 1 hour TTL
return data;
},
};
// Cache management
export const tmdbCacheAPI = {
clear: () => cache.clear(),
invalidate: (key: string) => cache.delete(key),
};

View File

@@ -0,0 +1,76 @@
// TMDB Movie and TV Genre Constants
export const TMDB_MOVIE_GENRES = {
ACTION: 28,
ADVENTURE: 12,
ANIMATION: 16,
COMEDY: 35,
CRIME: 80,
DOCUMENTARY: 99,
DRAMA: 18,
FAMILY: 10751,
FANTASY: 14,
HISTORY: 36,
HORROR: 27,
MUSIC: 10402,
MYSTERY: 9648,
ROMANCE: 10749,
SCIENCE_FICTION: 878,
TV_MOVIE: 10770,
THRILLER: 53,
WAR: 10752,
WESTERN: 37,
} as const;
export const TMDB_TV_GENRES = {
ACTION_ADVENTURE: 10759,
ANIMATION: 16,
COMEDY: 35,
CRIME: 80,
DOCUMENTARY: 99,
DRAMA: 18,
FAMILY: 10751,
FANTASY: 10765,
HISTORY: 36,
HORROR: 9648,
MYSTERY: 9648,
NEWS: 10763,
REALITY: 10764,
ROMANCE: 10749,
SCIENCE_FICTION: 10765,
SOAP: 10766,
TALK: 10767,
WAR_POLITICS: 10768,
WESTERN: 37,
} as const;
export const GENRE_NAMES: Record<number, string> = {
// Movie genres
28: 'Action',
12: 'Adventure',
16: 'Animation',
35: 'Comedy',
80: 'Crime',
99: 'Documentary',
18: 'Drama',
10751: 'Family',
14: 'Fantasy',
36: 'History',
27: 'Horror',
10402: 'Music',
9648: 'Mystery',
10749: 'Romance',
878: 'Science Fiction',
10770: 'TV Movie',
53: 'Thriller',
10752: 'War',
37: 'Western',
// TV genres
10759: 'Action & Adventure',
10765: 'Science Fiction & Fantasy',
10763: 'News',
10764: 'Reality',
10766: 'Soap',
10767: 'Talk',
10768: 'War & Politics',
};

348
src/lib/hooks/useTMDB.ts Normal file
View File

@@ -0,0 +1,348 @@
'use client';
import { useState, useEffect } from 'react';
import { tmdbMoviesAPI, tmdbTVAPI, tmdbPeopleAPI, tmdbGenresAPI } from '@/lib/api/tmdb';
import {
TMDBMovie,
TMDBTVSeries,
TMDBPerson,
TMDBGenre,
TMDBMovieDetails,
TMDBTVSeriesDetails,
} from '@/lib/types/tmdb';
interface UseAsyncState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
/**
* Hook for fetching popular movies
*/
export function usePopularMovies(page: number = 1) {
const [state, setState] = useState<UseAsyncState<TMDBMovie[]>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
const fetchData = async () => {
try {
setState((prev) => ({ ...prev, loading: true }));
const response = await tmdbMoviesAPI.getPopular(page);
setState({ data: response.results, loading: false, error: null });
} catch (error) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error('Unknown error'),
});
}
};
fetchData();
}, [page]);
return state;
}
/**
* Hook for fetching trending movies
*/
export function useTrendingMovies(
timeWindow: 'day' | 'week' = 'week',
page: number = 1
) {
const [state, setState] = useState<UseAsyncState<TMDBMovie[]>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
const fetchData = async () => {
try {
setState((prev) => ({ ...prev, loading: true }));
const response = await tmdbMoviesAPI.getTrending(timeWindow, page);
setState({ data: response.results, loading: false, error: null });
} catch (error) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error('Unknown error'),
});
}
};
fetchData();
}, [timeWindow, page]);
return state;
}
/**
* Hook for fetching movie by genre
*/
export function useMoviesByGenre(genreId: number, page: number = 1) {
const [state, setState] = useState<UseAsyncState<TMDBMovie[]>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
if (!genreId) {
setState({ data: null, loading: false, error: null });
return;
}
const fetchData = async () => {
try {
setState((prev) => ({ ...prev, loading: true }));
const response = await tmdbMoviesAPI.getByGenre(genreId, page);
setState({ data: response.results, loading: false, error: null });
} catch (error) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error('Unknown error'),
});
}
};
fetchData();
}, [genreId, page]);
return state;
}
/**
* Hook for fetching movie details
*/
export function useMovieDetails(movieId: number) {
const [state, setState] = useState<UseAsyncState<TMDBMovieDetails>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
if (!movieId) {
setState({ data: null, loading: false, error: null });
return;
}
const fetchData = async () => {
try {
setState((prev) => ({ ...prev, loading: true }));
const data = await tmdbMoviesAPI.getDetails(movieId);
setState({ data, loading: false, error: null });
} catch (error) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error('Unknown error'),
});
}
};
fetchData();
}, [movieId]);
return state;
}
/**
* Hook for fetching popular TV series
*/
export function usePopularTV(page: number = 1) {
const [state, setState] = useState<UseAsyncState<TMDBTVSeries[]>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
const fetchData = async () => {
try {
setState((prev) => ({ ...prev, loading: true }));
const response = await tmdbTVAPI.getPopular(page);
setState({ data: response.results, loading: false, error: null });
} catch (error) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error('Unknown error'),
});
}
};
fetchData();
}, [page]);
return state;
}
/**
* Hook for fetching TV series details
*/
export function useTVDetails(seriesId: number) {
const [state, setState] = useState<UseAsyncState<TMDBTVSeriesDetails>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
if (!seriesId) {
setState({ data: null, loading: false, error: null });
return;
}
const fetchData = async () => {
try {
setState((prev) => ({ ...prev, loading: true }));
const data = await tmdbTVAPI.getDetails(seriesId);
setState({ data, loading: false, error: null });
} catch (error) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error('Unknown error'),
});
}
};
fetchData();
}, [seriesId]);
return state;
}
/**
* Hook for searching movies
*/
export function useSearchMovies(query: string, page: number = 1) {
const [state, setState] = useState<UseAsyncState<TMDBMovie[]>>({
data: null,
loading: false,
error: null,
});
useEffect(() => {
if (!query.trim()) {
setState({ data: null, loading: false, error: null });
return;
}
const fetchData = async () => {
try {
setState((prev) => ({ ...prev, loading: true }));
const response = await tmdbMoviesAPI.search(query, page);
setState({ data: response.results, loading: false, error: null });
} catch (error) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error('Unknown error'),
});
}
};
const debounceTimer = setTimeout(fetchData, 300);
return () => clearTimeout(debounceTimer);
}, [query, page]);
return state;
}
/**
* Hook for searching TV series
*/
export function useSearchTV(query: string, page: number = 1) {
const [state, setState] = useState<UseAsyncState<TMDBTVSeries[]>>({
data: null,
loading: false,
error: null,
});
useEffect(() => {
if (!query.trim()) {
setState({ data: null, loading: false, error: null });
return;
}
const fetchData = async () => {
try {
setState((prev) => ({ ...prev, loading: true }));
const response = await tmdbTVAPI.search(query, page);
setState({ data: response.results, loading: false, error: null });
} catch (error) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error('Unknown error'),
});
}
};
const debounceTimer = setTimeout(fetchData, 300);
return () => clearTimeout(debounceTimer);
}, [query, page]);
return state;
}
/**
* Hook for fetching genres
*/
export function useMovieGenres() {
const [state, setState] = useState<UseAsyncState<TMDBGenre[]>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
const fetchData = async () => {
try {
const response = await tmdbGenresAPI.getMovieGenres();
setState({ data: response.genres, loading: false, error: null });
} catch (error) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error('Unknown error'),
});
}
};
fetchData();
}, []);
return state;
}
/**
* Hook for fetching TV genres
*/
export function useTVGenres() {
const [state, setState] = useState<UseAsyncState<TMDBGenre[]>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
const fetchData = async () => {
try {
const response = await tmdbGenresAPI.getTVGenres();
setState({ data: response.genres, loading: false, error: null });
} catch (error) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error('Unknown error'),
});
}
};
fetchData();
}, []);
return state;
}

105
src/lib/types/tmdb.ts Normal file
View File

@@ -0,0 +1,105 @@
// TMDB API Types and Data Models
export interface TMDBMovie {
id: number;
title: string;
original_title?: string;
overview: string;
poster_path: string | null;
backdrop_path: string | null;
release_date: string;
vote_average: number;
vote_count: number;
popularity: number;
genre_ids?: number[];
adult?: boolean;
video?: boolean;
}
export interface TMDBTVSeries {
id: number;
name: string;
original_name?: string;
overview: string;
poster_path: string | null;
backdrop_path: string | null;
first_air_date: string;
last_air_date?: string;
vote_average: number;
vote_count: number;
popularity: number;
genre_ids?: number[];
number_of_seasons?: number;
number_of_episodes?: number;
}
export interface TMDBGenre {
id: number;
name: string;
}
export interface TMDBPerson {
id: number;
name: string;
profile_path: string | null;
popularity: number;
known_for_department?: string;
known_for?: (TMDBMovie | TMDBTVSeries)[];
}
export interface TMDBActor extends TMDBPerson {
character?: string;
order?: number;
}
export interface TMDBDirector extends TMDBPerson {
job?: string;
}
export interface TMDBMovieDetails extends TMDBMovie {
genres: TMDBGenre[];
runtime: number;
budget: number;
revenue: number;
production_companies: Array<{
id: number;
name: string;
logo_path: string | null;
}>;
credits?: {
cast: TMDBActor[];
crew: TMDBDirector[];
};
}
export interface TMDBTVSeriesDetails extends TMDBTVSeries {
genres: TMDBGenre[];
episode_run_time: number[];
networks: Array<{
id: number;
name: string;
logo_path: string | null;
}>;
production_companies: Array<{
id: number;
name: string;
logo_path: string | null;
}>;
credits?: {
cast: TMDBActor[];
crew: TMDBDirector[];
};
}
export interface TMDBResponse<T> {
page: number;
results: T[];
total_pages: number;
total_results: number;
}
export interface CachedData<T> {
data: T;
timestamp: number;
ttl: number;
}

View File

@@ -0,0 +1,110 @@
// TMDB Data formatting utilities
import { TMDBMovie, TMDBTVSeries, TMDBMovieDetails, TMDBTVSeriesDetails } from '@/lib/types/tmdb';
/**
* Format a vote average (0-10) to a 5-star rating
*/
export function formatRating(voteAverage: number): number {
return Math.round((voteAverage / 10) * 5 * 2) / 2; // Round to nearest 0.5
}
/**
* Format vote count to a readable string
*/
export function formatVoteCount(voteCount: number): string {
if (voteCount >= 1_000_000) {
return `${(voteCount / 1_000_000).toFixed(1)}M`;
}
if (voteCount >= 1_000) {
return `${(voteCount / 1_000).toFixed(1)}K`;
}
return voteCount.toString();
}
/**
* Format runtime in minutes to HH:MM format
*/
export function formatRuntime(minutes: number): string {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return `${hours}h ${mins}m`;
}
/**
* Format release date to readable format
*/
export function formatDate(dateString: string): string {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
/**
* Extract year from release date
*/
export function extractYear(dateString: string): string {
return new Date(dateString).getFullYear().toString();
}
/**
* Format movie/TV series overview (truncate and add ellipsis)
*/
export function formatOverview(overview: string, maxLength: number = 160): string {
if (overview.length <= maxLength) {
return overview;
}
return overview.substring(0, maxLength).trim() + '...';
}
/**
* Format cast/crew member name
*/
export function formatName(name: string): string {
return name.trim();
}
/**
* Format character name (handle unknown characters)
*/
export function formatCharacter(character: string | undefined): string {
if (!character || character.trim() === '') {
return 'Unknown';
}
return character.trim();
}
/**
* Convert TMDB movie to a normalized product card format
*/
export function movieToProductCard(movie: TMDBMovie) {
return {
id: movie.id.toString(),
brand: 'CinemaFlow',
name: movie.title,
price: 'Stream Now',
rating: formatRating(movie.vote_average),
reviewCount: formatVoteCount(movie.vote_count),
imageSrc: `https://image.tmdb.org/t/p/w342${movie.poster_path}`,
imageAlt: movie.title,
};
}
/**
* Convert TMDB TV series to a normalized product card format
*/
export function tvToProductCard(series: TMDBTVSeries) {
return {
id: series.id.toString(),
brand: 'CinemaFlow',
name: series.name,
price: 'Stream Now',
rating: formatRating(series.vote_average),
reviewCount: formatVoteCount(series.vote_count),
imageSrc: `https://image.tmdb.org/t/p/w342${series.poster_path}`,
imageAlt: series.name,
};
}

View File

@@ -0,0 +1,57 @@
// TMDB Image URL utilities
const TMDB_IMAGE_BASE = 'https://image.tmdb.org/t/p';
export type ImageSize = 'w92' | 'w154' | 'w185' | 'w342' | 'w500' | 'w780' | 'original';
/**
* Get a full image URL from TMDB poster path
*/
export function getPosterUrl(
posterPath: string | null,
size: ImageSize = 'w342'
): string {
if (!posterPath) {
return '/images/placeholder-poster.jpg';
}
return `${TMDB_IMAGE_BASE}/${size}${posterPath}`;
}
/**
* Get a full image URL from TMDB backdrop path
*/
export function getBackdropUrl(
backdropPath: string | null,
size: ImageSize = 'w780'
): string {
if (!backdropPath) {
return '/images/placeholder-backdrop.jpg';
}
return `${TMDB_IMAGE_BASE}/${size}${backdropPath}`;
}
/**
* Get a full image URL from TMDB profile path
*/
export function getProfileUrl(
profilePath: string | null,
size: ImageSize = 'w185'
): string {
if (!profilePath) {
return '/images/placeholder-profile.jpg';
}
return `${TMDB_IMAGE_BASE}/${size}${profilePath}`;
}
/**
* Get a full image URL from TMDB logo path
*/
export function getLogoUrl(
logoPath: string | null,
size: ImageSize = 'w185'
): string {
if (!logoPath) {
return '';
}
return `${TMDB_IMAGE_BASE}/${size}${logoPath}`;
}