diff --git a/src/pages/Movies.jsx b/src/pages/Movies.jsx index e9ecd28..0f6380c 100644 --- a/src/pages/Movies.jsx +++ b/src/pages/Movies.jsx @@ -14,7 +14,7 @@ * Advanced: * - [ ] Pre-fetch details or combine with other Studio Ghibli endpoints (people, locations) * - [ ] Add fuzzy search (title, director, description) - * - [ ] Offline cache using indexedDB (e.g., idb library) + * - [X] Offline cache using indexedDB (e.g., idb library) * - [ ] Extract data layer + hook (useGhibliFilms) */ import { useEffect, useState } from 'react'; @@ -24,6 +24,7 @@ import Card from '../components/Card.jsx'; import HeroSection from '../components/HeroSection'; import Cinema from '../Images/Movie.jpg'; import Modal from '../components/Modal.jsx'; +import { getCachedMovies, saveMoviesToCache } from '../utilities/db'; export default function Movies() { const [films, setFilms] = useState([]); @@ -32,19 +33,78 @@ export default function Movies() { const [filter, setFilter] = useState(''); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedFilm, setSelectedFilm] = useState(null); + const [isOffline, setIsOffline] = useState(!navigator.onLine); + useEffect(() => { + const handleOffline = () => { + console.log('App is offline'); + setIsOffline(true); + }; + const handleOnline = () => { + console.log('App is online'); + setIsOffline(false); + }; - useEffect(() => { fetchFilms(); }, []); + window.addEventListener('offline', handleOffline); + window.addEventListener('online', handleOnline); - async function fetchFilms() { - try { - setLoading(true); setError(null); - const res = await fetch('https://ghibliapi.vercel.app/films'); - if (!res.ok) throw new Error('Failed to fetch'); - const json = await res.json(); - setFilms(json); - } catch (e) { setError(e); } finally { setLoading(false); } - } + return () => { + window.removeEventListener('offline', handleOffline); + window.removeEventListener('online', handleOnline); + }; + }, []); + + useEffect(() => { + async function loadMovies() { + try { + setLoading(true); + setError(null); + + if (isOffline) { + // --- OFFLINE LOGIC --- + const cachedFilms = await getCachedMovies(); + if (cachedFilms.length > 0) { + setFilms(cachedFilms); + } else { + setError(new Error("You are offline and no cached movies are available.")); + } + } else { + // --- ONLINE LOGIC --- + const res = await fetch('https://ghibliapi.vercel.app/films'); + if (!res.ok) throw new Error('Failed to fetch from API'); + + const json = await res.json(); + setFilms(json); + + await saveMoviesToCache(json); + } + } catch (e) { + console.error('Error during data loading:', e); + + if (!isOffline) { + console.log('API failed, attempting to load from cache...'); + try { + const cachedFilms = await getCachedMovies(); + if (cachedFilms.length > 0) { + setFilms(cachedFilms); + setError(null); + } else { + setError(new Error("API failed and no cached data is available.")); + } + } catch (cacheError) { + console.error('Cache fallback failed:', cacheError); + setError(e); + } + } else { + setError(e); + } + } finally { + setLoading(false); + } + } + + loadMovies(); + }, [isOffline]); const filtered = films.filter(f => f.director.toLowerCase().includes(filter.toLowerCase()) || f.release_date.includes(filter)); @@ -69,6 +129,19 @@ export default function Movies() { } subtitle="Dive deep into storytelling, performances, and the art of filmmaking." /> + {isOffline && ( +
+ You are in offline mode. +
+ )} +

Studio Ghibli Films

setFilter(e.target.value)} placeholder="Filter by director or year" /> {loading && } @@ -99,4 +172,4 @@ export default function Movies() { )} ); -} \ No newline at end of file +} diff --git a/src/utilities/db.js b/src/utilities/db.js new file mode 100644 index 0000000..b947deb --- /dev/null +++ b/src/utilities/db.js @@ -0,0 +1,43 @@ +import { openDB } from 'idb'; + +const DB_NAME = 'movie-db'; +const STORE_NAME = 'movies'; +const DB_VERSION = 1; + +async function initDB() { + const db = await openDB(DB_NAME, DB_VERSION, { + upgrade(db) { + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME, { keyPath: 'id' }); + } + }, + }); + return db; +} + +/** + * Gets all movies from the IndexedDB cache. + * @returns {Promise} A promise that resolves to an array of movie objects. + */ +export async function getCachedMovies() { + const db = await initDB(); + return db.getAll(STORE_NAME); +} + +/** + * Saves an array of movies to the IndexedDB cache. + * It clears the old data and adds the new data. + * @param {Array} movies - An array of movie objects to cache. + */ +export async function saveMoviesToCache(movies) { + const db = await initDB(); + const tx = db.transaction(STORE_NAME, 'readwrite'); + + await tx.objectStore(STORE_NAME).clear(); + + await Promise.all(movies.map(movie => { + return tx.objectStore(STORE_NAME).add(movie); + })); + + await tx.done; +}