← back to Nineoh Guide
feat(mobile): fix scroll (flex ScrollView reveals full cast + all 114 episodes) + direct per-episode TVmaze link on each card
746a4e4f01a0f1a780c2994b39dab366211d1377 · 2026-07-27 12:06:11 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit 746a4e4f01a0f1a780c2994b39dab366211d1377
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 27 12:06:11 2026 -0700
feat(mobile): fix scroll (flex ScrollView reveals full cast + all 114 episodes) + direct per-episode TVmaze link on each card
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
apps/mobile/App.tsx | 164 ++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 154 insertions(+), 10 deletions(-)
diff --git a/apps/mobile/App.tsx b/apps/mobile/App.tsx
index 7c56c02..2f82f50 100644
--- a/apps/mobile/App.tsx
+++ b/apps/mobile/App.tsx
@@ -32,14 +32,96 @@ const API_BASE =
const WATCHLIST_KEY = "nineoh.watchlist.v1";
const WATCHED_KEY = "nineoh.watchedThrough.v1";
-type TabKey = "episodes" | "cast" | "news" | "saved";
+type TabKey = "episodes" | "cast" | "media" | "news" | "saved";
const TABS: { key: TabKey; icon: string; label: string }[] = [
{ key: "episodes", icon: "📺", label: "Episodes" },
{ key: "cast", icon: "🎭", label: "Cast" },
+ { key: "media", icon: "🎬", label: "Watch" },
{ key: "news", icon: "📰", label: "News" },
{ key: "saved", icon: "★", label: "Saved" },
];
+// Curated external media — YouTube, podcasts, streaming & reference.
+// Landing/search URLs (not fabricated deep links) so nothing 404s.
+type MediaLink = { label: string; sub: string; url: string };
+const MEDIA: { section: string; items: MediaLink[] }[] = [
+ {
+ section: "▶ YouTube",
+ items: [
+ {
+ label: "Official clips & iconic scenes",
+ sub: "YouTube",
+ url: "https://www.youtube.com/results?search_query=beverly+hills+90210+official+clip",
+ },
+ {
+ label: "Cast reunions & interviews",
+ sub: "YouTube",
+ url: "https://www.youtube.com/results?search_query=beverly+hills+90210+cast+reunion+interview",
+ },
+ {
+ label: "Opening theme & title sequences",
+ sub: "YouTube",
+ url: "https://www.youtube.com/results?search_query=beverly+hills+90210+opening+theme",
+ },
+ {
+ label: "Retrospectives & where-are-they-now",
+ sub: "YouTube",
+ url: "https://www.youtube.com/results?search_query=beverly+hills+90210+retrospective+where+are+they+now",
+ },
+ ],
+ },
+ {
+ section: "🎧 Podcasts",
+ items: [
+ {
+ label: "9021OMG — Jennie Garth & Tori Spelling",
+ sub: "The official rewatch podcast",
+ url: "https://podcasts.apple.com/us/search?term=9021OMG",
+ },
+ {
+ label: "All 90210 rewatch podcasts",
+ sub: "Apple Podcasts",
+ url: "https://podcasts.apple.com/us/search?term=beverly%20hills%2090210",
+ },
+ {
+ label: "Listen on Spotify",
+ sub: "Spotify",
+ url: "https://open.spotify.com/search/9021OMG",
+ },
+ ],
+ },
+ {
+ section: "📺 Where to watch",
+ items: [
+ {
+ label: "Streaming availability",
+ sub: "JustWatch",
+ url: "https://www.justwatch.com/us/search?q=Beverly%20Hills%2090210",
+ },
+ {
+ label: "Watch on Hulu",
+ sub: "hulu.com",
+ url: "https://www.hulu.com/search?q=beverly%20hills%2090210",
+ },
+ ],
+ },
+ {
+ section: "🔎 More",
+ items: [
+ {
+ label: "Series overview & history",
+ sub: "Wikipedia",
+ url: "https://en.wikipedia.org/wiki/Beverly_Hills,_90210",
+ },
+ {
+ label: "Full cast & episode guide",
+ sub: "IMDb",
+ url: "https://www.imdb.com/find/?q=Beverly%20Hills%2090210",
+ },
+ ],
+ },
+];
+
type CastMember = {
id: string;
name: string;
@@ -66,7 +148,7 @@ function isAhead(
}
export default function App() {
- const [tab, setTab] = useState<TabKey>("episodes");
+ const [tab, setTab] = useState<TabKey>("media");
const [episodes, setEpisodes] = useState<Episode[]>([]);
const [cast, setCast] = useState<CastMember[]>([]);
@@ -207,12 +289,26 @@ export default function App() {
) : (
<Text style={styles.pending}>Recap coming soon</Text>
)}
- <Pressable
- onPress={() => markWatched(ep.seasonNumber, ep.episodeNumber)}
- hitSlop={6}
- >
- <Text style={styles.markWatched}>✓ watched to here</Text>
- </Pressable>
+ <View style={styles.epActions}>
+ <Pressable
+ onPress={() => markWatched(ep.seasonNumber, ep.episodeNumber)}
+ hitSlop={6}
+ >
+ <Text style={styles.markWatched}>✓ watched to here</Text>
+ </Pressable>
+ {ep.externalIds?.tvmaze ? (
+ <Pressable
+ onPress={() =>
+ Linking.openURL(
+ `https://www.tvmaze.com/episodes/${ep.externalIds.tvmaze}`
+ )
+ }
+ hitSlop={6}
+ >
+ <Text style={styles.epLink}>▶ View this episode ↗</Text>
+ </Pressable>
+ ) : null}
+ </View>
</BlurView>
);
};
@@ -267,7 +363,11 @@ export default function App() {
</View>
) : null}
- <ScrollView contentContainerStyle={styles.body}>
+ <ScrollView
+ style={styles.scroll}
+ contentContainerStyle={styles.body}
+ showsVerticalScrollIndicator={false}
+ >
{tab === "episodes" ? (
<>
<Text style={styles.disclaimer}>{APP.disclaimerShort}</Text>
@@ -382,6 +482,34 @@ export default function App() {
)}
</>
) : null}
+
+ {tab === "media" ? (
+ <>
+ <Text style={styles.sectionTitle}>🎬 Watch, listen & explore</Text>
+ <Text style={styles.disclaimer}>
+ Curated links out to YouTube, podcast apps and streaming guides. An
+ unofficial fan guide — not affiliated with or endorsed by the show.
+ </Text>
+ {MEDIA.map((g) => (
+ <View key={g.section}>
+ <Text style={styles.mediaGroup}>{g.section}</Text>
+ {g.items.map((m) => (
+ <Pressable
+ key={m.url}
+ onPress={() => Linking.openURL(m.url)}
+ style={styles.newsCard}
+ >
+ <Text style={styles.newsHeadline}>{m.label}</Text>
+ <Text style={styles.newsMeta}>
+ {m.sub}
+ {" ↗"}
+ </Text>
+ </Pressable>
+ ))}
+ </View>
+ ))}
+ </>
+ ) : null}
</ScrollView>
{/* Bottom tab bar */}
@@ -437,6 +565,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 12,
paddingVertical: 5,
},
+ scroll: { flex: 1 },
body: { padding: 20, paddingTop: 12, paddingBottom: 28 },
sectionTitle: {
fontSize: 17,
@@ -445,6 +574,13 @@ const styles = StyleSheet.create({
marginBottom: 12,
},
disclaimer: { fontSize: 12, color: "#666", marginBottom: 16 },
+ mediaGroup: {
+ fontSize: 14,
+ fontWeight: "700",
+ color: "#7b3f6e",
+ marginTop: 16,
+ marginBottom: 8,
+ },
empty: { fontSize: 14, color: "#8a1c1c", lineHeight: 20 },
card: {
backgroundColor: "rgba(255,255,255,0.5)",
@@ -471,7 +607,15 @@ const styles = StyleSheet.create({
borderRadius: 4,
padding: 6,
},
- markWatched: { fontSize: 11, color: "#888", marginTop: 6 },
+ epActions: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ marginTop: 8,
+ gap: 12,
+ },
+ markWatched: { fontSize: 11, color: "#888" },
+ epLink: { fontSize: 12, color: "#7b3f6e", fontWeight: "700" },
chipGuard: { backgroundColor: "#f3e9e9", borderColor: "#d8b4b4" },
chipGuardText: { fontSize: 13, color: "#8a1c1c" },
// cast
← 96bb1e7 docs(mobile): EAS ad-hoc build recipe — press 'a' to select
·
back to Nineoh Guide
·
feat(mobile): add Watch tab — scrollable YouTube / podcasts 70d1038 →