expotesting1 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +289 -0
  2. package/apps/expo-app/app.json +60 -0
  3. package/apps/expo-app/babel.config.js +7 -0
  4. package/apps/expo-app/index.js +6 -0
  5. package/apps/expo-app/package.json +46 -0
  6. package/apps/expo-app/src/App.jsx +37 -0
  7. package/apps/expo-app/src/navigation/RootNavigator.jsx +82 -0
  8. package/apps/expo-app/src/navigation/types.js +5 -0
  9. package/apps/expo-app/src/screens/HomeScreen.jsx +178 -0
  10. package/package.json +25 -0
  11. package/packages/animations/package.json +20 -0
  12. package/packages/animations/src/components/FadeView.jsx +42 -0
  13. package/packages/animations/src/components/ScaleView.jsx +28 -0
  14. package/packages/animations/src/components/SlideView.jsx +32 -0
  15. package/packages/animations/src/hooks/useFade.js +50 -0
  16. package/packages/animations/src/hooks/useScale.js +59 -0
  17. package/packages/animations/src/hooks/useSlide.js +53 -0
  18. package/packages/animations/src/index.js +21 -0
  19. package/packages/animations/src/reanimated.js +83 -0
  20. package/packages/core/package.json +22 -0
  21. package/packages/core/src/components/Button.jsx +92 -0
  22. package/packages/core/src/components/Card.jsx +47 -0
  23. package/packages/core/src/components/Container.jsx +61 -0
  24. package/packages/core/src/components/Input.jsx +83 -0
  25. package/packages/core/src/components/List.jsx +80 -0
  26. package/packages/core/src/components/index.js +9 -0
  27. package/packages/core/src/hooks/index.js +5 -0
  28. package/packages/core/src/hooks/useAsync.js +60 -0
  29. package/packages/core/src/hooks/useCounter.js +36 -0
  30. package/packages/core/src/hooks/useToggle.js +18 -0
  31. package/packages/core/src/index.js +5 -0
  32. package/packages/core/src/theme/index.js +67 -0
  33. package/packages/core/src/utils/helpers.js +93 -0
  34. package/packages/core/src/utils/index.js +10 -0
  35. package/packages/device/package.json +24 -0
  36. package/packages/device/src/hooks/useCamera.js +45 -0
  37. package/packages/device/src/hooks/useGallery.js +70 -0
  38. package/packages/device/src/hooks/useLocation.js +99 -0
  39. package/packages/device/src/index.js +5 -0
  40. package/packages/examples/package.json +36 -0
  41. package/packages/examples/src/experiments/animations-device/AnimationsDeviceScreen.jsx +291 -0
  42. package/packages/examples/src/experiments/basic-app/BasicAppScreen.jsx +162 -0
  43. package/packages/examples/src/experiments/components-props-state/ComponentsStateScreen.jsx +280 -0
  44. package/packages/examples/src/experiments/navigation/NavigationScreen.jsx +202 -0
  45. package/packages/examples/src/experiments/network-storage/NetworkStorageScreen.jsx +367 -0
  46. package/packages/examples/src/experiments/state-management/StateManagementScreen.jsx +255 -0
  47. package/packages/examples/src/index.js +76 -0
  48. package/packages/navigation/package.json +20 -0
  49. package/packages/navigation/src/DrawerNavigator.jsx +35 -0
  50. package/packages/navigation/src/StackNavigator.jsx +51 -0
  51. package/packages/navigation/src/TabNavigator.jsx +44 -0
  52. package/packages/navigation/src/createAppNavigator.jsx +48 -0
  53. package/packages/navigation/src/index.js +8 -0
  54. package/packages/navigation/src/types.js +18 -0
  55. package/packages/network/package.json +19 -0
  56. package/packages/network/src/apiClient.js +90 -0
  57. package/packages/network/src/fetchHelpers.js +97 -0
  58. package/packages/network/src/hooks/useFetch.js +56 -0
  59. package/packages/network/src/index.js +3 -0
  60. package/packages/network/src/types.js +4 -0
  61. package/packages/state/package.json +22 -0
  62. package/packages/state/src/context/AuthContext.jsx +94 -0
  63. package/packages/state/src/context/ThemeContext.jsx +79 -0
  64. package/packages/state/src/context/index.js +3 -0
  65. package/packages/state/src/index.js +5 -0
  66. package/packages/state/src/redux/hooks.js +12 -0
  67. package/packages/state/src/redux/index.js +7 -0
  68. package/packages/state/src/redux/slices/counterSlice.js +39 -0
  69. package/packages/state/src/redux/slices/postsSlice.js +92 -0
  70. package/packages/state/src/redux/store.js +32 -0
  71. package/packages/storage/package.json +24 -0
  72. package/packages/storage/src/asyncStorage.js +82 -0
  73. package/packages/storage/src/index.js +2 -0
  74. package/packages/storage/src/sqlite/database.js +65 -0
  75. package/packages/storage/src/sqlite/index.js +3 -0
  76. package/packages/storage/src/sqlite/operations.js +112 -0
  77. package/packages/storage/src/sqlite/useSQLite.js +45 -0
