Skip to content

Commit 7136ee4

Browse files
committed
feat(movies): Add offline cache support
1 parent 9a9cb71 commit 7136ee4

File tree

2 files changed

+128
-12
lines changed

2 files changed

+128
-12
lines changed

src/pages/Movies.jsx

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* Advanced:
1515
* - [ ] Pre-fetch details or combine with other Studio Ghibli endpoints (people, locations)
1616
* - [ ] Add fuzzy search (title, director, description)
17-
* - [ ] Offline cache using indexedDB (e.g., idb library)
17+
* - [X] Offline cache using indexedDB (e.g., idb library)
1818
* - [ ] Extract data layer + hook (useGhibliFilms)
1919
*/
2020
import { useEffect, useState } from 'react';
@@ -24,6 +24,7 @@ import Card from '../components/Card.jsx';
2424
import HeroSection from '../components/HeroSection';
2525
import Cinema from '../Images/Movie.jpg';
2626
import Modal from '../components/Modal.jsx';
27+
import { getCachedMovies, saveMoviesToCache } from '../utilities/db';
2728

2829
export default function Movies() {
2930
const [films, setFilms] = useState([]);
@@ -32,19 +33,78 @@ export default function Movies() {
3233
const [filter, setFilter] = useState('');
3334
const [isModalOpen, setIsModalOpen] = useState(false);
3435
const [selectedFilm, setSelectedFilm] = useState(null);
36+
const [isOffline, setIsOffline] = useState(!navigator.onLine);
3537

38+
useEffect(() => {
39+
const handleOffline = () => {
40+
console.log('App is offline');
41+
setIsOffline(true);
42+
};
43+
const handleOnline = () => {
44+
console.log('App is online');
45+
setIsOffline(false);
46+
};
3647

37-
useEffect(() => { fetchFilms(); }, []);
48+
window.addEventListener('offline', handleOffline);
49+
window.addEventListener('online', handleOnline);
3850

39-
async function fetchFilms() {
40-
try {
41-
setLoading(true); setError(null);
42-
const res = await fetch('https://ghibliapi.vercel.app/films');
43-
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); }
47-
}
51+
return () => {
52+
window.removeEventListener('offline', handleOffline);
53+
window.removeEventListener('online', handleOnline);
54+
};
55+
}, []);
56+
57+
useEffect(() => {
58+
async function loadMovies() {
59+
try {
60+
setLoading(true);
61+
setError(null);
62+
63+
if (isOffline) {
64+
// --- OFFLINE LOGIC ---
65+
const cachedFilms = await getCachedMovies();
66+
if (cachedFilms.length > 0) {
67+
setFilms(cachedFilms);
68+
} else {
69+
setError(new Error("You are offline and no cached movies are available."));
70+
}
71+
} else {
72+
// --- ONLINE LOGIC ---
73+
const res = await fetch('https://ghibliapi.vercel.app/films');
74+
if (!res.ok) throw new Error('Failed to fetch from API');
75+
76+
const json = await res.json();
77+
setFilms(json);
78+
79+
await saveMoviesToCache(json);
80+
}
81+
} catch (e) {
82+
console.error('Error during data loading:', e);
83+
84+
if (!isOffline) {
85+
console.log('API failed, attempting to load from cache...');
86+
try {
87+
const cachedFilms = await getCachedMovies();
88+
if (cachedFilms.length > 0) {
89+
setFilms(cachedFilms);
90+
setError(null);
91+
} else {
92+
setError(new Error("API failed and no cached data is available."));
93+
}
94+
} catch (cacheError) {
95+
console.error('Cache fallback failed:', cacheError);
96+
setError(e);
97+
}
98+
} else {
99+
setError(e);
100+
}
101+
} finally {
102+
setLoading(false);
103+
}
104+
}
105+
106+
loadMovies();
107+
}, [isOffline]);
48108

49109
const filtered = films.filter(f => f.director.toLowerCase().includes(filter.toLowerCase()) || f.release_date.includes(filter));
50110

@@ -69,6 +129,19 @@ export default function Movies() {
69129
}
70130
subtitle="Dive deep into storytelling, performances, and the art of filmmaking."
71131
/>
132+
{isOffline && (
133+
<div style={{
134+
padding: '10px',
135+
backgroundColor: '#333',
136+
color: 'white',
137+
textAlign: 'center',
138+
fontWeight: 'bold',
139+
margin: '10px 0'
140+
}}>
141+
You are in offline mode.
142+
</div>
143+
)}
144+
72145
<h2>Studio Ghibli Films</h2>
73146
<input value={filter} onChange={e => setFilter(e.target.value)} placeholder="Filter by director or year" />
74147
{loading && <Loading />}
@@ -99,4 +172,4 @@ export default function Movies() {
99172
)}
100173
</div>
101174
);
102-
}
175+
}

src/utilities/db.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { openDB } from 'idb';
2+
3+
const DB_NAME = 'movie-db';
4+
const STORE_NAME = 'movies';
5+
const DB_VERSION = 1;
6+
7+
async function initDB() {
8+
const db = await openDB(DB_NAME, DB_VERSION, {
9+
upgrade(db) {
10+
if (!db.objectStoreNames.contains(STORE_NAME)) {
11+
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
12+
}
13+
},
14+
});
15+
return db;
16+
}
17+
18+
/**
19+
* Gets all movies from the IndexedDB cache.
20+
* @returns {Promise<Array>} A promise that resolves to an array of movie objects.
21+
*/
22+
export async function getCachedMovies() {
23+
const db = await initDB();
24+
return db.getAll(STORE_NAME);
25+
}
26+
27+
/**
28+
* Saves an array of movies to the IndexedDB cache.
29+
* It clears the old data and adds the new data.
30+
* @param {Array} movies - An array of movie objects to cache.
31+
*/
32+
export async function saveMoviesToCache(movies) {
33+
const db = await initDB();
34+
const tx = db.transaction(STORE_NAME, 'readwrite');
35+
36+
await tx.objectStore(STORE_NAME).clear();
37+
38+
await Promise.all(movies.map(movie => {
39+
return tx.objectStore(STORE_NAME).add(movie);
40+
}));
41+
42+
await tx.done;
43+
}

0 commit comments

Comments
 (0)