22 * MOVIES (GHIBLI) DASHBOARD TODOs
33 * -------------------------------
44 * Easy:
5+ * - [x] Add IMDb Ratings using OMDb API (with caching)
56 * - [ ] Add select dropdown for director filtering instead of text filter
67 * - [ ] Add film poster (map titles to known images or placeholder search)
78 * - [ ] Show running time, score, producer fields
1718 * - [ ] Offline cache using indexedDB (e.g., idb library)
1819 * - [ ] Extract data layer + hook (useGhibliFilms)
1920 */
21+
2022import { useEffect , useState } from 'react' ;
2123import Loading from '../components/Loading.jsx' ;
2224import ErrorMessage from '../components/ErrorMessage.jsx' ;
@@ -33,39 +35,56 @@ export default function Movies() {
3335 const [ isModalOpen , setIsModalOpen ] = useState ( false ) ;
3436 const [ selectedFilm , setSelectedFilm ] = useState ( null ) ;
3537
36- // 🗝️ Replace with your own OMDb API key
3738 const OMDB_API_KEY = import . meta. env . VITE_OMDB_API_KEY ;
39+ const isLocal = window . location . hostname === 'localhost' ;
3840
41+ // 🧠 Fetch and cache IMDb rating (refresh automatically in local)
42+ async function fetchIMDbRating ( title ) {
43+ const cacheKey = `imdb_${ title } ` ;
44+ const cached = localStorage . getItem ( cacheKey ) ;
3945
40- useEffect ( ( ) => {
41- fetchFilms ( ) ;
42- } , [ ] ) ;
46+ // If not local and cached data exists, use it
47+ if ( ! isLocal && cached ) {
48+ try {
49+ return JSON . parse ( cached ) ;
50+ } catch {
51+ // continue to fetch
52+ }
53+ }
54+
55+ // Otherwise, fetch fresh data
56+ try {
57+ const res = await fetch (
58+ `https://www.omdbapi.com/?t=${ encodeURIComponent ( title ) } &apikey=${ OMDB_API_KEY } `
59+ ) ;
60+ const data = await res . json ( ) ;
61+
62+ if ( data && data . imdbRating ) {
63+ const ratingObj = { imdbRating : data . imdbRating } ;
64+ if ( ! isLocal ) localStorage . setItem ( cacheKey , JSON . stringify ( ratingObj ) ) ;
65+ return ratingObj ;
66+ }
67+ } catch ( err ) {
68+ console . error ( 'Failed to fetch IMDb rating for' , title , err ) ;
69+ }
4370
71+ return { imdbRating : 'N/A' } ;
72+ }
73+
74+ // 🎬 Fetch Ghibli films and attach IMDb ratings
4475 async function fetchFilms ( ) {
4576 try {
4677 setLoading ( true ) ;
4778 setError ( null ) ;
4879
49- // Step 1: Fetch Ghibli films
5080 const res = await fetch ( 'https://ghibliapi.vercel.app/films' ) ;
51- if ( ! res . ok ) throw new Error ( 'Failed to fetch' ) ;
81+ if ( ! res . ok ) throw new Error ( 'Failed to fetch films ' ) ;
5282 const filmsData = await res . json ( ) ;
5383
54- // Step 2: Fetch IMDb rating for each film
5584 const filmsWithRatings = await Promise . all (
5685 filmsData . map ( async ( film ) => {
57- try {
58- const omdbRes = await fetch (
59- `https://www.omdbapi.com/?t=${ encodeURIComponent ( film . title ) } &apikey=${ OMDB_API_KEY } `
60- ) ;
61- const omdbData = await omdbRes . json ( ) ;
62- return {
63- ...film ,
64- imdbRating : omdbData . imdbRating || 'N/A' , // ⭐ IMDb rating section
65- } ;
66- } catch {
67- return { ...film , imdbRating : 'N/A' } ;
68- }
86+ const imdb = await fetchIMDbRating ( film . title ) ;
87+ return { ...film , ...imdb } ;
6988 } )
7089 ) ;
7190
@@ -77,12 +96,18 @@ export default function Movies() {
7796 }
7897 }
7998
99+ useEffect ( ( ) => {
100+ fetchFilms ( ) ;
101+ } , [ ] ) ;
102+
103+ // 🧩 Filters
80104 const filtered = films . filter (
81105 ( f ) =>
82106 f . director . toLowerCase ( ) . includes ( filter . toLowerCase ( ) ) ||
83107 f . release_date . includes ( filter )
84108 ) ;
85109
110+ // 🎞️ Modal Handlers
86111 const openModal = ( film ) => {
87112 setSelectedFilm ( film ) ;
88113 setIsModalOpen ( true ) ;
@@ -105,24 +130,24 @@ export default function Movies() {
105130 subtitle = "Dive deep into storytelling, performances, and the art of filmmaking."
106131 />
107132 < h2 > Studio Ghibli Films</ h2 >
133+
108134 < input
109135 value = { filter }
110136 onChange = { ( e ) => setFilter ( e . target . value ) }
111137 placeholder = "Filter by director or year"
112138 />
139+
113140 { loading && < Loading /> }
114141 < ErrorMessage error = { error } />
142+
115143 < div className = "grid" >
116144 { filtered . map ( ( f ) => (
117145 < div
118146 key = { f . id }
119147 type = "button"
120148 onClick = { ( ) => openModal ( f ) }
121149 aria-label = { `Open details for ${ f . title } ` }
122- style = { {
123- display : 'contents' ,
124- cursor : 'pointer' ,
125- } }
150+ style = { { display : 'contents' , cursor : 'pointer' } }
126151 >
127152 < Card
128153 title = { `${ f . title } (${ f . release_date } )` }
@@ -133,13 +158,23 @@ export default function Movies() {
133158 < strong > Director:</ strong > { f . director }
134159 </ p >
135160 < p >
136- < strong > IMDb Rating:</ strong > ⭐ { f . imdbRating }
161+ < span
162+ style = { {
163+ fontStyle : 'italic' ,
164+ fontWeight : 500 ,
165+ color : 'white' ,
166+ } }
167+ >
168+ IMDb Rating:
169+ </ span > { ' ' }
170+ ⭐ { f . imdbRating || 'N/A' }
137171 </ p >
138172 < p > { f . description . slice ( 0 , 120 ) } ...</ p >
139173 </ Card >
140174 </ div >
141175 ) ) }
142176 </ div >
177+
143178 { isModalOpen && selectedFilm && (
144179 < Modal open = { isModalOpen } onClose = { closeModal } film = { selectedFilm } />
145180 ) }
0 commit comments