@@ -0,0 +1,45 @@
1
+ /**
2
+ * useCamera β€” manages Camera permission and provides a ref for expo-camera.
3
+ *
4
+ * The camera component itself is rendered by the consumer (useCamera only
5
+ * manages permissions and state), so this hook is fully decoupled from
6
+ * the camera UI.
7
+ *
8
+ * @example
9
+ * function CameraScreen() {
10
+ * const { permission, requestPermission, hasPermission } = useCamera();
11
+ *
12
+ * if (!hasPermission) {
13
+ * return (
14
+ * <View>
15
+ * <Text>Camera access needed</Text>
16
+ * <Button onPress={requestPermission}>Grant Permission</Button>
17
+ * </View>
18
+ * );
19
+ * }
20
+ *
21
+ * return <CameraView style={{ flex: 1 }} />;
22
+ * }
23
+ */
24
+
25
+ import { useCallback, useEffect, useState } from 'react';
26
+ import { Camera } from 'expo-camera';
27
+
28
+ export function useCamera(){
29
+ const [permission, setPermission] = useState(null);
30
+
31
+ useEffect(() => {
32
+ void Camera.getCameraPermissionsAsync().then((p) => setPermission(p.status));
33
+ }, []);
34
+
35
+ const requestPermission = useCallback(async () => {
36
+ const result = await Camera.requestCameraPermissionsAsync();
37
+ setPermission(result.status);
38
+ }, []);
39
+
40
+ return {
41
+ permission,
42
+ hasPermission: permission === 'granted',
43
+ isDenied: permission === 'denied',
44
+ requestPermission };
45
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * useGallery β€” picks images/videos from the device gallery using expo-image-picker.
3
+ *
4
+ * @example
5
+ * function ImagePicker() {
6
+ * const { pickImage, asset, isLoading } = useGallery({ aspect: [4, 3] });
7
+ *
8
+ * return (
9
+ * <View>
10
+ * {asset && <Image source={{ uri: asset.uri }} style={{ width: 200, height: 150 }} />}
11
+ * <Button onPress={pickImage} loading={isLoading}>Pick from Gallery</Button>
12
+ * </View>
13
+ * );
14
+ * }
15
+ */
16
+
17
+ import { useCallback, useState } from 'react';
18
+ import * as ImagePicker from 'expo-image-picker';
19
+
20
+ export function useGallery(options = {}){
21
+ const {
22
+ aspect,
23
+ allowsEditing = false,
24
+ quality = 0.8,
25
+ mediaTypes = ImagePicker.MediaTypeOptions.Images,
26
+ allowsMultipleSelection = false } = options;
27
+
28
+ const [asset, setAsset] = useState(null);
29
+ const [assets, setAssets] = useState([]);
30
+ const [isLoading, setIsLoading] = useState(false);
31
+ const [error, setError] = useState(null);
32
+
33
+ const pickImage = useCallback(async () => {
34
+ setError(null);
35
+ setIsLoading(true);
36
+
37
+ try {
38
+ // Request permission if not already granted
39
+ const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
40
+ if (status !== 'granted') {
41
+ setError('Gallery permission denied. Please allow access in Settings.');
42
+ return;
43
+ }
44
+
45
+ const result = await ImagePicker.launchImageLibraryAsync({
46
+ mediaTypes,
47
+ allowsEditing,
48
+ aspect,
49
+ quality,
50
+ allowsMultipleSelection });
51
+
52
+ if (!result.canceled && result.assets.length > 0) {
53
+ setAssets(result.assets);
54
+ setAsset(result.assets[0] ?? null);
55
+ }
56
+ } catch (err) {
57
+ setError(err instanceof Error ? err.message : 'Failed to pick image');
58
+ } finally {
59
+ setIsLoading(false);
60
+ }
61
+ }, [aspect, allowsEditing, quality, mediaTypes, allowsMultipleSelection]);
62
+
63
+ const clear = useCallback(() => {
64
+ setAsset(null);
65
+ setAssets([]);
66
+ setError(null);
67
+ }, []);
68
+
69
+ return { asset, assets, isLoading, error, pickImage, clear };
70
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * useLocation β€” fetches and optionally watches the device's GPS location.
3
+ *
4
+ * @example
5
+ * function MapScreen() {
6
+ * const { location, loading, error, getLocation } = useLocation({ watch: false });
7
+ *
8
+ * if (loading) return <ActivityIndicator />;
9
+ * if (error) return <Text>{error}</Text>;
10
+ *
11
+ * return (
12
+ * <Text>
13
+ * Lat: {location?.coords.latitude}, Lon: {location?.coords.longitude}
14
+ * </Text>
15
+ * );
16
+ * }
17
+ */
18
+
19
+ import { useCallback, useEffect, useRef, useState } from 'react';
20
+ import * as Location from 'expo-location';
21
+
22
+ export function useLocation(options = {}){
23
+ const {
24
+ watch = false,
25
+ distanceInterval = 10,
26
+ accuracy = Location.Accuracy.Balanced } = options;
27
+
28
+ const [location, setLocation] = useState(null);
29
+ const [address, setAddress] = useState(null);
30
+ const [loading, setLoading] = useState(false);
31
+ const [error, setError] = useState(null);
32
+ const subscriptionRef = useRef(null);
33
+
34
+ const requestPermission = async ()=> {
35
+ const { status } = await Location.requestForegroundPermissionsAsync();
36
+ if (status !== 'granted') {
37
+ setError('Location permission denied. Please allow access in Settings.');
38
+ return false;
39
+ }
40
+ return true;
41
+ };
42
+
43
+ const getLocation = useCallback(async () => {
44
+ setLoading(true);
45
+ setError(null);
46
+ try {
47
+ const granted = await requestPermission();
48
+ if (!granted) return;
49
+
50
+ const loc = await Location.getCurrentPositionAsync({ accuracy });
51
+ setLocation(loc);
52
+ } catch (err) {
53
+ setError(err instanceof Error ? err.message : 'Failed to get location');
54
+ } finally {
55
+ setLoading(false);
56
+ }
57
+ }, [accuracy]);
58
+
59
+ const reverseGeocode = useCallback(async () => {
60
+ if (!location) {
61
+ setError('No location available to reverse geocode');
62
+ return;
63
+ }
64
+ try {
65
+ const results = await Location.reverseGeocodeAsync({
66
+ latitude: location.coords.latitude,
67
+ longitude: location.coords.longitude });
68
+ setAddress(results[0] ?? null);
69
+ } catch (err) {
70
+ setError(err instanceof Error ? err.message : 'Reverse geocoding failed');
71
+ }
72
+ }, [location]);
73
+
74
+ // Set up watcher if watch=true
75
+ useEffect(() => {
76
+ if (!watch) return;
77
+
78
+ void (async () => {
79
+ const granted = await requestPermission();
80
+ if (!granted) return;
81
+
82
+ setLoading(true);
83
+ subscriptionRef.current = await Location.watchPositionAsync(
84
+ { accuracy, distanceInterval },
85
+ (loc) => {
86
+ setLocation(loc);
87
+ setLoading(false);
88
+ },
89
+ );
90
+ })();
91
+
92
+ return () => {
93
+ subscriptionRef.current?.remove();
94
+ subscriptionRef.current = null;
95
+ };
96
+ }, [watch, accuracy, distanceInterval]);
97
+
98
+ return { location, address, loading, error, getLocation, reverseGeocode };
99
+ }
@@ -0,0 +1,5 @@
1
+ export { useCamera } from './hooks/useCamera';
2
+
3
+ export { useGallery } from './hooks/useGallery';
4
+
5
+ export { useLocation } from './hooks/useLocation';
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@expotesting/examples",
3
+ "version": "1.0.0",
4
+ "description": "Runnable experiment modules mapping to the university syllabus",
5
+ "main": "src/index.js",
6
+ "types": "src/index.ts",
7
+ "scripts": {},
8
+ "dependencies": {
9
+ "@expotesting/core": "workspace:*",
10
+ "@expotesting/navigation": "workspace:*",
11
+ "@expotesting/state": "workspace:*",
12
+ "@expotesting/network": "workspace:*",
13
+ "@expotesting/storage": "workspace:*",
14
+ "@expotesting/animations": "workspace:*",
15
+ "@expotesting/device": "workspace:*"
16
+ },
17
+ "peerDependencies": {
18
+ "react": ">=18",
19
+ "react-native": ">=0.73",
20
+ "expo": ">=51",
21
+ "@react-navigation/native": "^6",
22
+ "@react-navigation/native-stack": "^6",
23
+ "@react-navigation/bottom-tabs": "^6",
24
+ "@react-navigation/drawer": "^6",
25
+ "@reduxjs/toolkit": "^2",
26
+ "react-redux": "^9",
27
+ "axios": "^1",
28
+ "@react-native-async-storage/async-storage": ">=1",
29
+ "expo-sqlite": ">=14",
30
+ "react-native-reanimated": ">=3",
31
+ "expo-camera": ">=15",
32
+ "expo-image-picker": ">=15",
33
+ "expo-location": ">=17"
34
+ },
35
+ "license": "MIT"
36
+ }
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Experiment 06 β€” Animations & Device Features
3
+ * Module 3: Animated API Β· Reanimated Β· Camera Β· Gallery Β· GPS
4
+ *
5
+ * Demonstrates:
6
+ * βœ“ Fade animation (Animated API)
7
+ * βœ“ Scale animation (spring)
8
+ * βœ“ Slide animation
9
+ * βœ“ Reanimated entering/exiting animations
10
+ * βœ“ Camera capture (Expo Camera)
11
+ * βœ“ Image gallery picker (Expo ImagePicker)
12
+ * βœ“ GPS location (Expo Location)
13
+ */
14
+
15
+ import React, { useState } from 'react';
16
+ import {
17
+ Image,
18
+ Pressable,
19
+ ScrollView,
20
+ StyleSheet,
21
+ Text,
22
+ View } from 'react-native';
23
+ import Animated, { FadeIn, FadeOut, ZoomIn, SlideInLeft } from 'react-native-reanimated';
24
+
25
+ import { FadeView, ScaleView, SlideView } from '@expotesting/animations';
26
+ import { useCamera, useGallery, useLocation } from '@expotesting/device';
27
+
28
+ // ── Animations Section ────────────────────────────────────────────────────────
29
+ function AnimationsSection(){
30
+ const [showFade, setShowFade] = useState(true);
31
+ const [showScale, setShowScale] = useState(true);
32
+ const [showSlide, setShowSlide] = useState(true);
33
+ const [showReanimated, setShowReanimated] = useState(false);
34
+
35
+ return (
36
+ <View style={styles.section}>
37
+ <Text style={styles.sectionTitle}>Animations</Text>
38
+
39
+ {/* Fade */}
40
+ <View style={styles.widget}>
41
+ <Text style={styles.widgetTitle}>FadeView β€” Animated API</Text>
42
+ <FadeView visible={showFade} duration={400}>
43
+ <View style={[styles.animBox, { backgroundColor: '#E1BEE7' }]}>
44
+ <Text style={styles.animLabel}>Fading content</Text>
45
+ </View>
46
+ </FadeView>
47
+ <Pressable style={styles.btn} onPress={() => setShowFade((v) => !v)}>
48
+ <Text style={styles.btnText}>{showFade ? 'Fade Out' : 'Fade In'}</Text>
49
+ </Pressable>
50
+ </View>
51
+
52
+ {/* Scale */}
53
+ <View style={styles.widget}>
54
+ <Text style={styles.widgetTitle}>ScaleView β€” Spring animation</Text>
55
+ <ScaleView visible={showScale}>
56
+ <View style={[styles.animBox, { backgroundColor: '#B2EBF2' }]}>
57
+ <Text style={styles.animLabel}>Scaling content</Text>
58
+ </View>
59
+ </ScaleView>
60
+ <Pressable style={styles.btn} onPress={() => setShowScale((v) => !v)}>
61
+ <Text style={styles.btnText}>{showScale ? 'Scale Out' : 'Scale In'}</Text>
62
+ </Pressable>
63
+ </View>
64
+
65
+ {/* Slide */}
66
+ <View style={styles.widget}>
67
+ <Text style={styles.widgetTitle}>SlideView β€” Slide from top</Text>
68
+ <SlideView visible={showSlide} direction="top">
69
+ <View style={[styles.animBox, { backgroundColor: '#C8E6C9' }]}>
70
+ <Text style={styles.animLabel}>Sliding content</Text>
71
+ </View>
72
+ </SlideView>
73
+ <Pressable style={styles.btn} onPress={() => setShowSlide((v) => !v)}>
74
+ <Text style={styles.btnText}>{showSlide ? 'Slide Out' : 'Slide In'}</Text>
75
+ </Pressable>
76
+ </View>
77
+
78
+ {/* Reanimated entering / exiting */}
79
+ <View style={styles.widget}>
80
+ <Text style={styles.widgetTitle}>Reanimated β€” Entering / Exiting</Text>
81
+ <Text style={styles.noteSmall}>
82
+ Uses entering={'{'}FadeIn{'}'} / exiting={'{'}FadeOut{'}'} / ZoomIn / SlideInLeft
83
+ </Text>
84
+ <Pressable style={styles.btn} onPress={() => setShowReanimated((v) => !v)}>
85
+ <Text style={styles.btnText}>{showReanimated ? 'Hide' : 'Show'} Animated Views</Text>
86
+ </Pressable>
87
+ {showReanimated && (
88
+ <View style={styles.row}>
89
+ <Animated.View entering={FadeIn.duration(500)} exiting={FadeOut.duration(300)} style={[styles.reaBox, { backgroundColor: '#F8BBD9' }]}>
90
+ <Text style={styles.reaLabel}>FadeIn</Text>
91
+ </Animated.View>
92
+ <Animated.View entering={ZoomIn.springify()} exiting={FadeOut} style={[styles.reaBox, { backgroundColor: '#DCEDC8' }]}>
93
+ <Text style={styles.reaLabel}>ZoomIn</Text>
94
+ </Animated.View>
95
+ <Animated.View entering={SlideInLeft.duration(400)} exiting={FadeOut} style={[styles.reaBox, { backgroundColor: '#B3E5FC' }]}>
96
+ <Text style={styles.reaLabel}>SlideLeft</Text>
97
+ </Animated.View>
98
+ </View>
99
+ )}
100
+ </View>
101
+ </View>
102
+ );
103
+ }
104
+
105
+ // ── Camera Section ────────────────────────────────────────────────────────────
106
+ function CameraSection(){
107
+ const { hasPermission, requestPermission, capturePhoto, lastPhoto, isCapturing } = useCamera();
108
+
109
+ return (
110
+ <View style={styles.widget}>
111
+ <Text style={styles.widgetTitle}>Camera (Expo Camera)</Text>
112
+ {!hasPermission ? (
113
+ <Pressable style={styles.btn} onPress={() => void requestPermission()}>
114
+ <Text style={styles.btnText}>Grant Camera Permission</Text>
115
+ </Pressable>
116
+ ) : (
117
+ <Pressable
118
+ style={[styles.btn, isCapturing && styles.btnDisabled]}
119
+ onPress={() => void capturePhoto()}
120
+ disabled={isCapturing}
121
+ >
122
+ <Text style={styles.btnText}>{isCapturing ? 'Capturing…' : 'πŸ“· Capture Photo'}</Text>
123
+ </Pressable>
124
+ )}
125
+ {lastPhoto && (
126
+ <Image
127
+ source={{ uri: lastPhoto.uri }}
128
+ style={styles.previewImage}
129
+ accessibilityLabel="Captured photo preview"
130
+ />
131
+ )}
132
+ </View>
133
+ );
134
+ }
135
+
136
+ // ── Gallery Section ───────────────────────────────────────────────────────────
137
+ function GallerySection(){
138
+ const { hasPermission, requestPermission, pickImage, selectedAsset, isPicking } = useGallery({
139
+ allowsEditing: true,
140
+ aspect: [4, 3] });
141
+
142
+ return (
143
+ <View style={styles.widget}>
144
+ <Text style={styles.widgetTitle}>Gallery (Expo ImagePicker)</Text>
145
+ {!hasPermission ? (
146
+ <Pressable style={styles.btn} onPress={() => void requestPermission()}>
147
+ <Text style={styles.btnText}>Grant Gallery Permission</Text>
148
+ </Pressable>
149
+ ) : (
150
+ <Pressable
151
+ style={[styles.btn, isPicking && styles.btnDisabled]}
152
+ onPress={() => void pickImage()}
153
+ disabled={isPicking}
154
+ >
155
+ <Text style={styles.btnText}>{isPicking ? 'Picking…' : 'πŸ–Ό Pick from Gallery'}</Text>
156
+ </Pressable>
157
+ )}
158
+ {selectedAsset && (
159
+ <Image
160
+ source={{ uri: selectedAsset.uri }}
161
+ style={styles.previewImage}
162
+ accessibilityLabel="Selected image preview"
163
+ />
164
+ )}
165
+ </View>
166
+ );
167
+ }
168
+
169
+ // ── Location Section ──────────────────────────────────────────────────────────
170
+ function LocationSection(){
171
+ const { location, hasPermission, requestPermission, getLocation, loading, error } = useLocation({
172
+ accuracy: 'balanced' });
173
+
174
+ return (
175
+ <View style={styles.widget}>
176
+ <Text style={styles.widgetTitle}>GPS Location (Expo Location)</Text>
177
+ {!hasPermission ? (
178
+ <Pressable style={styles.btn} onPress={() => void requestPermission()}>
179
+ <Text style={styles.btnText}>Grant Location Permission</Text>
180
+ </Pressable>
181
+ ) : (
182
+ <Pressable
183
+ style={[styles.btn, loading && styles.btnDisabled]}
184
+ onPress={() => void getLocation()}
185
+ disabled={loading}
186
+ >
187
+ <Text style={styles.btnText}>{loading ? 'Getting location…' : 'πŸ“ Get My Location'}</Text>
188
+ </Pressable>
189
+ )}
190
+ {error && <Text style={styles.errorText}>{error}</Text>}
191
+ {location && (
192
+ <View style={styles.locationBox}>
193
+ <LocationRow label="Latitude" value={location.coords.latitude.toFixed(6)} />
194
+ <LocationRow label="Longitude" value={location.coords.longitude.toFixed(6)} />
195
+ <LocationRow label="Accuracy" value={`${(location.coords.accuracy ?? 0).toFixed(0)}m`} />
196
+ <LocationRow label="Altitude" value={`${(location.coords.altitude ?? 0).toFixed(1)}m`} />
197
+ </View>
198
+ )}
199
+ </View>
200
+ );
201
+ }
202
+
203
+ function LocationRow({ label, value }){
204
+ return (
205
+ <View style={styles.locationRow}>
206
+ <Text style={styles.locationLabel}>{label}</Text>
207
+ <Text style={styles.locationValue}>{value}</Text>
208
+ </View>
209
+ );
210
+ }
211
+
212
+ // ── Main screen ───────────────────────────────────────────────────────────────
213
+ export function AnimationsDeviceScreen(){
214
+ return (
215
+ <ScrollView style={styles.container} contentContainerStyle={styles.content}>
216
+ <View style={styles.header}>
217
+ <Text style={styles.heading}>Experiment 06</Text>
218
+ <Text style={styles.subheading}>Animations & Device Features</Text>
219
+ </View>
220
+
221
+ <AnimationsSection />
222
+
223
+ <View style={styles.section}>
224
+ <Text style={styles.sectionTitle}>Device Features</Text>
225
+ <CameraSection />
226
+ <GallerySection />
227
+ <LocationSection />
228
+ </View>
229
+ </ScrollView>
230
+ );
231
+ }
232
+
233
+ // ── Styles ────────────────────────────────────────────────────────────────────
234
+ const styles = StyleSheet.create({
235
+ container: { flex: 1, backgroundColor: '#fff' },
236
+ content: { paddingBottom: 40 },
237
+ header: { backgroundColor: '#6200EE', padding: 24, paddingTop: 40 },
238
+ heading: { fontSize: 28, fontWeight: '700', color: '#fff' },
239
+ subheading: { fontSize: 16, color: 'rgba(255,255,255,0.8)', marginTop: 4 },
240
+ section: { padding: 16, borderBottomWidth: 1, borderBottomColor: '#e0e0e0' },
241
+ sectionTitle: { fontSize: 18, fontWeight: '700', color: '#333', marginBottom: 12 },
242
+ widget: { backgroundColor: '#f9f9f9', borderRadius: 12, padding: 14, marginBottom: 12 },
243
+ widgetTitle: { fontSize: 15, fontWeight: '700', color: '#333', marginBottom: 8 },
244
+ noteSmall: { fontSize: 12, color: '#888', marginBottom: 8 },
245
+ btn: {
246
+ backgroundColor: '#6200EE',
247
+ paddingVertical: 10,
248
+ paddingHorizontal: 20,
249
+ borderRadius: 8,
250
+ alignItems: 'center',
251
+ justifyContent: 'center' },
252
+ btnDisabled: { opacity: 0.5 },
253
+ btnText: { color: '#fff', fontWeight: '700', fontSize: 14 },
254
+ errorText: { color: '#B00020', fontSize: 13, marginVertical: 4 },
255
+ animBox: {
256
+ height: 64,
257
+ borderRadius: 8,
258
+ alignItems: 'center',
259
+ justifyContent: 'center',
260
+ marginBottom: 8 },
261
+ animLabel: { fontSize: 14, fontWeight: '600', color: '#333' },
262
+ row: { flexDirection: 'row', gap: 8, marginTop: 8, flexWrap: 'wrap' },
263
+ reaBox: {
264
+ flex: 1,
265
+ height: 60,
266
+ borderRadius: 8,
267
+ alignItems: 'center',
268
+ justifyContent: 'center',
269
+ minWidth: 80 },
270
+ reaLabel: { fontSize: 11, fontWeight: '700', color: '#333' },
271
+ previewImage: {
272
+ width: '100%',
273
+ height: 200,
274
+ borderRadius: 8,
275
+ marginTop: 10,
276
+ backgroundColor: '#eee' },
277
+ locationBox: {
278
+ backgroundColor: '#fff',
279
+ borderRadius: 8,
280
+ padding: 10,
281
+ marginTop: 8,
282
+ borderWidth: 1,
283
+ borderColor: '#e0e0e0' },
284
+ locationRow: {
285
+ flexDirection: 'row',
286
+ justifyContent: 'space-between',
287
+ paddingVertical: 4,
288
+ borderBottomWidth: 1,
289
+ borderBottomColor: '#f0f0f0' },
290
+ locationLabel: { fontSize: 13, color: '#666' },
291
+ locationValue: { fontSize: 13, fontWeight: '700', color: '#333' } });