1- /**
2- * MOVIES (GHIBLI) DASHBOARD TODOs
3- * -------------------------------
4- * Easy:
5- * - [x] Add IMDb Ratings using OMDb API (with caching)
6- * - [ ] Add select dropdown for director filtering instead of text filter
7- * - [ ] Add film poster (map titles to known images or placeholder search)
8- * - [ ] Show running time, score, producer fields
9- * - [ ] Expand/collapse description
10- * Medium:
11- * - [ ] Client-side pagination / virtualization for performance (future if many APIs)
12- * - [ ] Favorites / watchlist (localStorage)
13- * - [ ] Sort (Year asc/desc, Title A-Z, RT Score)
14- * - [ ] Detail modal with full info & external links
15- * Advanced:
16- * - [ ] Pre-fetch details or combine with other Studio Ghibli endpoints (people, locations)
17- * - [ ] Add fuzzy search (title, director, description)
18- * - [ ] Offline cache using indexedDB (e.g., idb library)
19- * - [ ] Extract data layer + hook (useGhibliFilms)
20- */
21-
221import { useEffect , useState } from 'react' ;
232import Loading from '../components/Loading.jsx' ;
243import ErrorMessage from '../components/ErrorMessage.jsx' ;
254import Card from '../components/Card.jsx' ;
265import HeroSection from '../components/HeroSection' ;
276import Cinema from '../Images/Movie.jpg' ;
287import Modal from '../components/Modal.jsx' ;
8+ import { getCachedMovies , saveMoviesToCache } from '../utilities/db' ;
299
3010export default function Movies ( ) {
3111 const [ films , setFilms ] = useState ( [ ] ) ;
@@ -34,12 +14,15 @@ export default function Movies() {
3414 const [ filter , setFilter ] = useState ( '' ) ;
3515 const [ isModalOpen , setIsModalOpen ] = useState ( false ) ;
3616 const [ selectedFilm , setSelectedFilm ] = useState ( null ) ;
17+ const [ isOffline , setIsOffline ] = useState ( ! navigator . onLine ) ;
3718
3819 const OMDB_API_KEY = import . meta. env . VITE_OMDB_API_KEY ;
3920 const isLocal = window . location . hostname === 'localhost' ;
4021
4122 // 🧠 Fetch and cache IMDb rating (refresh automatically in local)
4223 async function fetchIMDbRating ( title ) {
24+ if ( ! OMDB_API_KEY ) return { imdbRating : 'N/A' } ;
25+
4326 const cacheKey = `imdb_${ title } ` ;
4427 const cached = localStorage . getItem ( cacheKey ) ;
4528
@@ -48,11 +31,10 @@ export default function Movies() {
4831 try {
4932 return JSON . parse ( cached ) ;
5033 } catch {
51- // continue to fetch
34+ // fall through to fetch
5235 }
5336 }
5437
55- // Otherwise, fetch fresh data
5638 try {
5739 const res = await fetch (
5840 `https://www.omdbapi.com/?t=${ encodeURIComponent ( title ) } &apikey=${ OMDB_API_KEY } `
@@ -71,40 +53,100 @@ export default function Movies() {
7153 return { imdbRating : 'N/A' } ;
7254 }
7355
74- // 🎬 Fetch Ghibli films and attach IMDb ratings
75- async function fetchFilms ( ) {
76- try {
77- setLoading ( true ) ;
78- setError ( null ) ;
79-
80- const res = await fetch ( 'https://ghibliapi.vercel.app/films' ) ;
81- if ( ! res . ok ) throw new Error ( 'Failed to fetch films' ) ;
82- const filmsData = await res . json ( ) ;
83-
84- const filmsWithRatings = await Promise . all (
85- filmsData . map ( async ( film ) => {
86- const imdb = await fetchIMDbRating ( film . title ) ;
87- return { ...film , ...imdb } ;
88- } )
89- ) ;
56+ // Listen for online/offline and update state
57+ useEffect ( ( ) => {
58+ const handleOffline = ( ) => setIsOffline ( true ) ;
59+ const handleOnline = ( ) => setIsOffline ( false ) ;
9060
91- setFilms ( filmsWithRatings ) ;
92- } catch ( e ) {
93- setError ( e ) ;
94- } finally {
95- setLoading ( false ) ;
96- }
97- }
61+ window . addEventListener ( 'offline' , handleOffline ) ;
62+ window . addEventListener ( 'online' , handleOnline ) ;
9863
99- useEffect ( ( ) => {
100- fetchFilms ( ) ;
64+ return ( ) => {
65+ window . removeEventListener ( 'offline' , handleOffline ) ;
66+ window . removeEventListener ( 'online' , handleOnline ) ;
67+ } ;
10168 } , [ ] ) ;
10269
70+ // Load films (with offline cache fallback and IMDb enrichment)
71+ useEffect ( ( ) => {
72+ let cancelled = false ;
73+
74+ async function loadMovies ( ) {
75+ try {
76+ setLoading ( true ) ;
77+ setError ( null ) ;
78+
79+ if ( isOffline ) {
80+ // --- OFFLINE LOGIC ---
81+ const cachedFilms = await getCachedMovies ( ) ;
82+ if ( cachedFilms && cachedFilms . length > 0 ) {
83+ if ( ! cancelled ) setFilms ( cachedFilms ) ;
84+ } else {
85+ throw new Error ( 'You are offline and no cached movies are available.' ) ;
86+ }
87+ } else {
88+ // --- ONLINE LOGIC ---
89+ const res = await fetch ( 'https://ghibliapi.vercel.app/films' ) ;
90+ if ( ! res . ok ) throw new Error ( 'Failed to fetch from API' ) ;
91+
92+ const filmsData = await res . json ( ) ;
93+
94+ // Enrich with IMDb ratings in parallel (but limit concurrency if needed)
95+ const filmsWithRatings = await Promise . all (
96+ filmsData . map ( async ( film ) => {
97+ const imdb = await fetchIMDbRating ( film . title ) ;
98+ return { ...film , ...imdb } ;
99+ } )
100+ ) ;
101+
102+ if ( ! cancelled ) setFilms ( filmsWithRatings ) ;
103+
104+ // Save to indexedDB / cache for offline use
105+ try {
106+ await saveMoviesToCache ( filmsWithRatings ) ;
107+ } catch ( cacheErr ) {
108+ console . warn ( 'Failed to save movies to cache:' , cacheErr ) ;
109+ }
110+ }
111+ } catch ( e ) {
112+ console . error ( 'Error during data loading:' , e ) ;
113+
114+ // Attempt cache fallback if online fetch failed
115+ if ( ! isOffline ) {
116+ try {
117+ const cachedFilms = await getCachedMovies ( ) ;
118+ if ( cachedFilms && cachedFilms . length > 0 ) {
119+ if ( ! cancelled ) {
120+ setFilms ( cachedFilms ) ;
121+ setError ( null ) ;
122+ }
123+ } else {
124+ if ( ! cancelled ) setError ( e ) ;
125+ }
126+ } catch ( cacheError ) {
127+ console . error ( 'Cache fallback failed:' , cacheError ) ;
128+ if ( ! cancelled ) setError ( e ) ;
129+ }
130+ } else {
131+ if ( ! cancelled ) setError ( e ) ;
132+ }
133+ } finally {
134+ if ( ! cancelled ) setLoading ( false ) ;
135+ }
136+ }
137+
138+ loadMovies ( ) ;
139+
140+ return ( ) => {
141+ cancelled = true ;
142+ } ;
143+ } , [ isOffline ] ) ;
144+
103145 // 🧩 Filters
104146 const filtered = films . filter (
105147 ( f ) =>
106- f . director . toLowerCase ( ) . includes ( filter . toLowerCase ( ) ) ||
107- f . release_date . includes ( filter )
148+ f . director ? .toLowerCase ( ) . includes ( filter . toLowerCase ( ) ) ||
149+ f . release_date ? .includes ( filter )
108150 ) ;
109151
110152 // 🎞️ Modal Handlers
@@ -129,6 +171,22 @@ export default function Movies() {
129171 }
130172 subtitle = "Dive deep into storytelling, performances, and the art of filmmaking."
131173 />
174+
175+ { isOffline && (
176+ < div
177+ style = { {
178+ padding : '10px' ,
179+ backgroundColor : '#333' ,
180+ color : 'white' ,
181+ textAlign : 'center' ,
182+ fontWeight : 'bold' ,
183+ margin : '10px 0' ,
184+ } }
185+ >
186+ You are in offline mode.
187+ </ div >
188+ ) }
189+
132190 < h2 > Studio Ghibli Films</ h2 >
133191
134192 < input
@@ -144,40 +202,30 @@ export default function Movies() {
144202 { filtered . map ( ( f ) => (
145203 < div
146204 key = { f . id }
147- type = "button"
205+ role = "button"
148206 onClick = { ( ) => openModal ( f ) }
149207 aria-label = { `Open details for ${ f . title } ` }
150208 style = { { display : 'contents' , cursor : 'pointer' } }
151209 >
152- < Card
153- title = { `${ f . title } (${ f . release_date } )` }
154- japaneseTitle = { f . original_title }
155- image = { f . image }
156- >
210+ < Card title = { `${ f . title } (${ f . release_date } )` } japaneseTitle = { f . original_title } image = { f . image } >
157211 < p >
158212 < strong > Director:</ strong > { f . director }
159213 </ p >
214+
160215 < p >
161- < span
162- style = { {
163- fontStyle : 'italic' ,
164- fontWeight : 500 ,
165- color : 'white' ,
166- } }
167- >
216+ < span style = { { fontStyle : 'italic' , fontWeight : 500 , color : 'white' } } >
168217 IMDb Rating:
169218 </ span > { ' ' }
170219 ⭐ { f . imdbRating || 'N/A' }
171220 </ p >
172- < p > { f . description . slice ( 0 , 120 ) } ...</ p >
221+
222+ < p > { f . description ?. slice ( 0 , 120 ) } ...</ p >
173223 </ Card >
174224 </ div >
175225 ) ) }
176226 </ div >
177227
178- { isModalOpen && selectedFilm && (
179- < Modal open = { isModalOpen } onClose = { closeModal } film = { selectedFilm } />
180- ) }
228+ { isModalOpen && selectedFilm && < Modal open = { isModalOpen } onClose = { closeModal } film = { selectedFilm } /> }
181229 </ div >
182230 ) ;
183231}
0 commit comments