1- /**
2- * MOVIES (GHIBLI) DASHBOARD TODOs
3- * -------------------------------
4- * Easy:
5- * - [ ] Add select dropdown for director filtering instead of text filter
6- * - [ ] Add film poster (map titles to known images or placeholder search)
7- * - [ ] Show running time, score, producer fields
8- * - [ ] Expand/collapse description
9- * Medium:
10- * - [ ] Client-side pagination / virtualization for performance (future if many APIs)
11- * - [ ] Favorites / watchlist (localStorage)
12- * - [ ] Sort (Year asc/desc, Title A-Z, RT Score)
13- * - [ ] Detail modal with full info & external links
14- * Advanced:
15- * - [ ] Pre-fetch details or combine with other Studio Ghibli endpoints (people, locations)
16- * - [ ] Add fuzzy search (title, director, description)
17- * - [ ] Offline cache using indexedDB (e.g., idb library)
18- * - [ ] Extract data layer + hook (useGhibliFilms)
19- */
201import { useEffect , useState } from 'react' ;
212import Loading from '../components/Loading.jsx' ;
223import ErrorMessage from '../components/ErrorMessage.jsx' ;
@@ -33,20 +14,55 @@ export default function Movies() {
3314 const [ isModalOpen , setIsModalOpen ] = useState ( false ) ;
3415 const [ selectedFilm , setSelectedFilm ] = useState ( null ) ;
3516
17+ // 🗝️ Replace with your own OMDb API key
18+ const OMDB_API_KEY = import . meta. env . VITE_OMDB_API_KEY ;
3619
37- useEffect ( ( ) => { fetchFilms ( ) ; } , [ ] ) ;
20+
21+ useEffect ( ( ) => {
22+ fetchFilms ( ) ;
23+ } , [ ] ) ;
3824
3925 async function fetchFilms ( ) {
4026 try {
41- setLoading ( true ) ; setError ( null ) ;
27+ setLoading ( true ) ;
28+ setError ( null ) ;
29+
30+ // Step 1: Fetch Ghibli films
4231 const res = await fetch ( 'https://ghibliapi.vercel.app/films' ) ;
4332 if ( ! res . ok ) throw new Error ( 'Failed to fetch' ) ;
44- const json = await res . json ( ) ;
45- setFilms ( json ) ;
46- } catch ( e ) { setError ( e ) ; } finally { setLoading ( false ) ; }
33+ const filmsData = await res . json ( ) ;
34+
35+ // Step 2: Fetch IMDb rating for each film
36+ const filmsWithRatings = await Promise . all (
37+ filmsData . map ( async ( film ) => {
38+ try {
39+ const omdbRes = await fetch (
40+ `https://www.omdbapi.com/?t=${ encodeURIComponent ( film . title ) } &apikey=${ OMDB_API_KEY } `
41+ ) ;
42+ const omdbData = await omdbRes . json ( ) ;
43+ return {
44+ ...film ,
45+ imdbRating : omdbData . imdbRating || 'N/A' , // ⭐ IMDb rating section
46+ } ;
47+ } catch {
48+ return { ...film , imdbRating : 'N/A' } ;
49+ }
50+ } )
51+ ) ;
52+
53+ setFilms ( filmsWithRatings ) ;
54+ } catch ( e ) {
55+ setError ( e ) ;
56+ } finally {
57+ setLoading ( false ) ;
58+ }
4759 }
4860
49- const filtered = films . filter ( f => f . director . toLowerCase ( ) . includes ( filter . toLowerCase ( ) ) || f . release_date . includes ( filter ) ) ;
61+ const filtered = films . filter (
62+ ( f ) =>
63+ f . director . toLowerCase ( ) . includes ( filter . toLowerCase ( ) ) ||
64+ f . release_date . includes ( filter )
65+ ) ;
5066
5167 const openModal = ( film ) => {
5268 setSelectedFilm ( film ) ;
@@ -61,21 +77,25 @@ export default function Movies() {
6177 return (
6278 < div >
6379 < HeroSection
64- image = { Cinema }
65- title = {
66- < >
67- Lights, Camera, < span style = { { color : 'darkred' } } > Binge!</ span >
68- </ >
69- }
70- subtitle = "Dive deep into storytelling, performances, and the art of filmmaking."
71- />
80+ image = { Cinema }
81+ title = {
82+ < >
83+ Lights, Camera, < span style = { { color : 'darkred' } } > Binge!</ span >
84+ </ >
85+ }
86+ subtitle = "Dive deep into storytelling, performances, and the art of filmmaking."
87+ />
7288 < h2 > Studio Ghibli Films</ h2 >
73- < input value = { filter } onChange = { e => setFilter ( e . target . value ) } placeholder = "Filter by director or year" />
89+ < input
90+ value = { filter }
91+ onChange = { ( e ) => setFilter ( e . target . value ) }
92+ placeholder = "Filter by director or year"
93+ />
7494 { loading && < Loading /> }
7595 < ErrorMessage error = { error } />
7696 < div className = "grid" >
77- { filtered . map ( f => (
78- < div
97+ { filtered . map ( ( f ) => (
98+ < div
7999 key = { f . id }
80100 type = "button"
81101 onClick = { ( ) => openModal ( f ) }
@@ -85,18 +105,25 @@ export default function Movies() {
85105 cursor : 'pointer' ,
86106 } }
87107 >
88- < Card title = { `${ f . title } (${ f . release_date } )` } japaneseTitle = { f . original_title } image = { f . image } >
89- < p > < strong > Director:</ strong > { f . director } </ p >
90- < p > { f . description . slice ( 0 , 120 ) } ...</ p >
91- { /* TODO: Add poster images (need mapping) */ }
108+ < Card
109+ title = { `${ f . title } (${ f . release_date } )` }
110+ japaneseTitle = { f . original_title }
111+ image = { f . image }
112+ >
113+ < p >
114+ < strong > Director:</ strong > { f . director }
115+ </ p >
116+ < p >
117+ < strong > IMDb Rating:</ strong > ⭐ { f . imdbRating }
118+ </ p >
119+ < p > { f . description . slice ( 0 , 120 ) } ...</ p >
92120 </ Card >
93121 </ div >
94-
95122 ) ) }
96123 </ div >
97- { isModalOpen && selectedFilm && (
124+ { isModalOpen && selectedFilm && (
98125 < Modal open = { isModalOpen } onClose = { closeModal } film = { selectedFilm } />
99126 ) }
100127 </ div >
101128 ) ;
102- }
129+ }
0 commit comments