Merge version_2 into main #3
9
.env.example
Normal file
9
.env.example
Normal 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
151
src/app/directors/page.tsx
Normal 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
218
src/app/movie/[id]/page.tsx
Normal 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
140
src/app/movies/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
210
src/app/recommended/page.tsx
Normal file
210
src/app/recommended/page.tsx
Normal 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
140
src/app/series/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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
328
src/app/watch/page.tsx
Normal 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
341
src/lib/api/tmdb.ts
Normal 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),
|
||||
};
|
||||
76
src/lib/constants/tmdb-genres.ts
Normal file
76
src/lib/constants/tmdb-genres.ts
Normal 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
348
src/lib/hooks/useTMDB.ts
Normal 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
105
src/lib/types/tmdb.ts
Normal 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;
|
||||
}
|
||||
110
src/lib/utils/tmdb-formats.ts
Normal file
110
src/lib/utils/tmdb-formats.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
57
src/lib/utils/tmdb-image.ts
Normal file
57
src/lib/utils/tmdb-image.ts
Normal 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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user