Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 85 additions & 12 deletions src/pages/Movies.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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([]);
Expand All @@ -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));

Expand All @@ -69,6 +129,19 @@ export default function Movies() {
}
subtitle="Dive deep into storytelling, performances, and the art of filmmaking."
/>
{isOffline && (
<div style={{
padding: '10px',
backgroundColor: '#333',
color: 'white',
textAlign: 'center',
fontWeight: 'bold',
margin: '10px 0'
}}>
You are in offline mode.
</div>
)}

<h2>Studio Ghibli Films</h2>
<input value={filter} onChange={e => setFilter(e.target.value)} placeholder="Filter by director or year" />
{loading && <Loading />}
Expand Down Expand Up @@ -99,4 +172,4 @@ export default function Movies() {
)}
</div>
);
}
}
43 changes: 43 additions & 0 deletions src/utilities/db.js
Original file line number Diff line number Diff line change
@@ -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<Array>} 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;
}