diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c648085 --- /dev/null +++ b/.env.example @@ -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 diff --git a/src/app/directors/page.tsx b/src/app/directors/page.tsx new file mode 100644 index 0000000..ac490d4 --- /dev/null +++ b/src/app/directors/page.tsx @@ -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 ( + + + +
+
+ + setSearchQuery(e.target.value)} + className="bg-transparent outline-none text-sm w-full text-white placeholder-gray-400" + /> +
+
+ +
+ +
+ + +
+ ); +} diff --git a/src/app/movie/[id]/page.tsx b/src/app/movie/[id]/page.tsx new file mode 100644 index 0000000..fec2f0f --- /dev/null +++ b/src/app/movie/[id]/page.tsx @@ -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 ( + + + +
+ +
+ +
+ ({ + 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" + /> +
+ +
+ +
+ +
+ +
+ + +
+ ); +} diff --git a/src/app/movies/page.tsx b/src/app/movies/page.tsx new file mode 100644 index 0000000..5d48a37 --- /dev/null +++ b/src/app/movies/page.tsx @@ -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 ( + + + +
+
+ + setSearchQuery(e.target.value)} + className="bg-transparent outline-none text-sm w-full text-white placeholder-gray-400" + /> +
+
+ +
+ +
+ + +
+ ); +} diff --git a/src/app/recommended/page.tsx b/src/app/recommended/page.tsx new file mode 100644 index 0000000..d2eabd4 --- /dev/null +++ b/src/app/recommended/page.tsx @@ -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 ( + + + +
+ +
+ +
+ +
+ + + +
+ +
+ + +
+ ); +} \ No newline at end of file diff --git a/src/app/series/page.tsx b/src/app/series/page.tsx new file mode 100644 index 0000000..24ab0e7 --- /dev/null +++ b/src/app/series/page.tsx @@ -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 ( + + + +
+
+ + setSearchQuery(e.target.value)} + className="bg-transparent outline-none text-sm w-full text-white placeholder-gray-400" + /> +
+
+ +
+ +
+ + +
+ ); +} diff --git a/src/app/styles/variables.css b/src/app/styles/variables.css index 59f452d..9da25e9 100644 --- a/src/app/styles/variables.css +++ b/src/app/styles/variables.css @@ -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); diff --git a/src/app/watch/page.tsx b/src/app/watch/page.tsx new file mode 100644 index 0000000..63fbf90 --- /dev/null +++ b/src/app/watch/page.tsx @@ -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 ( + + + +
+ {/* Video Player Section */} +
+
+ {/* Video Placeholder */} +
+
+
+ + + {/* Content Section */} +
+
+ {/* Episode Info */} +
+

+ Season {currentSeason.number}, Episode {currentEpisode.number}: {currentEpisode.title} +

+

Duration: {currentEpisode.duration}

+
+ + {/* Navigation Controls */} +
+ + +
+ + {/* Season Selector */} +
+

Select Season

+
+ {seasons.map((season, index) => ( + + ))} +
+
+ + {/* Episode List */} +
+

Episodes

+
+ {currentSeason.episodes.map((episode, index) => ( + + ))} +
+
+
+
+ +
+ ); +} \ No newline at end of file diff --git a/src/lib/api/tmdb.ts b/src/lib/api/tmdb.ts new file mode 100644 index 0000000..153236c --- /dev/null +++ b/src/lib/api/tmdb.ts @@ -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> = new Map(); + private readonly DEFAULT_TTL = 5 * 60 * 1000; // 5 minutes + + set(key: string, data: T, ttl: number = this.DEFAULT_TTL): void { + this.cache.set(key, { + data, + timestamp: Date.now(), + ttl, + }); + } + + get(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( + endpoint: string, + params: Record = {} +): Promise { + 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> { + const cacheKey = `popular_movies_${page}`; + const cached = cache.get>(cacheKey); + if (cached) return cached; + + const data = await fetchTMDB>('/movie/popular', { + page, + }); + cache.set(cacheKey, data); + return data; + }, + + // Get trending movies + async getTrending( + timeWindow: 'day' | 'week' = 'week', + page: number = 1 + ): Promise> { + const cacheKey = `trending_movies_${timeWindow}_${page}`; + const cached = cache.get>(cacheKey); + if (cached) return cached; + + const data = await fetchTMDB>( + `/trending/movie/${timeWindow}`, + { page } + ); + cache.set(cacheKey, data); + return data; + }, + + // Get top rated movies + async getTopRated(page: number = 1): Promise> { + const cacheKey = `top_rated_movies_${page}`; + const cached = cache.get>(cacheKey); + if (cached) return cached; + + const data = await fetchTMDB>('/movie/top_rated', { + page, + }); + cache.set(cacheKey, data); + return data; + }, + + // Get movies by genre + async getByGenre( + genreId: number, + page: number = 1 + ): Promise> { + const cacheKey = `movies_genre_${genreId}_${page}`; + const cached = cache.get>(cacheKey); + if (cached) return cached; + + const data = await fetchTMDB>('/discover/movie', { + with_genres: genreId, + page, + }); + cache.set(cacheKey, data); + return data; + }, + + // Get movie details + async getDetails(movieId: number): Promise { + const cacheKey = `movie_details_${movieId}`; + const cached = cache.get(cacheKey); + if (cached) return cached; + + const data = await fetchTMDB(`/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> { + const cacheKey = `search_movies_${query}_${page}`; + const cached = cache.get>(cacheKey); + if (cached) return cached; + + const data = await fetchTMDB>('/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> { + const cacheKey = `popular_tv_${page}`; + const cached = cache.get>(cacheKey); + if (cached) return cached; + + const data = await fetchTMDB>('/tv/popular', { + page, + }); + cache.set(cacheKey, data); + return data; + }, + + // Get trending TV series + async getTrending( + timeWindow: 'day' | 'week' = 'week', + page: number = 1 + ): Promise> { + const cacheKey = `trending_tv_${timeWindow}_${page}`; + const cached = cache.get>(cacheKey); + if (cached) return cached; + + const data = await fetchTMDB>( + `/trending/tv/${timeWindow}`, + { page } + ); + cache.set(cacheKey, data); + return data; + }, + + // Get top rated TV series + async getTopRated(page: number = 1): Promise> { + const cacheKey = `top_rated_tv_${page}`; + const cached = cache.get>(cacheKey); + if (cached) return cached; + + const data = await fetchTMDB>('/tv/top_rated', { + page, + }); + cache.set(cacheKey, data); + return data; + }, + + // Get TV series by genre + async getByGenre( + genreId: number, + page: number = 1 + ): Promise> { + const cacheKey = `tv_genre_${genreId}_${page}`; + const cached = cache.get>(cacheKey); + if (cached) return cached; + + const data = await fetchTMDB>('/discover/tv', { + with_genres: genreId, + page, + }); + cache.set(cacheKey, data); + return data; + }, + + // Get TV series details + async getDetails(seriesId: number): Promise { + const cacheKey = `tv_details_${seriesId}`; + const cached = cache.get(cacheKey); + if (cached) return cached; + + const data = await fetchTMDB(`/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> { + const cacheKey = `search_tv_${query}_${page}`; + const cached = cache.get>(cacheKey); + if (cached) return cached; + + const data = await fetchTMDB>('/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> { + const cacheKey = `search_people_${query}_${page}`; + const cached = cache.get>(cacheKey); + if (cached) return cached; + + const data = await fetchTMDB>('/search/person', { + query, + page, + }); + cache.set(cacheKey, data); + return data; + }, + + // Get popular people + async getPopular(page: number = 1): Promise> { + const cacheKey = `popular_people_${page}`; + const cached = cache.get>(cacheKey); + if (cached) return cached; + + const data = await fetchTMDB>( + '/person/popular', + { page } + ); + cache.set(cacheKey, data); + return data; + }, + + // Get person details + async getDetails(personId: number): Promise { + const cacheKey = `person_details_${personId}`; + const cached = cache.get(cacheKey); + if (cached) return cached; + + const data = await fetchTMDB(`/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), +}; diff --git a/src/lib/constants/tmdb-genres.ts b/src/lib/constants/tmdb-genres.ts new file mode 100644 index 0000000..b5f3801 --- /dev/null +++ b/src/lib/constants/tmdb-genres.ts @@ -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 = { + // 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', +}; diff --git a/src/lib/hooks/useTMDB.ts b/src/lib/hooks/useTMDB.ts new file mode 100644 index 0000000..d72233a --- /dev/null +++ b/src/lib/hooks/useTMDB.ts @@ -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 { + data: T | null; + loading: boolean; + error: Error | null; +} + +/** + * Hook for fetching popular movies + */ +export function usePopularMovies(page: number = 1) { + const [state, setState] = useState>({ + 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>({ + 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>({ + 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>({ + 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>({ + 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>({ + 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>({ + 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>({ + 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>({ + 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>({ + 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; +} diff --git a/src/lib/types/tmdb.ts b/src/lib/types/tmdb.ts new file mode 100644 index 0000000..3e133ef --- /dev/null +++ b/src/lib/types/tmdb.ts @@ -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 { + page: number; + results: T[]; + total_pages: number; + total_results: number; +} + +export interface CachedData { + data: T; + timestamp: number; + ttl: number; +} diff --git a/src/lib/utils/tmdb-formats.ts b/src/lib/utils/tmdb-formats.ts new file mode 100644 index 0000000..ee252f1 --- /dev/null +++ b/src/lib/utils/tmdb-formats.ts @@ -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, + }; +} diff --git a/src/lib/utils/tmdb-image.ts b/src/lib/utils/tmdb-image.ts new file mode 100644 index 0000000..b01318c --- /dev/null +++ b/src/lib/utils/tmdb-image.ts @@ -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}`; +}