Skip to content

Commit 40ef37e

Browse files
Merge pull request #107 from Rugved-pro/main
IMDB ratings on movies page
2 parents d1f9668 + e0f7513 commit 40ef37e

File tree

3 files changed

+135
-78
lines changed

3 files changed

+135
-78
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
VITE_OMDB_API_KEY=your_api_key_here

src/pages/Header.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@
2323
.dashboard-subtitle {
2424
font-size: 1.1rem;
2525
color: #c8d6e5;
26-
}
26+
}

src/pages/Movies.jsx

Lines changed: 133 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,3 @@
1-
/**
2-
* MOVIES (GHIBLI) DASHBOARD TODOs
3-
* -------------------------------
4-
* Easy:
5-
* - [ ] Add select dropdown for director filtering instead of text filter
6-
* - [ ] Add film poster (map titles to known images or placeholder search)
7-
* - [ ] Show running time, score, producer fields
8-
* - [ ] Expand/collapse description
9-
* Medium:
10-
* - [ ] Client-side pagination / virtualization for performance (future if many APIs)
11-
* - [ ] Favorites / watchlist (localStorage)
12-
* - [ ] Sort (Year asc/desc, Title A-Z, RT Score)
13-
* - [ ] Detail modal with full info & external links
14-
* Advanced:
15-
* - [ ] Pre-fetch details or combine with other Studio Ghibli endpoints (people, locations)
16-
* - [ ] Add fuzzy search (title, director, description)
17-
* - [X] Offline cache using indexedDB (e.g., idb library)
18-
* - [ ] Extract data layer + hook (useGhibliFilms)
19-
*/
201
import { useEffect, useState } from 'react';
212
import Loading from '../components/Loading.jsx';
223
import ErrorMessage from '../components/ErrorMessage.jsx';
@@ -35,15 +16,47 @@ export default function Movies() {
3516
const [selectedFilm, setSelectedFilm] = useState(null);
3617
const [isOffline, setIsOffline] = useState(!navigator.onLine);
3718

19+
const OMDB_API_KEY = import.meta.env.VITE_OMDB_API_KEY;
20+
const isLocal = window.location.hostname === 'localhost';
21+
22+
// 🧠 Fetch and cache IMDb rating (refresh automatically in local)
23+
async function fetchIMDbRating(title) {
24+
if (!OMDB_API_KEY) return { imdbRating: 'N/A' };
25+
26+
const cacheKey = `imdb_${title}`;
27+
const cached = localStorage.getItem(cacheKey);
28+
29+
// If not local and cached data exists, use it
30+
if (!isLocal && cached) {
31+
try {
32+
return JSON.parse(cached);
33+
} catch {
34+
// fall through to fetch
35+
}
36+
}
37+
38+
try {
39+
const res = await fetch(
40+
`https://www.omdbapi.com/?t=${encodeURIComponent(title)}&apikey=${OMDB_API_KEY}`
41+
);
42+
const data = await res.json();
43+
44+
if (data && data.imdbRating) {
45+
const ratingObj = { imdbRating: data.imdbRating };
46+
if (!isLocal) localStorage.setItem(cacheKey, JSON.stringify(ratingObj));
47+
return ratingObj;
48+
}
49+
} catch (err) {
50+
console.error('Failed to fetch IMDb rating for', title, err);
51+
}
52+
53+
return { imdbRating: 'N/A' };
54+
}
55+
56+
// Listen for online/offline and update state
3857
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-
};
58+
const handleOffline = () => setIsOffline(true);
59+
const handleOnline = () => setIsOffline(false);
4760

4861
window.addEventListener('offline', handleOffline);
4962
window.addEventListener('online', handleOnline);
@@ -54,7 +67,10 @@ export default function Movies() {
5467
};
5568
}, []);
5669

