← back to Beverlyhillsvideos App
App Store Connect listing + submission-readiness package (store/LISTING.md)
e747893bb6f27d31b47bd5f93fd97accc3dd5ce6 · 2026-08-06 08:17:42 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A .gitignoreA app.jsonA app/(tabs)/_layout.tsxA app/(tabs)/guides.tsxA app/(tabs)/index.tsxA app/(tabs)/map.tsxA app/(tabs)/videos.tsxA app/_layout.tsxA assets/films.jsonA assets/icon.pngA assets/map-locations.jsonA assets/splash.pngA babel.config.jsA components/BHVWebView.tsxA components/FavoritesModal.tsxA components/FilmModal.tsxA constants/Colors.tsA eas.jsonA expo-env.d.tsA hooks/useFavorites.tsA hooks/useNetworkStatus.tsA metro.config.jsA package.jsonA store/LISTING.mdA tsconfig.jsonA types/index.ts
Diff
commit e747893bb6f27d31b47bd5f93fd97accc3dd5ce6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Aug 6 08:17:42 2026 -0700
App Store Connect listing + submission-readiness package (store/LISTING.md)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
.gitignore | 9 +
app.json | 72 ++++
app/(tabs)/_layout.tsx | 81 +++++
app/(tabs)/guides.tsx | 23 ++
app/(tabs)/index.tsx | 35 ++
app/(tabs)/map.tsx | 492 ++++++++++++++++++++++++++
app/(tabs)/videos.tsx | 23 ++
app/_layout.tsx | 23 ++
assets/films.json | 310 +++++++++++++++++
assets/icon.png | Bin 0 -> 20907 bytes
assets/map-locations.json | 777 ++++++++++++++++++++++++++++++++++++++++++
assets/splash.png | Bin 0 -> 45614 bytes
babel.config.js | 6 +
components/BHVWebView.tsx | 192 +++++++++++
components/FavoritesModal.tsx | 206 +++++++++++
components/FilmModal.tsx | 89 +++++
constants/Colors.ts | 21 ++
eas.json | 36 ++
expo-env.d.ts | 3 +
hooks/useFavorites.ts | 43 +++
hooks/useNetworkStatus.ts | 15 +
metro.config.js | 5 +
package.json | 38 +++
store/LISTING.md | 264 ++++++++++++++
tsconfig.json | 16 +
types/index.ts | 21 ++
26 files changed, 2800 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0294910
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
+.expo/
diff --git a/app.json b/app.json
new file mode 100644
index 0000000..3adc7e7
--- /dev/null
+++ b/app.json
@@ -0,0 +1,72 @@
+{
+ "expo": {
+ "name": "Beverly Hills Videos",
+ "slug": "beverlyhillsvideos",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "userInterfaceStyle": "light",
+ "newArchEnabled": true,
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#151310"
+ },
+ "assetBundlePatterns": [
+ "**/*"
+ ],
+ "ios": {
+ "supportsTablet": true,
+ "bundleIdentifier": "com.abrams.beverlyhillsvideos",
+ "buildNumber": "1",
+ "infoPlist": {
+ "NSLocationWhenInUseUsageDescription": "Show nearby Beverly Hills restaurants on the map.",
+ "NSAppTransportSecurity": {
+ "NSAllowsArbitraryLoads": true
+ }
+ }
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/icon.png",
+ "backgroundColor": "#151310"
+ },
+ "package": "com.abrams.beverlyhillsvideos",
+ "versionCode": 1,
+ "permissions": [
+ "android.permission.ACCESS_FINE_LOCATION",
+ "android.permission.ACCESS_COARSE_LOCATION",
+ "android.permission.INTERNET"
+ ]
+ },
+ "web": {
+ "favicon": "./assets/icon.png",
+ "bundler": "metro"
+ },
+ "plugins": [
+ "expo-router",
+ "expo-font",
+ [
+ "expo-splash-screen",
+ {
+ "image": "./assets/splash.png",
+ "imageWidth": 200,
+ "resizeMode": "contain",
+ "backgroundColor": "#151310"
+ }
+ ],
+ [
+ "react-native-maps",
+ {
+ "googleMapsApiKey": ""
+ }
+ ]
+ ],
+ "scheme": "beverlyhillsvideos",
+ "extra": {
+ "eas": {
+ "projectId": "TODO_REPLACE_WITH_EAS_PROJECT_ID"
+ }
+ }
+ }
+}
diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx
new file mode 100644
index 0000000..0c53edc
--- /dev/null
+++ b/app/(tabs)/_layout.tsx
@@ -0,0 +1,81 @@
+import { Tabs } from 'expo-router';
+import { Platform, Text } from 'react-native';
+import { Colors } from '@/constants/Colors';
+
+function TabIcon({ label, focused }: { label: string; focused: boolean }) {
+ return (
+ <Text
+ style={{
+ fontSize: 22,
+ color: focused ? Colors.tabActive : Colors.tabInactive,
+ }}
+ >
+ {label}
+ </Text>
+ );
+}
+
+export default function TabsLayout() {
+ return (
+ <Tabs
+ screenOptions={{
+ headerShown: false,
+ tabBarActiveTintColor: Colors.tabActive,
+ tabBarInactiveTintColor: Colors.tabInactive,
+ tabBarStyle: {
+ backgroundColor: Colors.cream,
+ borderTopColor: Colors.border,
+ borderTopWidth: 0.5,
+ elevation: 0,
+ shadowOpacity: 0,
+ height: Platform.OS === 'ios' ? 84 : 64,
+ paddingBottom: Platform.OS === 'ios' ? 28 : 10,
+ paddingTop: 8,
+ },
+ tabBarLabelStyle: {
+ fontSize: 11,
+ fontWeight: '500',
+ letterSpacing: 0.3,
+ marginTop: 2,
+ },
+ }}
+ >
+ <Tabs.Screen
+ name="index"
+ options={{
+ title: 'Home',
+ tabBarIcon: ({ focused }) => (
+ <TabIcon label={focused ? 'home_filled' : 'home'} focused={focused} />
+ ),
+ }}
+ />
+ <Tabs.Screen
+ name="videos"
+ options={{
+ title: 'Videos',
+ tabBarIcon: ({ focused }) => (
+ <TabIcon label={focused ? 'play_circle_filled' : 'play_circle'} focused={focused} />
+ ),
+ }}
+ />
+ <Tabs.Screen
+ name="map"
+ options={{
+ title: 'Map',
+ tabBarIcon: ({ focused }) => (
+ <TabIcon label={focused ? 'map_filled' : 'map'} focused={focused} />
+ ),
+ }}
+ />
+ <Tabs.Screen
+ name="guides"
+ options={{
+ title: 'Guides',
+ tabBarIcon: ({ focused }) => (
+ <TabIcon label={focused ? 'menu_book_filled' : 'menu_book'} focused={focused} />
+ ),
+ }}
+ />
+ </Tabs>
+ );
+}
diff --git a/app/(tabs)/guides.tsx b/app/(tabs)/guides.tsx
new file mode 100644
index 0000000..cbcdd87
--- /dev/null
+++ b/app/(tabs)/guides.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import BHVWebView from '@/components/BHVWebView';
+import { Colors } from '@/constants/Colors';
+
+export default function GuidesScreen() {
+ return (
+ <SafeAreaView style={styles.container} edges={['top']}>
+ <BHVWebView
+ initialUrl="https://beverlyhillsvideos.com/restaurants.html"
+ shareTitle="Beverly Hills Dining Guides"
+ />
+ </SafeAreaView>
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.cream,
+ },
+});
diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
new file mode 100644
index 0000000..900ebd1
--- /dev/null
+++ b/app/(tabs)/index.tsx
@@ -0,0 +1,35 @@
+import React, { useCallback } from 'react';
+import { Share, TouchableOpacity, Text, StyleSheet, Platform } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import BHVWebView from '@/components/BHVWebView';
+import { Colors } from '@/constants/Colors';
+
+export default function HomeScreen() {
+ const handleShare = useCallback(async () => {
+ try {
+ await Share.share({
+ title: 'Beverly Hills Videos',
+ message: 'Discover the best of Beverly Hills dining, shopping & culture — https://beverlyhillsvideos.com',
+ url: 'https://beverlyhillsvideos.com',
+ });
+ } catch {
+ // user dismissed
+ }
+ }, []);
+
+ return (
+ <SafeAreaView style={styles.container} edges={['top']}>
+ <BHVWebView
+ initialUrl="https://beverlyhillsvideos.com/"
+ shareTitle="Beverly Hills Videos"
+ />
+ </SafeAreaView>
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.cream,
+ },
+});
diff --git a/app/(tabs)/map.tsx b/app/(tabs)/map.tsx
new file mode 100644
index 0000000..7dfb6b5
--- /dev/null
+++ b/app/(tabs)/map.tsx
@@ -0,0 +1,492 @@
+import React, { useState, useCallback, useMemo, useRef } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ TouchableOpacity,
+ ScrollView,
+ Share,
+ Linking,
+ Platform,
+ Dimensions,
+} from 'react-native';
+import MapView, { Marker, Callout, Region, PROVIDER_DEFAULT } from 'react-native-maps';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { Colors } from '@/constants/Colors';
+import { useFavorites } from '@/hooks/useFavorites';
+import FilmModal from '@/components/FilmModal';
+import FavoritesModal from '@/components/FavoritesModal';
+import { MapLocation, LocationType } from '@/types';
+
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const locationsData = require('@/assets/map-locations.json') as { locations: MapLocation[] };
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const filmsData = require('@/assets/films.json') as { name: string; slug: string }[];
+
+const BEVERLY_HILLS_REGION: Region = {
+ latitude: 34.0696,
+ longitude: -118.4004,
+ latitudeDelta: 0.035,
+ longitudeDelta: 0.035,
+};
+
+const FILTER_TYPES: { key: LocationType | 'all'; label: string }[] = [
+ { key: 'all', label: 'All' },
+ { key: 'restaurant', label: 'Dining' },
+ { key: 'store', label: 'Shopping' },
+ { key: 'doctor', label: 'Wellness' },
+ { key: 'lawyer', label: 'Legal' },
+];
+
+function markerColor(type: LocationType): string {
+ switch (type) {
+ case 'restaurant': return Colors.markerRestaurant;
+ case 'store': return Colors.markerStore;
+ case 'doctor': return Colors.markerDoctor;
+ case 'lawyer': return Colors.markerLawyer;
+ default: return Colors.ink;
+ }
+}
+
+function nameToSlug(name: string): string {
+ return name
+ .toLowerCase()
+ .replace(/['']/g, '')
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-+|-+$/g, '');
+}
+
+function findFilmSlug(locationName: string): string | null {
+ const normalized = nameToSlug(locationName);
+ const match = filmsData.find(
+ (f) =>
+ f.slug === normalized ||
+ nameToSlug(f.name) === normalized ||
+ f.slug === normalized.replace(/-beverly-hills$/, '') ||
+ normalized.startsWith(f.slug)
+ );
+ return match?.slug ?? null;
+}
+
+interface CalloutContentProps {
+ location: MapLocation;
+ isFavorite: boolean;
+ onToggleFavorite: (name: string) => void;
+ onCall: () => void;
+ onMenu: () => void;
+ onFilm: () => void;
+ hasFilm: boolean;
+}
+
+function CalloutContent({
+ location,
+ isFavorite,
+ onToggleFavorite,
+ onCall,
+ onMenu,
+ onFilm,
+ hasFilm,
+}: CalloutContentProps) {
+ return (
+ <View style={calloutStyles.container}>
+ <View style={calloutStyles.header}>
+ <View style={calloutStyles.headerText}>
+ <Text style={calloutStyles.name} numberOfLines={2}>
+ {location.name}
+ </Text>
+ <Text style={calloutStyles.category}>{location.category}</Text>
+ </View>
+ <TouchableOpacity
+ onPress={() => onToggleFavorite(location.name)}
+ hitSlop={6}
+ style={calloutStyles.heartBtn}
+ >
+ <Text style={[calloutStyles.heart, isFavorite && calloutStyles.heartActive]}>
+ {isFavorite ? 'hearts' : 'heart'}
+ </Text>
+ </TouchableOpacity>
+ </View>
+ <Text style={calloutStyles.address} numberOfLines={2}>
+ {location.address}
+ </Text>
+ <View style={calloutStyles.actions}>
+ {location.phone ? (
+ <TouchableOpacity style={calloutStyles.btn} onPress={onCall}>
+ <Text style={calloutStyles.btnText}>Call</Text>
+ </TouchableOpacity>
+ ) : null}
+ {location.menu_url ? (
+ <TouchableOpacity style={[calloutStyles.btn, calloutStyles.btnOutline]} onPress={onMenu}>
+ <Text style={calloutStyles.btnOutlineText}>Menu</Text>
+ </TouchableOpacity>
+ ) : null}
+ {hasFilm ? (
+ <TouchableOpacity style={[calloutStyles.btn, calloutStyles.btnFilm]} onPress={onFilm}>
+ <Text style={calloutStyles.btnText}>Film</Text>
+ </TouchableOpacity>
+ ) : null}
+ </View>
+ </View>
+ );
+}
+
+const calloutStyles = StyleSheet.create({
+ container: {
+ width: 230,
+ padding: 12,
+ backgroundColor: '#fff',
+ borderRadius: 10,
+ shadowColor: '#000',
+ shadowOpacity: 0.15,
+ shadowRadius: 8,
+ shadowOffset: { width: 0, height: 3 },
+ elevation: 4,
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ marginBottom: 4,
+ },
+ headerText: {
+ flex: 1,
+ marginRight: 8,
+ },
+ name: {
+ fontSize: 14,
+ fontWeight: '700',
+ color: Colors.ink,
+ lineHeight: 18,
+ },
+ category: {
+ fontSize: 11,
+ color: Colors.gold,
+ fontWeight: '600',
+ marginTop: 2,
+ textTransform: 'uppercase',
+ letterSpacing: 0.5,
+ },
+ heartBtn: {
+ padding: 2,
+ },
+ heart: {
+ fontSize: 18,
+ color: Colors.border,
+ },
+ heartActive: {
+ color: Colors.gold,
+ },
+ address: {
+ fontSize: 11,
+ color: Colors.textSecondary,
+ lineHeight: 15,
+ marginBottom: 10,
+ },
+ actions: {
+ flexDirection: 'row',
+ gap: 6,
+ flexWrap: 'wrap',
+ },
+ btn: {
+ backgroundColor: Colors.green,
+ paddingVertical: 5,
+ paddingHorizontal: 12,
+ borderRadius: 5,
+ },
+ btnOutline: {
+ backgroundColor: 'transparent',
+ borderWidth: 1,
+ borderColor: Colors.green,
+ },
+ btnFilm: {
+ backgroundColor: Colors.gold,
+ },
+ btnText: {
+ color: '#fff',
+ fontSize: 12,
+ fontWeight: '600',
+ },
+ btnOutlineText: {
+ color: Colors.green,
+ fontSize: 12,
+ fontWeight: '600',
+ },
+});
+
+export default function MapScreen() {
+ const mapRef = useRef<MapView>(null);
+ const [activeFilter, setActiveFilter] = useState<LocationType | 'all'>('all');
+ const [filmModal, setFilmModal] = useState<{ url: string; title: string } | null>(null);
+ const [showFavorites, setShowFavorites] = useState(false);
+ const { favorites, isFavorite, toggle } = useFavorites();
+
+ const locations = locationsData.locations;
+
+ const filtered = useMemo(() => {
+ if (activeFilter === 'all') return locations;
+ return locations.filter((loc) => loc.type === activeFilter);
+ }, [locations, activeFilter]);
+
+ const handleCall = useCallback((phone: string) => {
+ Linking.openURL(`tel:${phone}`).catch(() => {});
+ }, []);
+
+ const handleMenu = useCallback((url: string) => {
+ Linking.openURL(url).catch(() => {});
+ }, []);
+
+ const handleFilm = useCallback((location: MapLocation) => {
+ const slug = location.slug ?? findFilmSlug(location.name);
+ if (slug) {
+ setFilmModal({
+ url: `https://beverlyhillsvideos.com/restaurants/${slug}.html`,
+ title: location.name,
+ });
+ }
+ }, []);
+
+ const handleShare = useCallback(async () => {
+ try {
+ await Share.share({
+ title: 'Beverly Hills Videos — Map',
+ message: 'Explore Beverly Hills dining, shopping & more — https://beverlyhillsvideos.com',
+ url: 'https://beverlyhillsvideos.com',
+ });
+ } catch {
+ // user dismissed
+ }
+ }, []);
+
+ const handleRecenter = useCallback(() => {
+ mapRef.current?.animateToRegion(BEVERLY_HILLS_REGION, 400);
+ }, []);
+
+ const favCount = favorites.size;
+
+ return (
+ <SafeAreaView style={styles.container} edges={['top']}>
+ {/* Header */}
+ <View style={styles.header}>
+ <Text style={styles.headerTitle}>Beverly Hills</Text>
+ <View style={styles.headerActions}>
+ <TouchableOpacity onPress={handleShare} style={styles.iconBtn} hitSlop={6}>
+ <Text style={styles.iconText}>share</Text>
+ </TouchableOpacity>
+ <TouchableOpacity
+ onPress={() => setShowFavorites(true)}
+ style={styles.iconBtn}
+ hitSlop={6}
+ >
+ <Text style={styles.iconText}>favorite</Text>
+ {favCount > 0 && (
+ <View style={styles.badge}>
+ <Text style={styles.badgeText}>{favCount > 9 ? '9+' : favCount}</Text>
+ </View>
+ )}
+ </TouchableOpacity>
+ </View>
+ </View>
+
+ {/* Filter chips */}
+ <View style={styles.filterBar}>
+ <ScrollView
+ horizontal
+ showsHorizontalScrollIndicator={false}
+ contentContainerStyle={styles.filterScroll}
+ >
+ {FILTER_TYPES.map((f) => (
+ <TouchableOpacity
+ key={f.key}
+ style={[
+ styles.chip,
+ activeFilter === f.key && styles.chipActive,
+ ]}
+ onPress={() => setActiveFilter(f.key)}
+ activeOpacity={0.75}
+ >
+ <Text
+ style={[
+ styles.chipText,
+ activeFilter === f.key && styles.chipTextActive,
+ ]}
+ >
+ {f.label}
+ </Text>
+ </TouchableOpacity>
+ ))}
+ </ScrollView>
+ </View>
+
+ {/* Map */}
+ <MapView
+ ref={mapRef}
+ style={styles.map}
+ provider={PROVIDER_DEFAULT}
+ initialRegion={BEVERLY_HILLS_REGION}
+ showsUserLocation
+ showsMyLocationButton={false}
+ showsCompass
+ showsScale
+ >
+ {filtered.map((location, idx) => {
+ const slug = location.slug ?? findFilmSlug(location.name);
+ const hasFilm = Boolean(slug);
+ const color = markerColor(location.type);
+
+ return (
+ <Marker
+ key={`${location.name}-${idx}`}
+ coordinate={{ latitude: location.lat, longitude: location.lng }}
+ pinColor={color}
+ title={location.name}
+ >
+ <Callout tooltip onPress={() => {}}>
+ <CalloutContent
+ location={location}
+ isFavorite={isFavorite(location.name)}
+ onToggleFavorite={toggle}
+ onCall={() => location.phone && handleCall(location.phone)}
+ onMenu={() => location.menu_url && handleMenu(location.menu_url)}
+ onFilm={() => handleFilm(location)}
+ hasFilm={hasFilm}
+ />
+ </Callout>
+ </Marker>
+ );
+ })}
+ </MapView>
+
+ {/* Recenter button */}
+ <TouchableOpacity style={styles.recenterBtn} onPress={handleRecenter} activeOpacity={0.85}>
+ <Text style={styles.recenterText}>center_focus_strong</Text>
+ </TouchableOpacity>
+
+ {/* Film modal */}
+ {filmModal && (
+ <FilmModal
+ visible={Boolean(filmModal)}
+ url={filmModal.url}
+ title={filmModal.title}
+ onClose={() => setFilmModal(null)}
+ />
+ )}
+
+ {/* Favorites modal */}
+ <FavoritesModal
+ visible={showFavorites}
+ onClose={() => setShowFavorites(false)}
+ locations={locations}
+ favorites={favorites}
+ onToggleFavorite={toggle}
+ />
+ </SafeAreaView>
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.cream,
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 18,
+ paddingVertical: 10,
+ backgroundColor: Colors.cream,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: Colors.border,
+ },
+ headerTitle: {
+ flex: 1,
+ fontSize: 20,
+ fontWeight: '700',
+ color: Colors.ink,
+ letterSpacing: 0.2,
+ },
+ headerActions: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 14,
+ },
+ iconBtn: {
+ position: 'relative',
+ padding: 4,
+ },
+ iconText: {
+ fontSize: 22,
+ color: Colors.ink,
+ },
+ badge: {
+ position: 'absolute',
+ top: -2,
+ right: -2,
+ backgroundColor: Colors.gold,
+ borderRadius: 8,
+ minWidth: 16,
+ height: 16,
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingHorizontal: 3,
+ },
+ badgeText: {
+ color: '#fff',
+ fontSize: 9,
+ fontWeight: '700',
+ },
+ filterBar: {
+ backgroundColor: Colors.cream,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: Colors.border,
+ },
+ filterScroll: {
+ paddingHorizontal: 14,
+ paddingVertical: 8,
+ gap: 8,
+ },
+ chip: {
+ paddingVertical: 6,
+ paddingHorizontal: 16,
+ borderRadius: 20,
+ backgroundColor: Colors.surface,
+ borderWidth: 1,
+ borderColor: Colors.border,
+ },
+ chipActive: {
+ backgroundColor: Colors.green,
+ borderColor: Colors.green,
+ },
+ chipText: {
+ fontSize: 13,
+ fontWeight: '500',
+ color: Colors.textSecondary,
+ },
+ chipTextActive: {
+ color: '#fff',
+ fontWeight: '600',
+ },
+ map: {
+ flex: 1,
+ },
+ recenterBtn: {
+ position: 'absolute',
+ bottom: 24,
+ right: 18,
+ backgroundColor: Colors.cream,
+ borderRadius: 26,
+ width: 52,
+ height: 52,
+ alignItems: 'center',
+ justifyContent: 'center',
+ shadowColor: '#000',
+ shadowOpacity: 0.18,
+ shadowRadius: 8,
+ shadowOffset: { width: 0, height: 2 },
+ elevation: 4,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: Colors.border,
+ },
+ recenterText: {
+ fontSize: 24,
+ color: Colors.green,
+ },
+});
diff --git a/app/(tabs)/videos.tsx b/app/(tabs)/videos.tsx
new file mode 100644
index 0000000..07a652b
--- /dev/null
+++ b/app/(tabs)/videos.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import BHVWebView from '@/components/BHVWebView';
+import { Colors } from '@/constants/Colors';
+
+export default function VideosScreen() {
+ return (
+ <SafeAreaView style={styles.container} edges={['top']}>
+ <BHVWebView
+ initialUrl="https://beverlyhillsvideos.com/videos.html"
+ shareTitle="Beverly Hills Videos — Watch Films"
+ />
+ </SafeAreaView>
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.cream,
+ },
+});
diff --git a/app/_layout.tsx b/app/_layout.tsx
new file mode 100644
index 0000000..4aedf45
--- /dev/null
+++ b/app/_layout.tsx
@@ -0,0 +1,23 @@
+import { useEffect } from 'react';
+import { Stack } from 'expo-router';
+import { StatusBar } from 'expo-status-bar';
+import * as SplashScreen from 'expo-splash-screen';
+import { Colors } from '@/constants/Colors';
+
+SplashScreen.preventAutoHideAsync();
+
+export default function RootLayout() {
+ useEffect(() => {
+ // Hide splash after fonts/assets would load here
+ SplashScreen.hideAsync();
+ }, []);
+
+ return (
+ <>
+ <StatusBar style="dark" backgroundColor={Colors.cream} />
+ <Stack screenOptions={{ headerShown: false }}>
+ <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
+ </Stack>
+ </>
+ );
+}
diff --git a/assets/films.json b/assets/films.json
new file mode 100644
index 0000000..3aabea6
--- /dev/null
+++ b/assets/films.json
@@ -0,0 +1,310 @@
+[
+ {
+ "name": "Spago Beverly Hills",
+ "slug": "spago",
+ "blurb": "Wolfgang Puck’s flagship on Canon Drive — the restaurant that helped define California fine dining.",
+ "src": "/video/spago.mp4",
+ "poster": "/video/spago.jpg"
+ },
+ {
+ "name": "Funke",
+ "slug": "funke",
+ "blurb": "Tucked along South Santa Monica Boulevard, Funke is chef Evan Funke's temple to handmade pasta.",
+ "src": "/video/funke.mp4",
+ "poster": "/video/funke.jpg"
+ },
+ {
+ "name": "Mr. Chow",
+ "slug": "mr-chow",
+ "blurb": "On North Camden Drive, Mr. Chow has been Beverly Hills' theater of Beijing cuisine since 1974.",
+ "src": "/video/mr-chow.mp4",
+ "poster": "/video/mr-chow.jpg"
+ },
+ {
+ "name": "Il Pastaio",
+ "slug": "il-pastaio",
+ "blurb": "At the corner of Canon and Brighton, Il Pastaio is Beverly Hills' beloved sidewalk trattoria.",
+ "src": "/video/il-pastaio.mp4",
+ "poster": "/video/il-pastaio.jpg"
+ },
+ {
+ "name": "Matsuhisa",
+ "slug": "matsuhisa",
+ "blurb": "On La Cienega, Matsuhisa is where Nobu Matsuhisa changed sushi forever.",
+ "src": "/video/matsuhisa.mp4",
+ "poster": "/video/matsuhisa.jpg"
+ },
+ {
+ "name": "Lawry's The Prime Rib",
+ "slug": "lawry-s-the-prime-rib",
+ "blurb": "Since 1938, Lawry's The Prime Rib has carved its legend tableside from gleaming silver carts.",
+ "src": "/video/lawry-s-the-prime-rib.mp4",
+ "poster": "/video/lawry-s-the-prime-rib.jpg"
+ },
+ {
+ "name": "Avra Beverly Hills",
+ "slug": "avra-beverly-hills",
+ "blurb": "On North Beverly Drive, Avra brings the Greek islands to Beverly Hills.",
+ "src": "/video/avra-beverly-hills.mp4",
+ "poster": "/video/avra-beverly-hills.jpg"
+ },
+ {
+ "name": "The Grill on the Alley",
+ "slug": "the-grill-on-the-alley",
+ "blurb": "Tucked off Dayton Way, The Grill on the Alley is Beverly Hills' quintessential power-lunch table.",
+ "src": "/video/the-grill-on-the-alley.mp4",
+ "poster": "/video/the-grill-on-the-alley.jpg"
+ },
+ {
+ "name": "E. Baldi",
+ "slug": "e-baldi",
+ "blurb": "On Canon Drive, E. Baldi is Beverly Hills' insider Italian, run by Giorgio Baldi's son Edoardo.",
+ "src": "/video/e-baldi.mp4",
+ "poster": "/video/e-baldi.jpg"
+ },
+ {
+ "name": "La Scala",
+ "slug": "la-scala",
+ "blurb": "A Beverly Hills institution since 1956, La Scala on Canon Drive is home of the original chopped salad.",
+ "src": "/video/la-scala.mp4",
+ "poster": "/video/la-scala.jpg"
+ },
+ {
+ "name": "Nate 'n Al's",
+ "slug": "nate-n-al-s",
+ "blurb": "On Beverly Drive since 1945, Nate 'n Al's is the beloved deli of Beverly Hills.",
+ "src": "/video/nate-n-al-s.mp4",
+ "poster": "/video/nate-n-al-s.jpg"
+ },
+ {
+ "name": "Wally's",
+ "slug": "wally-s",
+ "blurb": "On Canon Drive, Wally's pairs a world-class wine list with polished market cuisine. What began as a…",
+ "src": "/video/wally-s.mp4",
+ "poster": "/video/wally-s.jpg"
+ },
+ {
+ "name": "The Polo Lounge",
+ "slug": "the-polo-lounge",
+ "blurb": "Inside the pink-and-green Beverly Hills Hotel, the Polo Lounge is old Hollywood itself. Since 1941,…",
+ "src": "/video/the-polo-lounge.mp4",
+ "poster": "/video/the-polo-lounge.jpg"
+ },
+ {
+ "name": "The Belvedere",
+ "slug": "the-belvedere",
+ "blurb": "At the Peninsula Beverly Hills, The Belvedere is refined hotel dining at its finest. A jewel-box…",
+ "src": "/video/the-belvedere.mp4",
+ "poster": "/video/the-belvedere.jpg"
+ },
+ {
+ "name": "Steak 48",
+ "slug": "steak-48",
+ "blurb": "On Canon Drive, Steak 48 brings modern steakhouse energy to Beverly Hills. An open kitchen, seafood…",
+ "src": "/video/steak-48.mp4",
+ "poster": "/video/steak-48.jpg"
+ },
+ {
+ "name": "Il Cielo",
+ "slug": "il-cielo",
+ "blurb": "On Burton Way, Il Cielo is Beverly Hills' most romantic dinner. A twinkling garden of fairy lights…",
+ "src": "/video/il-cielo.mp4",
+ "poster": "/video/il-cielo.jpg"
+ },
+ {
+ "name": "Cipriani",
+ "slug": "cipriani",
+ "blurb": "The Venetian legend arrives in Beverly Hills. Cipriani brings its Bellinis, its carpaccio, and its…",
+ "src": "/video/cipriani.mp4",
+ "poster": "/video/cipriani.jpg"
+ },
+ {
+ "name": "CUT by Wolfgang Puck",
+ "blurb": "Inside the Beverly Wilshire, CUT is Wolfgang Puck's Michelin-starred modern steakhouse — the room…",
+ "src": "/video/cut.mp4",
+ "poster": "/video/cut.jpg",
+ "slug": "cut"
+ },
+ {
+ "name": "Mastro's Steakhouse Beverly Hills",
+ "blurb": "On North Canon Drive, Mastro's is the steakhouse where Beverly Hills celebrates — live music, the…",
+ "src": "/video/mastros.mp4",
+ "poster": "/video/mastros.jpg",
+ "slug": "mastros"
+ },
+ {
+ "name": "Nozawa Bar",
+ "slug": "nozawa-bar",
+ "blurb": "Hidden behind Sugarfish on Canon Drive, Nozawa Bar is Beverly Hills' most exclusive sushi counter —…",
+ "src": "/video/nozawa-bar.mp4",
+ "poster": "/video/nozawa-bar.jpg"
+ },
+ {
+ "name": "matu",
+ "slug": "matu",
+ "blurb": "On Canon Drive, matu is a minimalist temple to one thing done perfectly — single-ranch Wagyu,…",
+ "src": "/video/matu.mp4",
+ "poster": "/video/matu.jpg"
+ },
+ {
+ "name": "Porta Via",
+ "slug": "porta-via",
+ "blurb": "On Canon Drive, Porta Via is Beverly Hills' beloved market-driven cafe — sunny, effortless, and…",
+ "src": "/video/porta-via.mp4",
+ "poster": "/video/porta-via.jpg"
+ },
+ {
+ "name": "Via Alloro",
+ "slug": "via-alloro",
+ "blurb": "On Brighton Way, Via Alloro brings warm Tuscan hospitality to the heart of Beverly Hills. Handmade…",
+ "src": "/video/via-alloro.mp4",
+ "poster": "/video/via-alloro.jpg"
+ },
+ {
+ "name": "Culina, Four Seasons",
+ "slug": "culina-four-seasons",
+ "blurb": "At the Four Seasons, Culina is modern Italian dining with a crudo bar and a glamorous garden…",
+ "src": "/video/culina-four-seasons.mp4",
+ "poster": "/video/culina-four-seasons.jpg"
+ },
+ {
+ "name": "The Blvd",
+ "slug": "the-blvd",
+ "blurb": "Inside the Beverly Wilshire on Rodeo Drive, The Blvd is the ultimate people-watching table.…",
+ "src": "/video/the-blvd.mp4",
+ "poster": "/video/the-blvd.jpg"
+ },
+ {
+ "name": "Dante Beverly Hills",
+ "slug": "dante-beverly-hills",
+ "blurb": "Atop The Maybourne, Dante is Beverly Hills' most glamorous rooftop — Italian coastal cooking,…",
+ "src": "/video/dante-beverly-hills.mp4",
+ "poster": "/video/dante-beverly-hills.jpg"
+ },
+ {
+ "name": "Sugarfish Beverly Hills",
+ "slug": "sugarfish-beverly-hills",
+ "blurb": "On Canon Drive, Sugarfish brought chef Kazunori Nozawa's philosophy to everyone — warm rice,…",
+ "src": "/video/sugarfish-beverly-hills.mp4",
+ "poster": "/video/sugarfish-beverly-hills.jpg"
+ },
+ {
+ "name": "Tempura Endo",
+ "slug": "tempura-endo",
+ "blurb": "Tempura Endo brings a centuries-old Kyoto lineage to Beverly Hills — an intimate counter devoted to…",
+ "src": "/video/tempura-endo.mp4",
+ "poster": "/video/tempura-endo.jpg"
+ },
+ {
+ "name": "Frida Beverly Hills",
+ "slug": "frida-beverly-hills",
+ "blurb": "On Burton Way, Frida serves refined regional Mexican in a warm, art-filled room. Handmade moles,…",
+ "src": "/video/frida-beverly-hills.mp4",
+ "poster": "/video/frida-beverly-hills.jpg"
+ },
+ {
+ "name": "The Honor Bar",
+ "slug": "the-honor-bar",
+ "blurb": "On South Beverly Drive, The Honor Bar is the Hillstone group's polished neighborhood favorite —…",
+ "src": "/video/the-honor-bar.mp4",
+ "poster": "/video/the-honor-bar.jpg"
+ },
+ {
+ "name": "Marea",
+ "slug": "marea",
+ "blurb": "On Camden Drive, Marea brings its acclaimed New York coastal Italian to Beverly Hills — luxurious…",
+ "src": "/video/marea.mp4",
+ "poster": "/video/marea.jpg"
+ },
+ {
+ "name": "Ocean Prime",
+ "slug": "ocean-prime",
+ "blurb": "On Wilshire Boulevard, Ocean Prime is a glamorous surf-and-turf destination — prime steaks,…",
+ "src": "/video/ocean-prime.mp4",
+ "poster": "/video/ocean-prime.jpg"
+ },
+ {
+ "name": "Azur Beverly Hills",
+ "slug": "azur-beverly-hills",
+ "blurb": "New to Canon Drive, Azur brings the French Riviera to Beverly Hills — sun-washed Mediterranean…",
+ "src": "/video/azur-beverly-hills.mp4",
+ "poster": "/video/azur-beverly-hills.jpg"
+ },
+ {
+ "name": "Bad Roman",
+ "slug": "bad-roman",
+ "blurb": "In the former Palm space on Canon Drive, Bad Roman is theatrical, over-the-top Italian-American —…",
+ "src": "/video/bad-roman.mp4",
+ "poster": "/video/bad-roman.jpg"
+ },
+ {
+ "name": "88 Club",
+ "slug": "88-club",
+ "blurb": "On Santa Monica Boulevard, 88 Club is Beverly Hills' glamorous take on the Chinese supper club —…",
+ "src": "/video/88-club.mp4",
+ "poster": "/video/88-club.jpg"
+ },
+ {
+ "name": "Fogo de Chao",
+ "slug": "fogo-de-chao",
+ "blurb": "On Restaurant Row, Fogo de Chao brings Brazilian churrasco to Beverly Hills — endless fire-roasted…",
+ "src": "/video/fogo-de-chao.mp4",
+ "poster": "/video/fogo-de-chao.jpg"
+ },
+ {
+ "name": "The Rooftop by JG",
+ "slug": "the-rooftop-by-jg",
+ "blurb": "Atop the Waldorf Astoria, The Rooftop by JG pairs Jean-Georges Vongerichten's cooking with sweeping…",
+ "src": "/video/the-rooftop-by-jg.mp4",
+ "poster": "/video/the-rooftop-by-jg.jpg"
+ },
+ {
+ "name": "The Hideaway",
+ "slug": "the-hideaway",
+ "blurb": "Inside the Rodeo Collection, The Hideaway is a glamorous Mexican steakhouse — prime cuts with a…",
+ "src": "/video/the-hideaway.mp4",
+ "poster": "/video/the-hideaway.jpg"
+ },
+ {
+ "name": "The Terrace at The Maybourne",
+ "slug": "the-terrace-at-the-maybourne",
+ "blurb": "At The Maybourne, The Terrace is an idyllic all-day retreat overlooking Beverly Canon Gardens —…",
+ "src": "/video/the-terrace-at-the-maybourne.mp4",
+ "poster": "/video/the-terrace-at-the-maybourne.jpg"
+ },
+ {
+ "name": "Miura",
+ "slug": "miura",
+ "blurb": "On Rodeo Drive, Miura is a Michelin-starred edomae sushi counter led by chef Derek Wilcox —…",
+ "src": "/video/miura.mp4",
+ "poster": "/video/miura.jpg"
+ },
+ {
+ "name": "South Beverly Grill",
+ "slug": "south-beverly-grill",
+ "blurb": "On South Beverly Drive, South Beverly Grill is the Hillstone group's polished all-day favorite — a…",
+ "src": "/video/south-beverly-grill.mp4",
+ "poster": "/video/south-beverly-grill.jpg"
+ },
+ {
+ "name": "Piccolo Paradiso",
+ "slug": "piccolo-paradiso",
+ "blurb": "On South Beverly Drive, Piccolo Paradiso is the warm, family-run Italian the neighborhood adores —…",
+ "src": "/video/piccolo-paradiso.mp4",
+ "poster": "/video/piccolo-paradiso.jpg"
+ },
+ {
+ "name": "Kazan",
+ "slug": "kazan",
+ "blurb": "On La Cienega, Kazan is a Michelin Bib Gourmand ramen house — rich, soulful bowls and Japanese…",
+ "src": "/video/kazan.mp4",
+ "poster": "/video/kazan.jpg"
+ },
+ {
+ "name": "Xi'an",
+ "slug": "xi-an",
+ "blurb": "On Canon Drive since 1996, Xi'an is Beverly Hills' beloved healthy Chinese institution — lighter,…",
+ "src": "/video/xi-an.mp4",
+ "poster": "/video/xi-an.jpg"
+ }
+]
diff --git a/assets/icon.png b/assets/icon.png
new file mode 100644
index 0000000..83a418b
Binary files /dev/null and b/assets/icon.png differ
diff --git a/assets/map-locations.json b/assets/map-locations.json
new file mode 100644
index 0000000..8f536dc
--- /dev/null
+++ b/assets/map-locations.json
@@ -0,0 +1,777 @@
+{
+ "locations": [
+ {
+ "name": "Spago",
+ "type": "restaurant",
+ "category": "Californian",
+ "address": "176 N Canon Dr, Beverly Hills, CA 90210",
+ "lat": 34.067665,
+ "lng": -118.3978072,
+ "phone": "+1-310-385-0880",
+ "menu_url": "https://wolfgangpuck.com/restaurants/spago-beverly-hills/"
+ },
+ {
+ "name": "Mastro's Steakhouse",
+ "type": "restaurant",
+ "category": "Steakhouse",
+ "address": "246 N Canon Dr, Beverly Hills, CA 90210",
+ "lat": 34.0688014,
+ "lng": -118.398793,
+ "phone": "+1-310-888-8782",
+ "menu_url": "https://www.mastrosrestaurants.com/location/mastros-steakhouse-beverly-hills/"
+ },
+ {
+ "name": "CUT by Wolfgang Puck",
+ "type": "restaurant",
+ "category": "Steakhouse",
+ "address": "9500 Wilshire Blvd, Beverly Hills, CA 90212",
+ "lat": 34.0668881,
+ "lng": -118.4003735,
+ "phone": "+1-310-276-8500",
+ "menu_url": "https://wolfgangpuck.com/restaurants/cut-beverly-hills/"
+ },
+ {
+ "name": "Mr. Chow",
+ "type": "restaurant",
+ "category": "Chinese",
+ "address": "344 N Camden Dr, Beverly Hills, CA 90210",
+ "lat": 34.0680733,
+ "lng": -118.4033227,
+ "phone": "+1-310-278-9911",
+ "menu_url": "https://www.mrchow.com/location/beverly-hills/"
+ },
+ {
+ "name": "Il Pastaio",
+ "type": "restaurant",
+ "category": "Italian",
+ "address": "400 N Canon Dr, Beverly Hills, CA 90210",
+ "lat": 34.0708486,
+ "lng": -118.4008226,
+ "phone": "+1-310-205-5444",
+ "menu_url": "https://www.ilpastaiobeverlyhills.com/our-menu/"
+ },
+ {
+ "name": "Matsuhisa",
+ "type": "restaurant",
+ "category": "Japanese",
+ "address": "129 N La Cienega Blvd, Beverly Hills, CA 90211",
+ "lat": 34.0683282,
+ "lng": -118.3765998,
+ "phone": "+1-310-659-9639",
+ "menu_url": "https://matsuhisabeverlyhills.com/menu/"
+ },
+ {
+ "name": "Lawry's The Prime Rib",
+ "type": "restaurant",
+ "category": "Prime rib",
+ "address": "100 N La Cienega Blvd, Beverly Hills, CA 90211",
+ "lat": 34.0678045,
+ "lng": -118.3760675,
+ "phone": "+1-310-652-2827",
+ "menu_url": "https://www.lawrysonline.com/lawrys-the-prime-rib-beverly-hills/"
+ },
+ {
+ "name": "Avra Beverly Hills",
+ "type": "restaurant",
+ "category": "Greek",
+ "address": "233 N Beverly Dr, Beverly Hills, CA 90210",
+ "lat": 34.067517,
+ "lng": -118.399645,
+ "phone": "+1-310-425-4200",
+ "menu_url": "https://theavragroup.com/avra-menus/beverly-hills-all-day"
+ },
+ {
+ "name": "The Grill on the Alley",
+ "type": "restaurant",
+ "category": "American",
+ "address": "9560 Dayton Way, Beverly Hills, CA 90210",
+ "lat": 34.0673002,
+ "lng": -118.402091,
+ "phone": "+1-310-276-0615",
+ "menu_url": "https://thegrillonthealley.com/beverly-hills/beverly-hills-menu/"
+ },
+ {
+ "name": "Funke",
+ "type": "restaurant",
+ "category": "Italian",
+ "address": "9388 S Santa Monica Blvd, Beverly Hills, CA 90210",
+ "lat": 34.0718063,
+ "lng": -118.4015099,
+ "phone": "+1-310-341-3737",
+ "menu_url": "https://www.funkela.com/"
+ },
+ {
+ "name": "E. Baldi",
+ "type": "restaurant",
+ "category": "Italian",
+ "address": "375 N Canon Dr, Beverly Hills, CA 90210",
+ "lat": 34.0701835,
+ "lng": -118.4005653,
+ "phone": "+1-310-248-2633",
+ "menu_url": "https://ebaldi.com/menu-intro"
+ },
+ {
+ "name": "La Scala",
+ "type": "restaurant",
+ "category": "Italian",
+ "address": "434 N Canon Dr, Beverly Hills, CA 90210",
+ "lat": 34.0713151,
+ "lng": -118.4014557,
+ "phone": "+1-310-275-0579",
+ "menu_url": "https://www.lascalabeverlyhills.com/menu"
+ },
+ {
+ "name": "Nate 'n Al's",
+ "type": "restaurant",
+ "category": "Deli",
+ "address": "414 N Beverly Dr, Beverly Hills, CA 90210",
+ "lat": 34.0703035,
+ "lng": -118.4021621,
+ "phone": "+1-310-274-0101",
+ "menu_url": "https://natenals.com/menus-1"
+ },
+ {
+ "name": "Wally's",
+ "type": "restaurant",
+ "category": "Wine bar",
+ "address": "447 N Canon Dr, Beverly Hills, CA 90210",
+ "lat": 34.0714005,
+ "lng": -118.4017989,
+ "phone": "+1-310-475-3540",
+ "menu_url": "https://www.wallywine.com/beverlyhills"
+ },
+ {
+ "name": "The Polo Lounge",
+ "type": "restaurant",
+ "category": "Californian",
+ "address": "9641 Sunset Blvd, Beverly Hills, CA 90210",
+ "lat": 34.0814561,
+ "lng": -118.4143932,
+ "phone": "+1-310-887-2777",
+ "menu_url": "https://www.dorchestercollection.com/los-angeles/the-beverly-hills-hotel/dining/polo-lounge"
+ },
+ {
+ "name": "The Belvedere",
+ "type": "restaurant",
+ "category": "Fine dining",
+ "address": "9882 S Santa Monica Blvd, Beverly Hills, CA 90212",
+ "lat": 34.0679346,
+ "lng": -118.4082522,
+ "phone": "+1-310-788-2306",
+ "menu_url": "https://www.peninsula.com/en/beverly-hills/hotel-fine-dining/the-belvedere"
+ },
+ {
+ "name": "Steak 48",
+ "type": "restaurant",
+ "category": "Steakhouse",
+ "address": "250 N Canon Dr, Beverly Hills, CA 90210",
+ "lat": 34.0685893,
+ "lng": -118.3987238,
+ "phone": "+1-310-359-8300",
+ "menu_url": "https://www.steak48.com/steakhouses/beverly-hills/menus/"
+ },
+ {
+ "name": "Cipriani",
+ "type": "restaurant",
+ "category": "Italian",
+ "address": "9560 Dayton Way, Beverly Hills, CA 90210",
+ "lat": 34.0673002,
+ "lng": -118.402091,
+ "phone": "+1-310-271-9600",
+ "menu_url": "https://www.cipriani.com/cipriani-beverly-hills"
+ },
+ {
+ "name": "Il Cielo",
+ "type": "restaurant",
+ "category": "Italian",
+ "address": "9018 Burton Way, Beverly Hills, CA 90211",
+ "lat": 34.0718554,
+ "lng": -118.3883723,
+ "phone": "+1-310-276-9990",
+ "menu_url": "https://www.ilcielo.com/menus/"
+ },
+ {
+ "name": "Gucci",
+ "type": "store",
+ "category": "Fashion",
+ "address": "347 N Rodeo Dr, Beverly Hills, CA 90210",
+ "lat": 34.0685924,
+ "lng": -118.402649,
+ "phone": "+1-310-278-3451"
+ },
+ {
+ "name": "Louis Vuitton",
+ "type": "store",
+ "category": "Fashion",
+ "address": "295 N Rodeo Dr, Beverly Hills, CA 90210",
+ "lat": 34.067368,
+ "lng": -118.4013943,
+ "phone": "+1-310-859-0457"
+ },
+ {
+ "name": "Chanel",
+ "type": "store",
+ "category": "Fashion",
+ "address": "400 N Rodeo Dr, Beverly Hills, CA 90210",
+ "lat": 34.0694034,
+ "lng": -118.402939,
+ "phone": "+1-310-278-5500"
+ },
+ {
+ "name": "Dior",
+ "type": "store",
+ "category": "Fashion",
+ "address": "309 N Rodeo Dr, Beverly Hills, CA 90210",
+ "lat": 34.0679223,
+ "lng": -118.401958,
+ "phone": "+1-310-859-4700"
+ },
+ {
+ "name": "Prada",
+ "type": "store",
+ "category": "Fashion",
+ "address": "343 N Rodeo Dr, Beverly Hills, CA 90210",
+ "lat": 34.0684678,
+ "lng": -118.402524,
+ "phone": "+1-310-278-8661"
+ },
+ {
+ "name": "Tiffany & Co.",
+ "type": "store",
+ "category": "Jewelry",
+ "address": "210 N Rodeo Dr, Beverly Hills, CA 90210",
+ "lat": 34.0673085,
+ "lng": -118.4008559,
+ "phone": "+1-310-273-8880"
+ },
+ {
+ "name": "Cartier",
+ "type": "store",
+ "category": "Jewelry",
+ "address": "370 N Rodeo Dr, Beverly Hills, CA 90210",
+ "lat": 34.069199,
+ "lng": -118.402742,
+ "phone": "+1-310-275-4272"
+ },
+ {
+ "name": "Hermès",
+ "type": "store",
+ "category": "Fashion",
+ "address": "434 N Rodeo Dr, Beverly Hills, CA 90210",
+ "lat": 34.0700805,
+ "lng": -118.403617,
+ "phone": "+1-310-278-6440"
+ },
+ {
+ "name": "Saint Laurent",
+ "type": "store",
+ "category": "Fashion",
+ "address": "326 N Rodeo Dr, Beverly Hills, CA 90210",
+ "lat": 34.0684106,
+ "lng": -118.4019843,
+ "phone": "+1-310-271-4110"
+ },
+ {
+ "name": "Versace",
+ "type": "store",
+ "category": "Fashion",
+ "address": "248 N Rodeo Dr, Beverly Hills, CA 90210",
+ "lat": 34.0682272,
+ "lng": -118.401844,
+ "phone": "+1-310-205-3921"
+ },
+ {
+ "name": "Tom Ford",
+ "type": "store",
+ "category": "Fashion",
+ "address": "346 N Rodeo Dr, Beverly Hills, CA 90210",
+ "lat": 34.0687678,
+ "lng": -118.4023041,
+ "phone": "+1-310-274-8686"
+ },
+ {
+ "name": "Two Rodeo",
+ "type": "store",
+ "category": "Shopping",
+ "address": "2 Rodeo Dr, Beverly Hills, CA 90210",
+ "lat": 34.0676065,
+ "lng": -118.401447,
+ "phone": "+1-310-247-7040"
+ },
+ {
+ "name": "PROFILES Beverly Hills Plastic Surgery",
+ "type": "doctor",
+ "category": "Plastic Surgery",
+ "address": "435 N Bedford Dr, Suite 500, Beverly Hills, CA 90210",
+ "lat": 34.068325,
+ "lng": -118.405831,
+ "phone": "+1-310-276-6800"
+ },
+ {
+ "name": "Beverly Hills Plastic Surgery Group",
+ "type": "doctor",
+ "category": "Plastic Surgery",
+ "address": "436 N Bedford Dr, Suite 214, Beverly Hills, CA 90210",
+ "lat": 34.068695,
+ "lng": -118.405512,
+ "phone": "+1-310-275-6600"
+ },
+ {
+ "name": "Dr. Michael K. Newman Plastic Surgery",
+ "type": "doctor",
+ "category": "Plastic Surgery",
+ "address": "416 N Bedford Dr, Suite 403, Beverly Hills, CA 90210",
+ "lat": 34.068337,
+ "lng": -118.405063,
+ "phone": "+1-310-859-0010"
+ },
+ {
+ "name": "Dermatology Associates Medical Group",
+ "type": "doctor",
+ "category": "Dermatology",
+ "address": "465 N Roxbury Dr, Suite 803, Beverly Hills, CA 90210",
+ "lat": 34.068198,
+ "lng": -118.407444,
+ "phone": "+1-310-274-9954"
+ },
+ {
+ "name": "Beverly Hills Dermatology Consultants",
+ "type": "doctor",
+ "category": "Dermatology",
+ "address": "433 N Camden Dr, Suite 805, Beverly Hills, CA 90210",
+ "lat": 34.069263,
+ "lng": -118.40518,
+ "phone": "+1-310-550-7661"
+ },
+ {
+ "name": "Ava MD Dermatology (Dr. Ava Shamban)",
+ "type": "doctor",
+ "category": "Dermatology",
+ "address": "9915 S Santa Monica Blvd, Beverly Hills, CA 90212",
+ "lat": 34.067935,
+ "lng": -118.408252,
+ "phone": "+1-310-843-9915"
+ },
+ {
+ "name": "Epione Beverly Hills (Dr. Simon Ourian)",
+ "type": "doctor",
+ "category": "Medspa / Aesthetics",
+ "address": "444 N Camden Dr, Beverly Hills, CA 90210",
+ "lat": 34.069553,
+ "lng": -118.404649,
+ "phone": "+1-310-651-6267"
+ },
+ {
+ "name": "Maven Medi Spa",
+ "type": "doctor",
+ "category": "Medspa / Aesthetics",
+ "address": "415 N Crescent Dr, Suite 140, Beverly Hills, CA 90210",
+ "lat": 34.071421,
+ "lng": -118.400085,
+ "phone": "+1-310-601-8606"
+ },
+ {
+ "name": "Dental90210",
+ "type": "doctor",
+ "category": "Cosmetic Dentistry",
+ "address": "415 N Crescent Dr, Suite 380, Beverly Hills, CA 90210",
+ "lat": 34.0714,
+ "lng": -118.40006,
+ "phone": "+1-424-653-9729"
+ },
+ {
+ "name": "Beverly Hills Aesthetic Dentistry",
+ "type": "doctor",
+ "category": "Cosmetic Dentistry",
+ "address": "435 N Bedford Dr, Suite 414, Beverly Hills, CA 90210",
+ "lat": 34.06831,
+ "lng": -118.40581,
+ "phone": "+1-310-276-2088"
+ },
+ {
+ "name": "Beverly Hills Institute of Ophthalmology",
+ "type": "doctor",
+ "category": "Ophthalmology / Lasik",
+ "address": "416 N Bedford Dr, Suite 300, Beverly Hills, CA 90210",
+ "lat": 34.06836,
+ "lng": -118.40504,
+ "phone": "+1-310-273-2333"
+ },
+ {
+ "name": "Beverly Hills Eye Associates",
+ "type": "doctor",
+ "category": "Ophthalmology / Lasik",
+ "address": "450 N Bedford Dr, Suite 101, Beverly Hills, CA 90210",
+ "lat": 34.069015,
+ "lng": -118.405825,
+ "phone": "+1-310-274-9205"
+ },
+ {
+ "name": "Law Advocate Group, LLP",
+ "type": "lawyer",
+ "category": "Entertainment Law",
+ "address": "9701 Wilshire Blvd, Suite 1000, Beverly Hills, CA 90212",
+ "lat": 34.067155,
+ "lng": -118.406142,
+ "phone": "+1-310-651-3065"
+ },
+ {
+ "name": "Johnson & Johnson LLP",
+ "type": "lawyer",
+ "category": "Entertainment Law",
+ "address": "439 N Canon Dr, Suite 200, Beverly Hills, CA 90210",
+ "lat": 34.0710875,
+ "lng": -118.4021873,
+ "phone": "+1-310-975-1080"
+ },
+ {
+ "name": "Lowe & Associates, P.C.",
+ "type": "lawyer",
+ "category": "Entertainment Law",
+ "address": "8383 Wilshire Blvd, Suite 1038, Beverly Hills, CA 90211",
+ "lat": 34.0651811,
+ "lng": -118.3734211,
+ "phone": "+1-310-477-5811"
+ },
+ {
+ "name": "Berenji Divorce & Family Law Group",
+ "type": "lawyer",
+ "category": "Family Law",
+ "address": "9465 Wilshire Blvd, Suite 333, Beverly Hills, CA 90212",
+ "lat": 34.067276,
+ "lng": -118.3995798,
+ "phone": "+1-213-277-2586"
+ },
+ {
+ "name": "Feinberg & Waller, APC",
+ "type": "lawyer",
+ "category": "Family Law",
+ "address": "9440 Santa Monica Blvd, Suite 301, Beverly Hills, CA 90210",
+ "lat": 34.0713071,
+ "lng": -118.402949,
+ "phone": "+1-310-627-2488"
+ },
+ {
+ "name": "LLM Family Law",
+ "type": "lawyer",
+ "category": "Family Law",
+ "address": "9777 Wilshire Blvd, Suite 507, Beverly Hills, CA 90212",
+ "lat": 34.0674116,
+ "lng": -118.4084039,
+ "phone": "+1-310-278-3112"
+ },
+ {
+ "name": "Mansouri Law Offices",
+ "type": "lawyer",
+ "category": "Family Law",
+ "address": "350 S Beverly Dr, Suite 330, Beverly Hills, CA 90212",
+ "lat": 34.060629,
+ "lng": -118.39898,
+ "phone": "+1-310-878-4077"
+ },
+ {
+ "name": "Howard & Howard",
+ "type": "lawyer",
+ "category": "Business / Litigation",
+ "address": "9465 Wilshire Blvd, Suite 300, Beverly Hills, CA 90212",
+ "lat": 34.06729,
+ "lng": -118.39956,
+ "phone": "+1-424-303-7700"
+ },
+ {
+ "name": "Levitt, Leichenger & Aberle LLP",
+ "type": "lawyer",
+ "category": "Business / Litigation",
+ "address": "8383 Wilshire Blvd, Suite 970, Beverly Hills, CA 90211",
+ "lat": 34.06519,
+ "lng": -118.37344,
+ "phone": "+1-323-524-2400"
+ },
+ {
+ "name": "Ervin Cohen & Jessup LLP",
+ "type": "lawyer",
+ "category": "Business / Litigation",
+ "address": "9401 Wilshire Blvd, 12th Floor, Beverly Hills, CA 90212",
+ "lat": 34.0674317,
+ "lng": -118.3981156,
+ "phone": "+1-310-273-6333"
+ },
+ {
+ "name": "Law Offices of Robert M. Bernstein",
+ "type": "lawyer",
+ "category": "Criminal Defense",
+ "address": "9465 Wilshire Blvd, Suite 300, Beverly Hills, CA 90212",
+ "lat": 34.06726,
+ "lng": -118.39959,
+ "phone": "+1-310-477-1480"
+ },
+ {
+ "name": "Cohen Counsel",
+ "type": "lawyer",
+ "category": "Estate Planning",
+ "address": "9465 Wilshire Blvd, 3rd Floor, Beverly Hills, CA 90212",
+ "lat": 34.06727,
+ "lng": -118.3996,
+ "phone": "+1-424-254-9925"
+ },
+ {
+ "name": "Law Offices of Lawrence H. Jacobson",
+ "type": "lawyer",
+ "category": "Real Estate Law",
+ "address": "9401 Wilshire Blvd, Suite 1250, Beverly Hills, CA 90212",
+ "lat": 34.06742,
+ "lng": -118.39813,
+ "phone": "+1-310-271-0747"
+ },
+ {
+ "name": "Nozawa Bar",
+ "type": "restaurant",
+ "category": "Sushi omakase",
+ "address": "212 N Canon Dr, Beverly Hills, CA 90210",
+ "lat": 34.06813,
+ "lng": -118.39827,
+ "phone": "+1-424-216-6158",
+ "menu_url": "https://www.exploretock.com/nozawabar"
+ },
+ {
+ "name": "matu",
+ "type": "restaurant",
+ "category": "Wagyu steakhouse",
+ "address": "239 S Beverly Dr, Beverly Hills, CA 90212",
+ "lat": 34.06388,
+ "lng": -118.39917,
+ "phone": "+1-424-317-5031",
+ "menu_url": "https://www.matusteak.com/"
+ },
+ {
+ "name": "Porta Via",
+ "type": "restaurant",
+ "category": "Californian bistro",
+ "address": "424 N Canon Dr, Beverly Hills, CA 90210",
+ "lat": 34.07116,
+ "lng": -118.4013,
+ "phone": "+1-310-274-6534",
+ "menu_url": "https://portaviarestaurants.com/beverly-hills/"
+ },
+ {
+ "name": "Via Alloro",
+ "type": "restaurant",
+ "category": "Italian",
+ "address": "301 N Canon Dr, Beverly Hills, CA 90210",
+ "lat": 34.06924,
+ "lng": -118.39982,
+ "phone": "+1-310-275-2900",
+ "menu_url": "https://www.viaalloro.com/"
+ },
+ {
+ "name": "Culina, Four Seasons",
+ "type": "restaurant",
+ "category": "Modern Italian",
+ "address": "300 S Doheny Dr, Los Angeles, CA 90048",
+ "lat": 34.07295,
+ "lng": -118.38921,
+ "phone": "+1-310-860-4000",
+ "menu_url": "https://www.culinarestaurant.com/"
+ },
+ {
+ "name": "The Blvd",
+ "type": "restaurant",
+ "category": "Californian / New American",
+ "address": "9500 Wilshire Blvd, Beverly Hills, CA 90212",
+ "lat": 34.06689,
+ "lng": -118.40037,
+ "phone": "+1-310-385-3901",
+ "menu_url": "https://www.theblvdrestaurant.com/"
+ },
+ {
+ "name": "Dante Beverly Hills",
+ "type": "restaurant",
+ "category": "Italian / rooftop",
+ "address": "225 N Canon Dr, Beverly Hills, CA 90210",
+ "lat": 34.06779,
+ "lng": -118.39894,
+ "phone": "+1-310-860-7989",
+ "menu_url": "https://www.dantebeverlyhills.com/menus-1/"
+ },
+ {
+ "name": "Sugarfish Beverly Hills",
+ "type": "restaurant",
+ "category": "Sushi",
+ "address": "212 N Canon Dr, Beverly Hills, CA 90210",
+ "lat": 34.06815,
+ "lng": -118.39829,
+ "phone": "+1-310-276-6900",
+ "menu_url": "https://www.sugarfishsushi.com/"
+ },
+ {
+ "name": "Tempura Endo",
+ "type": "restaurant",
+ "category": "Japanese tempura",
+ "address": "9777 S Santa Monica Blvd, Beverly Hills, CA 90210",
+ "lat": 34.07171,
+ "lng": -118.40251,
+ "phone": "+1-310-708-0224",
+ "menu_url": "http://www.beverlyhills-endo.com/"
+ },
+ {
+ "name": "Frida Beverly Hills",
+ "type": "restaurant",
+ "category": "Mexican",
+ "address": "236 S Beverly Dr, Beverly Hills, CA 90212",
+ "lat": 34.06393,
+ "lng": -118.39897,
+ "phone": "+1-310-278-7666",
+ "menu_url": "https://www.fridarestaurant.com/"
+ },
+ {
+ "name": "The Honor Bar",
+ "type": "restaurant",
+ "category": "American / burgers",
+ "address": "122 S Beverly Dr, Beverly Hills, CA 90212",
+ "lat": 34.06652,
+ "lng": -118.39896,
+ "phone": "+1-310-550-0292",
+ "menu_url": "https://honorbar.com/locations/beverlyhills/"
+ },
+ {
+ "name": "Marea",
+ "type": "restaurant",
+ "category": "Coastal Italian / seafood",
+ "address": "430 N Camden Dr, Beverly Hills, CA 90210",
+ "lat": 34.06936,
+ "lng": -118.40434,
+ "phone": "+1-310-620-8463",
+ "menu_url": "https://www.marearestaurant.com/beverly-hills/menu"
+ },
+ {
+ "name": "Ocean Prime",
+ "type": "restaurant",
+ "category": "Steak & seafood",
+ "address": "9595 Wilshire Blvd, Beverly Hills, CA 90212",
+ "lat": 34.06747,
+ "lng": -118.40261,
+ "phone": "+1-310-859-4818",
+ "menu_url": "https://ocean-prime.com/locations-menus/beverly-hills/menus/"
+ },
+ {
+ "name": "Azur Beverly Hills",
+ "type": "restaurant",
+ "category": "French Mediterranean",
+ "address": "453 N Canon Dr, Beverly Hills, CA 90210",
+ "lat": 34.0715,
+ "lng": -118.40189,
+ "phone": "+1-310-933-9955",
+ "menu_url": "https://www.azurbeverlyhills.com/"
+ },
+ {
+ "name": "Bad Roman",
+ "type": "restaurant",
+ "category": "Italian",
+ "address": "267 N Canon Dr, Beverly Hills, CA 90210",
+ "lat": 34.06889,
+ "lng": -118.39947,
+ "phone": "+1-310-861-4040",
+ "menu_url": "https://www.badromanrestaurants.com/menus-beverly-hills/"
+ },
+ {
+ "name": "88 Club",
+ "type": "restaurant",
+ "category": "Modern Chinese",
+ "address": "9737 S Santa Monica Blvd, Beverly Hills, CA 90210",
+ "lat": 34.07161,
+ "lng": -118.40241,
+ "phone": "+1-310-968-9955",
+ "menu_url": "https://www.88clubbh.com/"
+ },
+ {
+ "name": "Fogo de Chao",
+ "type": "restaurant",
+ "category": "Brazilian steakhouse",
+ "address": "133 N La Cienega Blvd, Beverly Hills, CA 90211",
+ "lat": 34.06867,
+ "lng": -118.3766,
+ "phone": "+1-310-289-7755",
+ "menu_url": "https://fogodechao.com/location/beverly-hills/"
+ },
+ {
+ "name": "The Rooftop by JG",
+ "type": "restaurant",
+ "category": "New American / rooftop",
+ "address": "9850 Wilshire Blvd, Beverly Hills, CA 90210",
+ "lat": 34.06658,
+ "lng": -118.41164,
+ "phone": "+1-310-860-6566",
+ "menu_url": "https://waldorfastoriabeverlyhills.com/dining/"
+ },
+ {
+ "name": "The Hideaway",
+ "type": "restaurant",
+ "category": "Mexican steakhouse",
+ "address": "421 N Rodeo Dr, Beverly Hills, CA 90210",
+ "lat": 34.06963,
+ "lng": -118.40366,
+ "phone": "+1-310-974-8020",
+ "menu_url": "https://www.thehideawaybeverlyhills.com/"
+ },
+ {
+ "name": "The Terrace at The Maybourne",
+ "type": "restaurant",
+ "category": "Californian",
+ "address": "225 N Canon Dr, Beverly Hills, CA 90210",
+ "lat": 34.06775,
+ "lng": -118.3989,
+ "phone": "+1-310-860-7886",
+ "menu_url": "https://www.maybourne.com/en/hotels/the-maybourne-beverly-hills/restaurants-bars/the-terrace"
+ },
+ {
+ "name": "Miura",
+ "type": "restaurant",
+ "category": "Sushi omakase",
+ "address": "218 N Rodeo Dr, Beverly Hills, CA 90210",
+ "lat": 34.06742,
+ "lng": -118.40059,
+ "phone": "+1-424-204-9584",
+ "menu_url": "https://miura-beverlyhills.com/"
+ },
+ {
+ "name": "South Beverly Grill",
+ "type": "restaurant",
+ "category": "American",
+ "address": "122 S Beverly Dr, Beverly Hills, CA 90212",
+ "lat": 34.0665,
+ "lng": -118.39894,
+ "phone": "+1-310-550-0242",
+ "menu_url": "https://southbeverlygrill.com/menu/"
+ },
+ {
+ "name": "Piccolo Paradiso",
+ "type": "restaurant",
+ "category": "Italian",
+ "address": "150 S Beverly Dr, Beverly Hills, CA 90212",
+ "lat": 34.06546,
+ "lng": -118.39891,
+ "phone": "+1-310-271-0030",
+ "menu_url": "https://piccoloparadisobeverlyhills.com/our-menu/"
+ },
+ {
+ "name": "Kazan",
+ "type": "restaurant",
+ "category": "Japanese ramen",
+ "address": "111 N La Cienega Blvd, Beverly Hills, CA 90211",
+ "lat": 34.06747,
+ "lng": -118.37636,
+ "phone": "+1-424-512-9101",
+ "menu_url": "https://kazanbeverlyhills.com/"
+ },
+ {
+ "name": "Xi'an",
+ "type": "restaurant",
+ "category": "Chinese",
+ "address": "362 N Canon Dr, Beverly Hills, CA 90210",
+ "lat": 34.07014,
+ "lng": -118.40026,
+ "phone": "+1-310-275-3345",
+ "menu_url": "https://www.xianbeverlyhills.com/menu"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/assets/splash.png b/assets/splash.png
new file mode 100644
index 0000000..b3887df
Binary files /dev/null and b/assets/splash.png differ
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 0000000..9d89e13
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function (api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/components/BHVWebView.tsx b/components/BHVWebView.tsx
new file mode 100644
index 0000000..cbd6afe
--- /dev/null
+++ b/components/BHVWebView.tsx
@@ -0,0 +1,192 @@
+import React, { useRef, useState, useCallback } from 'react';
+import {
+ View,
+ StyleSheet,
+ ActivityIndicator,
+ Text,
+ TouchableOpacity,
+ Share,
+ Platform,
+} from 'react-native';
+import { WebView, WebViewNavigation } from 'react-native-webview';
+import { Linking } from 'react-native';
+import { Colors } from '@/constants/Colors';
+import { useNetworkStatus } from '@/hooks/useNetworkStatus';
+
+const BASE_DOMAIN = 'beverlyhillsvideos.com';
+const BASE_URL = `https://${BASE_DOMAIN}`;
+
+function isInternalUrl(url: string): boolean {
+ try {
+ const parsed = new URL(url);
+ return (
+ parsed.hostname === BASE_DOMAIN ||
+ parsed.hostname === `www.${BASE_DOMAIN}`
+ );
+ } catch {
+ return false;
+ }
+}
+
+interface Props {
+ initialUrl: string;
+ shareTitle?: string;
+}
+
+export default function BHVWebView({ initialUrl, shareTitle }: Props) {
+ const webViewRef = useRef<WebView>(null);
+ const [loading, setLoading] = useState(true);
+ const [canGoBack, setCanGoBack] = useState(false);
+ const { isConnected } = useNetworkStatus();
+
+ const handleNavigationChange = useCallback((nav: WebViewNavigation) => {
+ setCanGoBack(nav.canGoBack);
+ const url = nav.url;
+
+ // External links: tel:, mailto:, off-domain — open in system browser
+ if (
+ url.startsWith('tel:') ||
+ url.startsWith('mailto:') ||
+ url.startsWith('sms:') ||
+ (!isInternalUrl(url) && !url.startsWith('about:') && !url.startsWith('data:'))
+ ) {
+ // Prevent WebView navigation for external URLs
+ // We use onShouldStartLoadWithRequest for this (see below)
+ }
+ }, []);
+
+ const handleShouldStartLoad = useCallback(
+ ({ url }: { url: string }): boolean => {
+ if (
+ url.startsWith('tel:') ||
+ url.startsWith('mailto:') ||
+ url.startsWith('sms:')
+ ) {
+ Linking.openURL(url).catch(() => {});
+ return false;
+ }
+ if (!url.startsWith('about:') && !url.startsWith('data:') && !isInternalUrl(url)) {
+ Linking.openURL(url).catch(() => {});
+ return false;
+ }
+ return true;
+ },
+ []
+ );
+
+ const handleShare = useCallback(async () => {
+ try {
+ await Share.share({
+ title: shareTitle ?? 'Beverly Hills Videos',
+ message: `${shareTitle ?? 'Beverly Hills Videos'} — ${BASE_URL}`,
+ url: BASE_URL,
+ });
+ } catch {
+ // user dismissed
+ }
+ }, [shareTitle]);
+
+ if (!isConnected) {
+ return (
+ <View style={styles.offlineContainer}>
+ <Text style={styles.offlineIcon}>wifi_off</Text>
+ <Text style={styles.offlineTitle}>No Connection</Text>
+ <Text style={styles.offlineBody}>
+ Connect to the internet and tap Retry to load Beverly Hills Videos.
+ </Text>
+ <TouchableOpacity
+ style={styles.retryButton}
+ onPress={() => webViewRef.current?.reload()}
+ activeOpacity={0.8}
+ >
+ <Text style={styles.retryText}>Retry</Text>
+ </TouchableOpacity>
+ </View>
+ );
+ }
+
+ return (
+ <View style={styles.container}>
+ {/* Share header button injected via parent layout, but we expose a method */}
+ <WebView
+ ref={webViewRef}
+ source={{ uri: initialUrl }}
+ style={styles.webview}
+ onLoadStart={() => setLoading(true)}
+ onLoadEnd={() => setLoading(false)}
+ onNavigationStateChange={handleNavigationChange}
+ onShouldStartLoadWithRequest={handleShouldStartLoad}
+ pullToRefreshEnabled
+ allowsBackForwardNavigationGestures={Platform.OS === 'ios'}
+ allowsInlineMediaPlayback
+ mediaPlaybackRequiresUserAction={false}
+ javaScriptEnabled
+ domStorageEnabled
+ startInLoadingState={false}
+ decelerationRate="normal"
+ originWhitelist={['https://*', 'http://*', 'tel:*', 'mailto:*']}
+ contentInsetAdjustmentBehavior="automatic"
+ />
+ {loading && (
+ <View style={styles.loadingOverlay} pointerEvents="none">
+ <ActivityIndicator size="large" color={Colors.green} />
+ </View>
+ )}
+ </View>
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: Colors.cream,
+ },
+ webview: {
+ flex: 1,
+ backgroundColor: Colors.cream,
+ },
+ loadingOverlay: {
+ ...StyleSheet.absoluteFillObject,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: Colors.cream,
+ },
+ offlineContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: Colors.cream,
+ padding: 32,
+ },
+ offlineIcon: {
+ fontSize: 48,
+ marginBottom: 16,
+ color: Colors.textSecondary,
+ },
+ offlineTitle: {
+ fontSize: 22,
+ fontWeight: '600',
+ color: Colors.ink,
+ marginBottom: 8,
+ textAlign: 'center',
+ },
+ offlineBody: {
+ fontSize: 15,
+ color: Colors.textSecondary,
+ textAlign: 'center',
+ lineHeight: 22,
+ marginBottom: 32,
+ },
+ retryButton: {
+ backgroundColor: Colors.green,
+ paddingVertical: 14,
+ paddingHorizontal: 40,
+ borderRadius: 8,
+ },
+ retryText: {
+ color: '#fff',
+ fontSize: 16,
+ fontWeight: '600',
+ letterSpacing: 0.5,
+ },
+});
diff --git a/components/FavoritesModal.tsx b/components/FavoritesModal.tsx
new file mode 100644
index 0000000..7ca4201
--- /dev/null
+++ b/components/FavoritesModal.tsx
@@ -0,0 +1,206 @@
+import React from 'react';
+import {
+ Modal,
+ View,
+ Text,
+ TouchableOpacity,
+ FlatList,
+ StyleSheet,
+ SafeAreaView,
+ Linking,
+} from 'react-native';
+import { Colors } from '@/constants/Colors';
+import { MapLocation } from '@/types';
+
+interface Props {
+ visible: boolean;
+ onClose: () => void;
+ locations: MapLocation[];
+ favorites: Set<string>;
+ onToggleFavorite: (name: string) => void;
+}
+
+export default function FavoritesModal({
+ visible,
+ onClose,
+ locations,
+ favorites,
+ onToggleFavorite,
+}: Props) {
+ const favorited = locations.filter((loc) => favorites.has(loc.name));
+
+ return (
+ <Modal
+ visible={visible}
+ animationType="slide"
+ presentationStyle="pageSheet"
+ onRequestClose={onClose}
+ >
+ <SafeAreaView style={styles.safeArea}>
+ <View style={styles.header}>
+ <Text style={styles.heading}>Favorites</Text>
+ <TouchableOpacity onPress={onClose} style={styles.doneBtn} hitSlop={8}>
+ <Text style={styles.doneText}>Done</Text>
+ </TouchableOpacity>
+ </View>
+
+ {favorited.length === 0 ? (
+ <View style={styles.empty}>
+ <Text style={styles.emptyIcon}>heart</Text>
+ <Text style={styles.emptyTitle}>No favorites yet</Text>
+ <Text style={styles.emptyBody}>
+ Tap the heart on any restaurant to save it here.
+ </Text>
+ </View>
+ ) : (
+ <FlatList
+ data={favorited}
+ keyExtractor={(item) => item.name}
+ contentContainerStyle={styles.list}
+ ItemSeparatorComponent={() => <View style={styles.separator} />}
+ renderItem={({ item }) => (
+ <View style={styles.row}>
+ <View style={styles.rowInfo}>
+ <Text style={styles.rowName}>{item.name}</Text>
+ <Text style={styles.rowCategory}>{item.category}</Text>
+ <Text style={styles.rowAddress} numberOfLines={1}>
+ {item.address}
+ </Text>
+ </View>
+ <View style={styles.rowActions}>
+ {item.phone ? (
+ <TouchableOpacity
+ style={styles.actionBtn}
+ onPress={() => Linking.openURL(`tel:${item.phone}`)}
+ >
+ <Text style={styles.actionText}>Call</Text>
+ </TouchableOpacity>
+ ) : null}
+ <TouchableOpacity
+ style={styles.heartBtn}
+ onPress={() => onToggleFavorite(item.name)}
+ hitSlop={6}
+ >
+ <Text style={styles.heart}>heart_filled</Text>
+ </TouchableOpacity>
+ </View>
+ </View>
+ )}
+ />
+ )}
+ </SafeAreaView>
+ </Modal>
+ );
+}
+
+const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ backgroundColor: Colors.cream,
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 20,
+ paddingVertical: 14,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: Colors.border,
+ backgroundColor: Colors.cream,
+ },
+ heading: {
+ flex: 1,
+ fontSize: 20,
+ fontWeight: '700',
+ color: Colors.ink,
+ letterSpacing: 0.2,
+ },
+ doneBtn: {
+ paddingVertical: 4,
+ },
+ doneText: {
+ color: Colors.green,
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ empty: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 40,
+ },
+ emptyIcon: {
+ fontSize: 40,
+ color: Colors.gold,
+ marginBottom: 12,
+ },
+ emptyTitle: {
+ fontSize: 20,
+ fontWeight: '600',
+ color: Colors.ink,
+ marginBottom: 8,
+ },
+ emptyBody: {
+ fontSize: 15,
+ color: Colors.textSecondary,
+ textAlign: 'center',
+ lineHeight: 22,
+ },
+ list: {
+ paddingVertical: 8,
+ },
+ separator: {
+ height: StyleSheet.hairlineWidth,
+ backgroundColor: Colors.border,
+ marginLeft: 20,
+ },
+ row: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 20,
+ paddingVertical: 14,
+ backgroundColor: Colors.cream,
+ },
+ rowInfo: {
+ flex: 1,
+ marginRight: 12,
+ },
+ rowName: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: Colors.ink,
+ marginBottom: 2,
+ },
+ rowCategory: {
+ fontSize: 13,
+ color: Colors.gold,
+ marginBottom: 2,
+ fontWeight: '500',
+ },
+ rowAddress: {
+ fontSize: 12,
+ color: Colors.textSecondary,
+ },
+ rowActions: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 10,
+ },
+ actionBtn: {
+ backgroundColor: Colors.green,
+ paddingVertical: 6,
+ paddingHorizontal: 14,
+ borderRadius: 6,
+ },
+ actionText: {
+ color: '#fff',
+ fontSize: 13,
+ fontWeight: '600',
+ },
+ heartBtn: {
+ padding: 4,
+ },
+ heart: {
+ fontSize: 20,
+ color: Colors.gold,
+ },
+});
diff --git a/components/FilmModal.tsx b/components/FilmModal.tsx
new file mode 100644
index 0000000..6d5f7da
--- /dev/null
+++ b/components/FilmModal.tsx
@@ -0,0 +1,89 @@
+import React from 'react';
+import {
+ Modal,
+ View,
+ StyleSheet,
+ TouchableOpacity,
+ Text,
+ SafeAreaView,
+ Platform,
+ StatusBar,
+} from 'react-native';
+import { WebView } from 'react-native-webview';
+import { Colors } from '@/constants/Colors';
+
+interface Props {
+ visible: boolean;
+ url: string;
+ title: string;
+ onClose: () => void;
+}
+
+export default function FilmModal({ visible, url, title, onClose }: Props) {
+ return (
+ <Modal
+ visible={visible}
+ animationType="slide"
+ presentationStyle="pageSheet"
+ onRequestClose={onClose}
+ >
+ <SafeAreaView style={styles.safeArea}>
+ <View style={styles.header}>
+ <Text style={styles.title} numberOfLines={1}>
+ {title}
+ </Text>
+ <TouchableOpacity onPress={onClose} style={styles.closeBtn} hitSlop={8}>
+ <Text style={styles.closeText}>Done</Text>
+ </TouchableOpacity>
+ </View>
+ <WebView
+ source={{ uri: url }}
+ style={styles.webview}
+ allowsInlineMediaPlayback
+ mediaPlaybackRequiresUserAction={false}
+ javaScriptEnabled
+ domStorageEnabled
+ originWhitelist={['https://*', 'http://*']}
+ contentInsetAdjustmentBehavior="automatic"
+ />
+ </SafeAreaView>
+ </Modal>
+ );
+}
+
+const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ backgroundColor: Colors.ink,
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ backgroundColor: Colors.ink,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: 'rgba(255,255,255,0.15)',
+ },
+ title: {
+ flex: 1,
+ color: Colors.cream,
+ fontSize: 16,
+ fontWeight: '600',
+ letterSpacing: 0.3,
+ marginRight: 12,
+ },
+ closeBtn: {
+ paddingVertical: 4,
+ paddingHorizontal: 2,
+ },
+ closeText: {
+ color: Colors.gold,
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ webview: {
+ flex: 1,
+ backgroundColor: Colors.ink,
+ },
+});
diff --git a/constants/Colors.ts b/constants/Colors.ts
new file mode 100644
index 0000000..f0fcff9
--- /dev/null
+++ b/constants/Colors.ts
@@ -0,0 +1,21 @@
+export const Colors = {
+ cream: '#f6f3ec',
+ ink: '#151310',
+ green: '#1f3d30',
+ gold: '#b8935a',
+
+ // Map marker colors by location type
+ markerRestaurant: '#1f3d30',
+ markerStore: '#b8935a',
+ markerDoctor: '#1a2e4a',
+ markerLawyer: '#6b1a1a',
+
+ // UI
+ background: '#f6f3ec',
+ surface: '#ffffff',
+ border: '#e0dcd4',
+ textPrimary: '#151310',
+ textSecondary: '#5a5650',
+ tabActive: '#1f3d30',
+ tabInactive: '#9a9590',
+} as const;
diff --git a/eas.json b/eas.json
new file mode 100644
index 0000000..221f7bb
--- /dev/null
+++ b/eas.json
@@ -0,0 +1,36 @@
+{
+ "cli": {
+ "version": ">= 12.0.0",
+ "appVersionSource": "remote"
+ },
+ "build": {
+ "development": {
+ "developmentClient": true,
+ "distribution": "internal",
+ "ios": {
+ "simulator": true
+ }
+ },
+ "preview": {
+ "distribution": "internal",
+ "ios": {
+ "simulator": false
+ }
+ },
+ "production": {
+ "autoIncrement": true,
+ "ios": {
+ "resourceClass": "m-medium"
+ }
+ }
+ },
+ "submit": {
+ "production": {
+ "ios": {
+ "appleTeamId": "3VAV3KMNZK",
+ "appleId": "TODO_APPLE_ID_EMAIL",
+ "ascAppId": "TODO_ASC_APP_ID"
+ }
+ }
+ }
+}
diff --git a/expo-env.d.ts b/expo-env.d.ts
new file mode 100644
index 0000000..bf3c169
--- /dev/null
+++ b/expo-env.d.ts
@@ -0,0 +1,3 @@
+/// <reference types="expo/types" />
+
+// NOTE: This file should not be edited and should be in your git ignore
diff --git a/hooks/useFavorites.ts b/hooks/useFavorites.ts
new file mode 100644
index 0000000..5804f80
--- /dev/null
+++ b/hooks/useFavorites.ts
@@ -0,0 +1,43 @@
+import { useState, useEffect, useCallback } from 'react';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+const STORAGE_KEY = '@bhv:favorites';
+
+export function useFavorites() {
+ const [favorites, setFavorites] = useState<Set<string>>(new Set());
+ const [loaded, setLoaded] = useState(false);
+
+ useEffect(() => {
+ AsyncStorage.getItem(STORAGE_KEY)
+ .then((raw) => {
+ if (raw) {
+ setFavorites(new Set(JSON.parse(raw) as string[]));
+ }
+ })
+ .catch(() => {})
+ .finally(() => setLoaded(true));
+ }, []);
+
+ const toggle = useCallback(
+ async (name: string) => {
+ setFavorites((prev) => {
+ const next = new Set(prev);
+ if (next.has(name)) {
+ next.delete(name);
+ } else {
+ next.add(name);
+ }
+ AsyncStorage.setItem(STORAGE_KEY, JSON.stringify([...next])).catch(() => {});
+ return next;
+ });
+ },
+ []
+ );
+
+ const isFavorite = useCallback(
+ (name: string) => favorites.has(name),
+ [favorites]
+ );
+
+ return { favorites, isFavorite, toggle, loaded };
+}
diff --git a/hooks/useNetworkStatus.ts b/hooks/useNetworkStatus.ts
new file mode 100644
index 0000000..c0a6666
--- /dev/null
+++ b/hooks/useNetworkStatus.ts
@@ -0,0 +1,15 @@
+import { useState, useEffect } from 'react';
+import NetInfo from '@react-native-community/netinfo';
+
+export function useNetworkStatus() {
+ const [isConnected, setIsConnected] = useState<boolean>(true);
+
+ useEffect(() => {
+ const unsubscribe = NetInfo.addEventListener((state) => {
+ setIsConnected(state.isConnected ?? true);
+ });
+ return unsubscribe;
+ }, []);
+
+ return { isConnected };
+}
diff --git a/metro.config.js b/metro.config.js
new file mode 100644
index 0000000..32938e8
--- /dev/null
+++ b/metro.config.js
@@ -0,0 +1,5 @@
+const { getDefaultConfig } = require('expo/metro-config');
+
+const config = getDefaultConfig(__dirname);
+
+module.exports = config;
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..c03712f
--- /dev/null
+++ b/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "beverlyhillsvideos",
+ "version": "1.0.0",
+ "main": "expo-router/entry",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web",
+ "typecheck": "tsc --noEmit",
+ "lint": "expo lint"
+ },
+ "dependencies": {
+ "expo": "~57.0.0",
+ "expo-router": "~5.1.0",
+ "expo-status-bar": "~2.2.0",
+ "expo-splash-screen": "~0.30.0",
+ "expo-font": "~13.3.0",
+ "expo-linking": "~7.1.0",
+ "expo-constants": "~17.1.0",
+ "react": "19.0.0",
+ "react-native": "0.79.5",
+ "react-native-webview": "13.12.5",
+ "react-native-maps": "1.20.1",
+ "@react-native-async-storage/async-storage": "2.1.2",
+ "@react-native-community/netinfo": "11.4.1",
+ "react-native-safe-area-context": "4.14.0",
+ "react-native-screens": "~4.10.0",
+ "@react-navigation/bottom-tabs": "^7.3.10"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.0",
+ "@types/react": "~19.0.0",
+ "@types/react-native": "~0.73.0",
+ "typescript": "~5.8.0",
+ "expo-module-scripts": "^4.0.0"
+ }
+}
diff --git a/store/LISTING.md b/store/LISTING.md
new file mode 100644
index 0000000..a2609be
--- /dev/null
+++ b/store/LISTING.md
@@ -0,0 +1,264 @@
+# Beverly Hills Videos — App Store Connect Listing & Submission-Readiness Package
+
+**App:** Beverly Hills Videos
+**Bundle ID:** `com.abrams.beverlyhillsvideos`
+**Developer / Seller:** Steve Abrams (Abrams)
+**Platform:** iOS (iPhone + iPad — `supportsTablet: true`)
+**Version:** 1.0.0 (build 1)
+**Price:** Free (ad-supported — Google AdSense inside web content)
+**Prepared:** 2026-08-06
+
+> Character limits verified against current (2026) App Store Connect requirements:
+> App Name ≤30, Subtitle ≤30, Promotional Text ≤170, Keywords ≤100, Description ≤4,000.
+> All fields below were counted with `wc -c` and are within limit. Sources listed at the end.
+
+---
+
+## 1. App Name (≤30 characters)
+
+**PRIMARY (use this):**
+> `Beverly Hills Videos` — **20 chars**
+
+**Alternatives:**
+1. `Beverly Hills Videos: Guide` — **27 chars**
+2. `Beverly Hills Video Guide` — **25 chars**
+
+Rationale: the primary matches the installed Expo `name` and the domain, keeping brand + store + push notification identity consistent. The alternatives buy extra keyword weight ("Guide") if ASO testing shows the bare name isn't ranking for "guide."
+
+---
+
+## 2. Subtitle (≤30 characters)
+
+**PRIMARY (use this):**
+> `Films, Dining Map & City Guide` — **30 chars** (exactly at limit)
+
+**Alternatives:**
+1. `Restaurants, Films & City Map` — **29 chars**
+2. `Your Insider Guide to 90210` — **27 chars**
+
+Rationale: the subtitle is an indexed field, so it does double duty as ad copy AND keyword real estate. The primary front-loads the three pillars (films, map, guide) that differentiate the app from a plain restaurant list, and captures "dining," "map," and "guide" search terms not already spent in the name.
+
+---
+
+## 3. Promotional Text (≤170 characters)
+
+> `Discover Beverly Hills like an insider: original short films on 44 top restaurants, a tap-to-call dining map, menus, and luxury shopping & city guides. Free.`
+> — **157 chars**
+
+Rationale: Promotional text is editable WITHOUT resubmitting a build, so it's the seasonal/announcement lever — swap it for "New films added this month" or event tie-ins without a review cycle. It is NOT indexed for search, so it's pure conversion copy.
+
+---
+
+## 4. Keywords (≤100 characters, comma-separated, no spaces after commas)
+
+> `beverly hills,90210,restaurants,dining,food,menus,LA,travel,guide,map,luxury,shopping,eats,cafe`
+> — **95 chars**
+
+Notes on optimization:
+- No spaces after commas — Apple treats each comma-token as a term and does NOT count trailing spaces as useful; removing them frees characters for more terms.
+- Does not repeat the full app name "Beverly Hills Videos." (The term `beverly hills` is the single highest-value search phrase for this app and is a distinct phrase, not the verbatim app name; Apple indexes the name + keyword fields together, so if you want to be maximally conservative you can drop `beverly hills,` from keywords and reclaim 14 chars for e.g. `dinner,reservations` — the name already carries "Beverly Hills.")
+- Singular/plural: Apple auto-handles simple plurals, so we use `restaurant`→`restaurants` once and rely on Apple's stemming rather than wasting chars on both.
+- `90210` captures the iconic ZIP as a branded search term.
+- Conservative-swap alternative (drops the app-name overlap entirely):
+ `90210,restaurants,dining,food,menus,cuisine,LA,travel,guide,map,luxury,shopping,eats,nearby` — **91 chars**
+
+---
+
+## 5. Description (≤4,000 characters)
+
+> Character count of the block below: **~2,180 chars** (well within 4,000; room to extend with a films roster if desired).
+
+```
+Beverly Hills Videos is the insider's guide to the most glamorous square mile in the world — beautifully filmed, effortless to use, and made for the way you actually explore a city.
+
+Watch, then go. Instead of scrolling through look-alike listings, you'll watch original short films on 44 of Beverly Hills' top restaurants — the room, the plates, the mood — so you know exactly where you want to be tonight before you ever leave home.
+
+WHAT'S INSIDE
+
+• 44 original restaurant films — short, cinematic looks at the city's best dining, from power-lunch icons to hidden late-night gems.
+
+• A native, tappable map — every restaurant pinned on a fast, real map. Tap a pin to call the restaurant or open its menu in a single touch. No pinch-and-zoom guesswork.
+
+• Luxury shopping & city guides — Rodeo Drive and beyond, plus curated guides to what makes Beverly Hills, Beverly Hills.
+
+• Favorites — save the places you love with a tap and find them again instantly, even offline.
+
+• Works offline — your saved spots and core guides stay with you when the signal drops.
+
+• Share in a tap — send a restaurant, film, or guide straight to a friend.
+
+WHY YOU'LL LOVE IT
+
+It's the difference between reading about a place and seeing it. Beverly Hills Videos turns "where should we eat?" into a two-minute film and a one-tap call. Whether you live in the 90210, you're flying in for the weekend, or you're planning the perfect night out, this is the guide that shows you first and gets you there fastest.
+
+Elegant. Fast. Free. The way Beverly Hills should feel.
+
+Beverly Hills Videos is a free, ad-supported app. Films, map, favorites, and guides are all included at no cost.
+
+Download Beverly Hills Videos and see the city before you go.
+```
+
+---
+
+## 6. Category — Primary + Secondary
+
+**Recommendation:**
+- **Primary: Travel**
+- **Secondary: Food & Drink**
+
+**The argument (Travel vs. Food & Drink):**
+
+The app is a *destination guide* first and a restaurant tool second. Its scope is broader than food — it bundles luxury-shopping guides, city guides, and a map-driven "explore this place" experience. That framing sits squarely in **Travel**, where the competitive set is city/destination guides (the app wins on production value — actual films — vs. text listings). Travel also has a lighter, less brutally saturated top-charts race than Food & Drink, which is dominated by delivery giants (DoorDash, Uber Eats, Yelp) an indie guide cannot outrank.
+
+**Food & Drink as secondary** still captures the high-intent "restaurants / dining" searchers, since 44 restaurant films are the app's beating heart. This gives you presence in both category browse surfaces.
+
+Counter-argument acknowledged: if post-launch analytics show the overwhelming majority of engagement is the restaurant films + call/menu actions (i.e., users treat it as a dining app, not a trip-planning app), **flip them** — Primary: Food & Drink, Secondary: Travel. Category is changeable without a new binary, so treat this as an A/B lever, not a permanent decision. Start Travel because the app's *promise* (the whole city) is broader than its busiest feature.
+
+---
+
+## 7. Age Rating
+
+**Recommended rating: 4+.**
+
+There is no objectionable content: no violence, no mature/suggestive themes, no profanity, no gambling, no user-generated content, no unrestricted web access to arbitrary sites (the WebView wraps the app's own curated content). Answer the App Store Connect age-rating questionnaire as follows:
+
+| Questionnaire item | Answer |
+|---|---|
+| Cartoon or Fantasy Violence | None |
+| Realistic Violence | None |
+| Sexual Content or Nudity | None |
+| Profanity or Crude Humor | None |
+| Alcohol, Tobacco, or Drug Use or References | **None** (restaurant films may show a bar/wine on-screen incidentally; this is ambient, not a *reference or depiction* the questionnaire targets — if any film prominently features cocktails/bar culture, answer **Infrequent/Mild** to be safe, which still yields 4+/12+ depending on Apple's current matrix. Review footage before answering.) |
+| Horror/Fear Themes | None |
+| Mature/Suggestive Themes | None |
+| Medical/Treatment Info | None |
+| Gambling (simulated or real) | None |
+| Contests | None |
+| Unrestricted Web Access | **No** — the WebView loads only the app's own beverlyhillsvideos.com content, not the open web. |
+| User-Generated Content | None |
+
+**Caveat to confirm before submitting:** the "Unrestricted Web Access" toggle. Because the app embeds a WebView, a reviewer will check whether users can navigate to arbitrary external sites. If external links inside the web content can escape to the open web, either (a) answer **Yes** to Unrestricted Web Access (forces 17+), or (b) constrain outbound navigation to the app's own domain + intended call/menu deep links so you can honestly answer **No** and keep 4+. **Recommended: constrain navigation and keep 4+.**
+
+---
+
+## 8. App Privacy — "Nutrition Label"
+
+The binary has NO login and NO account. BUT two things drive the privacy label: (a) **Google AdSense inside the WebView** serves third-party ads, and (b) the app requests **location** (`NSLocationWhenInUseUsageDescription`) to show nearby restaurants on the native map. Both must be disclosed. Under-disclosing third-party ad SDKs/tags is one of the most common rejection/removal reasons — over-disclose rather than under-disclose.
+
+### Declare "Data Used to Track You" — YES
+
+Google AdSense sets advertising/identifier data that is combined with third-party data for targeted advertising. That is Apple's definition of **tracking**. Declare:
+
+| Data type | Category | Linked to user? | Used to track? | Why |
+|---|---|---|---|---|
+| **Device ID / Advertising identifier** | Identifiers | No (no account) | **Yes** | AdSense uses ad/device identifiers to serve and measure ads. |
+| **Product Interaction / Usage Data** | Usage Data | No | **Yes** | Ad impression/click and page-view signals sent to the ad network. |
+| **Coarse/Precise Location** | Location | No | **No** (functional) | Used on-device to center the map / show nearby spots. Only declare as *tracking* if location is passed to the ad network — keep it functional-only. |
+| **Crash Data / Diagnostics** | Diagnostics | No | No | If any analytics/crash SDK is present (declare only if actually integrated). |
+
+### App Tracking Transparency (ATT) is REQUIRED
+
+Because you declare tracking, you MUST present the **ATT prompt** (`AppTrackingTransparency`) before any tracking identifier is used, and you must not access the IDFA until the user opts in. Add `NSUserTrackingUsageDescription` to Info.plist (e.g., "We use your data to show more relevant ads that keep this app free."). Google's ad tags must respect the ATT result. Without ATT, tracking-declared apps get rejected under Guideline 5.1.2.
+
+### Privacy Manifest (`PrivacyInfo.xcprivacy`) — REQUIRED
+
+Apple requires a privacy manifest declaring the data your app AND embedded third-party SDKs collect, plus reasons for any "required-reason" APIs. Ensure the AdSense/ad integration's collection is reflected. If you add Google's Mobile Ads SDK (rather than pure web AdSense), ship its bundled privacy manifest.
+
+### Summary of what to disclose and why
+- **Identifiers → Yes, used to track** (AdSense advertising ID).
+- **Usage Data → Yes, used to track** (ad interaction/impressions).
+- **Location → collected, functional only, NOT tracking** (map nearby).
+- **No "Data Linked to You"** — there's no account, so nothing links to an identity; identifiers/usage sit under "Data Not Linked to You" for the collection question, but STILL flagged under "Used to Track You."
+- Present **ATT**, ship the **privacy manifest**, and add **NSUserTrackingUsageDescription**.
+
+> If you would rather NOT show the ATT prompt and NOT declare tracking, you must configure AdSense/ad serving to **non-personalized ads only** and pass no cross-app identifiers — then Identifiers/Usage can be declared "Not used to track." That's a product decision; the safe default for an AdSense-monetized app is: declare tracking + show ATT.
+
+---
+
+## 9. Support URL + Marketing URL
+
+- **Support URL:** `https://beverlyhillsvideos.com/support`
+ (Create this page: a short contact form or a `mailto:` + FAQ. Apple requires a working support URL; a bare homepage risks a Guideline 1.5 metadata rejection.)
+- **Marketing URL (optional but recommended):** `https://beverlyhillsvideos.com`
+- **Privacy Policy URL (REQUIRED for any app that collects data / serves ads):** `https://beverlyhillsvideos.com/privacy`
+ This is mandatory here because AdSense collects data. The privacy policy must name Google as a third-party ad partner, describe advertising identifiers/usage collection, and link to Google's ad policies.
+
+---
+
+## 10. "What's New" (Version 1.0)
+
+> ```
+> Welcome to Beverly Hills Videos — your insider guide to the 90210.
+>
+> • 44 original short films on the city's top restaurants
+> • A native map: tap a pin to call or open the menu
+> • Save your favorites — available offline
+> • Luxury shopping and city guides
+> • Share any spot in a tap
+>
+> Thanks for downloading. We're just getting started — more films and guides are on the way.
+> ```
+
+---
+
+## 11. Guideline 4.2 Justification Memo (for the App Review notes)
+
+> Paste this into **App Review Information → Notes** in App Store Connect.
+
+**Beverly Hills Videos is a native iOS application, not a repackaged website.** While some rich media (the restaurant films and certain guide content) is delivered from beverlyhillsvideos.com through an embedded web component, the app's structure, navigation, and core interactions are all native and could not be reproduced by simply loading a URL in Safari. The app is built with React Native and presents a native tab-bar architecture (Films, Map, Favorites, Guides), so users move through the experience using platform-standard native navigation rather than web page loads.
+
+**The centerpiece is a native map.** Restaurants are rendered on a native `react-native-maps` map (MapKit), with tappable native pins that trigger native device actions — one tap places a phone call to the restaurant, opens its menu, or saves it to Favorites. This map, its pins, and the call/menu/favorite actions are implemented with native iOS APIs (CoreLocation for "show me what's nearby," the system dialer for calls), not web links, and are a primary reason a user installs the app rather than visiting the site.
+
+**The app adds substantial native functionality beyond any website:** favorites are persisted locally on the device so they survive relaunch and are available offline; core guides and saved content are cached for offline use so the app remains useful without a connection; the native iOS share sheet lets users send a restaurant, film, or guide to a friend; and the app respects platform conventions including App Tracking Transparency for its ad-supported model. In short, the website is one content source feeding a genuinely native, offline-capable, map-and-favorites app — it is the curated, insider guide to Beverly Hills delivered as a first-class iOS experience, which is exactly what Guideline 4.2 asks for.
+
+---
+
+## 12. Screenshot Plan
+
+**Required sizes (2026 App Store Connect):**
+- **6.9" iPhone (required)** — portrait **1320 × 2868** px (iPhone 16 Pro Max). This is the canonical required set; if you supply it, App Store Connect can down-scale for smaller devices.
+- **6.7" iPhone** — portrait **1290 × 2796** px (the fallback set App Store Connect uses if you don't ship 6.9").
+- **6.5" iPhone** — portrait **1284 × 2778** px (or 1242 × 2688). Only strictly required if you skip the 6.9" set; recommended to supply for older-device shoppers.
+- **iPad 13"** (because `supportsTablet: true`) — portrait **2064 × 2752** px. Required if the app is offered on iPad; if you'd rather not produce iPad art for v1.0, set the app to **iPhone-only** in App Store Connect to remove the iPad screenshot requirement.
+
+**Format:** sRGB PNG or JPEG, no transparency, exact pixel dimensions (not "close").
+
+**The 5 screenshots (same set, exported at each required size):**
+
+| # | What it shows | One-line caption |
+|---|---|---|
+| 1 | **Hero / Films tab** — a striking still from a restaurant film with the film grid below and the app wordmark. | "See Beverly Hills before you go." |
+| 2 | **A restaurant film playing** — full-bleed video frame with title + "Call" and "Menu" buttons visible. | "44 original films on the city's best tables." |
+| 3 | **The native map** — pins across Beverly Hills, one pin tapped showing the call/menu callout. | "Tap a pin. Call or open the menu instantly." |
+| 4 | **Favorites (with an offline badge)** — saved restaurants in a clean list/grid. | "Save your favorites — even offline." |
+| 5 | **Luxury shopping & city guides** — Rodeo Drive / guide imagery in the Guides tab. | "Your insider guide to shopping and the city." |
+
+**Production tips:**
+- Put the caption text ON the screenshot as an overlay band (screenshot caption text IS indexed by Apple, so use real search terms: "Beverly Hills," "restaurants," "map," "menu").
+- Lead with #1 and #3 — the film hero and the map are the two frames that most clearly say "this is not a website."
+- Keep the luxury aesthetic: dark/elegant background (`#151310` matches the app splash) with gold or off-white type.
+
+---
+
+## Pre-Submission Readiness Checklist
+
+- [ ] App icon 1024×1024 (no alpha, no rounded corners baked in).
+- [ ] Screenshots at required sizes (§12) uploaded.
+- [ ] Support URL live at `/support`; Privacy Policy live at `/privacy` (names Google/AdSense).
+- [ ] App Privacy answers entered (§8): Identifiers + Usage = **used to track**; Location = functional.
+- [ ] **ATT prompt** implemented + `NSUserTrackingUsageDescription` in Info.plist.
+- [ ] **`PrivacyInfo.xcprivacy`** privacy manifest present (app + ad SDK).
+- [ ] Age rating questionnaire completed (§7); confirm "Unrestricted Web Access = No" by constraining WebView navigation to own domain.
+- [ ] Guideline 4.2 memo (§11) pasted into App Review notes.
+- [ ] Export compliance: uses only standard HTTPS/exempt encryption → set `ITSAppUsesNonExemptEncryption = false` in Info.plist (avoids the per-build compliance prompt).
+- [ ] Category set: Travel (primary) / Food & Drink (secondary).
+- [ ] Build uploaded to App Store Connect and selected for the version (a green build ≠ an accepted upload — verify the build appears in ASC).
+- [ ] Contact info + demo notes: no login required — state "No account needed" in review notes.
+
+---
+
+## Sources (verified 2026)
+
+- App Store metadata character limits (Name 30 / Subtitle 30 / Promo 170 / Keywords 100 / Description 4000): [AppLaunchFlow](https://www.applaunchflow.com/blog/app-store-metadata-character-limits-2026), [AppScreenshotStudio](https://appscreenshotstudio.com/blog/app-store-metadata-for-indie-devs-title-subtitle-keywords-2026), [wordlimit.ai](https://wordlimit.ai/limits/app-store)
+- Screenshot required sizes (6.9" 1320×2868 required; 6.7" 1290×2796; 6.5" 1284×2778; iPad 13" 2064×2752): [AppLaunchFlow](https://www.applaunchflow.com/app-store-screenshot-sizes), [Screenhance](https://screenhance.com/blog/app-store-screenshot-dimensions-2026), [Newly](https://newly.app/guides/app-store-screenshot-sizes)
+- App Privacy / tracking / WebView / ATT: [Apple — User Privacy and Data Use](https://developer.apple.com/app-store/user-privacy-and-data-use/), [Apple — App Privacy Details](https://developer.apple.com/app-store/app-privacy-details/)
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..99b8474
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "expo/tsconfig.base",
+ "compilerOptions": {
+ "strict": true,
+ "paths": {
+ "@/*": ["./*"]
+ },
+ "baseUrl": "."
+ },
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ ".expo/types/**/*.d.ts",
+ "expo-env.d.ts"
+ ]
+}
diff --git a/types/index.ts b/types/index.ts
new file mode 100644
index 0000000..af078e3
--- /dev/null
+++ b/types/index.ts
@@ -0,0 +1,21 @@
+export type LocationType = 'restaurant' | 'store' | 'doctor' | 'lawyer';
+
+export interface MapLocation {
+ name: string;
+ type: LocationType;
+ category: string;
+ address: string;
+ lat: number;
+ lng: number;
+ phone?: string;
+ menu_url?: string;
+ slug?: string;
+}
+
+export interface Film {
+ name: string;
+ slug: string;
+ blurb: string;
+ src: string;
+ poster: string;
+}
(oldest)
·
back to Beverlyhillsvideos App
·
auto-save: 2026-08-06T08:19:59 (6 files) — app.json app/_lay 79d7e54 →