70+
// Load films (with offline cache fallback and IMDb enrichment)
5771
useEffect(() => {
72+
let cancelled = false;
73+
5874
async function loadMovies() {
5975
try {
6076
setLoading(true);
@@ -63,51 +79,77 @@ export default function Movies() {
6379
if (isOffline) {
6480
// --- OFFLINE LOGIC ---
6581
const cachedFilms = await getCachedMovies();
66-
if (cachedFilms.length > 0) {
67-
setFilms(cachedFilms);
82+
if (cachedFilms && cachedFilms.length > 0) {
83+
if (!cancelled) setFilms(cachedFilms);
6884
} else {
69-
setError(new Error("You are offline and no cached movies are available."));
85+
throw new Error('You are offline and no cached movies are available.');
7086
}
7187
} else {
7288
// --- ONLINE LOGIC ---
7389
const res = await fetch('https://ghibliapi.vercel.app/films');
7490
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);
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+
}
80110
}
81111
} catch (e) {
82112
console.error('Error during data loading:', e);
83-
113+
114+
// Attempt cache fallback if online fetch failed
84115
if (!isOffline) {
85-
console.log('API failed, attempting to load from cache...');
86116
try {
87117
const cachedFilms = await getCachedMovies();
88-
if (cachedFilms.length > 0) {
89-
setFilms(cachedFilms);
90-
setError(null);
118+
if (cachedFilms && cachedFilms.length > 0) {
119+
if (!cancelled) {
120+
setFilms(cachedFilms);
121+
setError(null);
122+
}
91123
} else {
92-
setError(new Error("API failed and no cached data is available."));
124+
if (!cancelled) setError(e);
93125
}
94126
} catch (cacheError) {
95127
console.error('Cache fallback failed:', cacheError);
96-
setError(e);
128+
if (!cancelled) setError(e);
97129
}
98130
} else {
99-
setError(e);
131+
if (!cancelled) setError(e);
100132
}
101133
} finally {
102-
setLoading(false);
134+
if (!cancelled) setLoading(false);
103135
}
104136
}
105137

106138
loadMovies();
107-
}, [isOffline]);
108139

109-
const filtered = films.filter(f => f.director.toLowerCase().includes(filter.toLowerCase()) || f.release_date.includes(filter));
140+
return () => {
141+
cancelled = true;
142+
};
143+
}, [isOffline]);
144+
145+
// 🧩 Filters
146+
const filtered = films.filter(
147+
(f) =>
148+
f.director?.toLowerCase().includes(filter.toLowerCase()) ||
149+
f.release_date?.includes(filter)
150+
);
110151

152+
// 🎞️ Modal Handlers
111153
const openModal = (film) => {
112154
setSelectedFilm(film);
113155
setIsModalOpen(true);
@@ -121,55 +163,69 @@ export default function Movies() {
121163
return (
122164
<div>
123165
<HeroSection
124-
image={Cinema}
125-
title={
126-
<>
127-
Lights, Camera, <span style={{ color: 'darkred' }}>Binge!</span>
128-
</>
129-
}
130-
subtitle="Dive deep into storytelling, performances, and the art of filmmaking."
131-
/>
166+
image={Cinema}
167+
title={
168+
<>
169+
Lights, Camera, <span style={{ color: 'darkred' }}>Binge!</span>
170+
</>
171+
}
172+
subtitle="Dive deep into storytelling, performances, and the art of filmmaking."
173+
/>
174+
132175
{isOffline && (
133-
<div style={{
134-
padding: '10px',
135-
backgroundColor: '#333',
136-
color: 'white',
137-
textAlign: 'center',
138-
fontWeight: 'bold',
139-
margin: '10px 0'
140-
}}>
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+
>
141186
You are in offline mode.
142187
</div>
143188
)}
144189

145190
<h2>Studio Ghibli Films</h2>
146-
<input value={filter} onChange={e => setFilter(e.target.value)} placeholder="Filter by director or year" />
191+
192+
<input
193+
value={filter}
194+
onChange={(e) => setFilter(e.target.value)}
195+
placeholder="Filter by director or year"
196+
/>
197+
147198
{loading && <Loading />}
148199
<ErrorMessage error={error} />
200+
149201
<div className="grid">
150-
{filtered.map(f => (
151-
<div
202+
{filtered.map((f) => (
203+
<div
152204
key={f.id}
153-
type="button"
205+
role="button"
154206
onClick={() => openModal(f)}
155207
aria-label={`Open details for ${f.title}`}
156-
style={{
157-
display: 'contents',
158-
cursor: 'pointer',
159-
}}
208+
style={{ display: 'contents', cursor: 'pointer' }}
160209
>
161210
<Card title={`${f.title} (${f.release_date})`} japaneseTitle={f.original_title} image={f.image}>
162-
<p><strong>Director:</strong> {f.director}</p>
163-
<p>{f.description.slice(0,120)}...</p>
164-
{/* TODO: Add poster images (need mapping) */}
211+
<p>
212+
<strong>Director:</strong> {f.director}
213+
</p>
214+
215+
<p>
216+
<span style={{ fontStyle: 'italic', fontWeight: 500, color: 'white' }}>
217+
IMDb Rating:
218+
</span>{' '}
219+
{f.imdbRating || 'N/A'}
220+
</p>
221+
222+
<p>{f.description?.slice(0, 120)}...</p>
165223
</Card>
166224
</div>
167-
168225
))}
169226
</div>
170-
{isModalOpen && selectedFilm && (
171-
<Modal open={isModalOpen} onClose={closeModal} film={selectedFilm} />
172-
)}
227+
228+
{isModalOpen && selectedFilm && <Modal open={isModalOpen} onClose={closeModal} film={selectedFilm} />}
173229
</div>
174230
);
175231
}

0 commit comments

Comments
 (0)