Skip to content

Commit 68dae0d

Browse files
committed
feat: allow importing haptics
1 parent 3b9ee38 commit 68dae0d

File tree

8 files changed

+351
-18
lines changed

8 files changed

+351
-18
lines changed

example/app/_layout.tsx

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,34 @@
11
import { Stack } from 'expo-router';
22
import { useHapticEngine } from 'react-native-ahap';
33
import { GestureHandlerRootView } from 'react-native-gesture-handler';
4+
import { RecorderProvider } from '../src/contexts/RecorderContext';
5+
import { KeyboardProvider } from 'react-native-keyboard-controller';
46

57
export default function RootLayout() {
68
useHapticEngine();
79

810
return (
9-
<GestureHandlerRootView style={{ flex: 1 }}>
10-
<Stack
11-
screenOptions={{
12-
headerShown: false,
13-
contentStyle: { backgroundColor: 'transparent' },
14-
}}
15-
/>
16-
</GestureHandlerRootView>
11+
<KeyboardProvider>
12+
<GestureHandlerRootView style={{ flex: 1 }}>
13+
<RecorderProvider>
14+
<Stack
15+
screenOptions={{
16+
headerShown: false,
17+
contentStyle: { backgroundColor: 'transparent' },
18+
}}
19+
>
20+
<Stack.Screen name="index" />
21+
<Stack.Screen name="playground" />
22+
<Stack.Screen name="recorder" />
23+
<Stack.Screen
24+
name="import-modal"
25+
options={{
26+
presentation: 'formSheet',
27+
}}
28+
/>
29+
</Stack>
30+
</RecorderProvider>
31+
</GestureHandlerRootView>
32+
</KeyboardProvider>
1733
);
1834
}

example/app/import-modal.tsx

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import {
2+
View,
3+
Text,
4+
TextInput,
5+
TouchableOpacity,
6+
StyleSheet,
7+
Platform,
8+
} from 'react-native';
9+
import { router } from 'expo-router';
10+
import { useState } from 'react';
11+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
12+
import type { RecordedHaptic } from '../src/types/recording';
13+
import { useRecorder } from '../src/contexts/RecorderContext';
14+
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
15+
import { recordedHapticSchema } from '../src/schemas/recordingSchema';
16+
import { ZodError } from 'zod';
17+
18+
export default function ImportModal() {
19+
const insets = useSafeAreaInsets();
20+
const { importRecording } = useRecorder();
21+
const [title, setTitle] = useState('');
22+
const [jsonText, setJsonText] = useState('');
23+
const [error, setError] = useState('');
24+
25+
const handleImport = () => {
26+
setError('');
27+
28+
// Validate title
29+
if (!title.trim()) {
30+
setError('Please enter a title');
31+
return;
32+
}
33+
34+
// Validate and parse JSON
35+
try {
36+
const parsed = JSON.parse(jsonText);
37+
38+
// Validate using Zod schema
39+
const validationResult = recordedHapticSchema.safeParse(parsed);
40+
41+
if (!validationResult.success) {
42+
// Format Zod errors for user-friendly display
43+
const zodError = validationResult.error;
44+
const firstError = zodError.issues[0];
45+
const errorPath = firstError?.path.join('.') || 'unknown field';
46+
const errorMessage = firstError?.message || 'Invalid format';
47+
setError(`Validation error at ${errorPath}: ${errorMessage}`);
48+
return;
49+
}
50+
51+
// Create the recording object with the new title
52+
const recording: RecordedHaptic = {
53+
...validationResult.data,
54+
name: title,
55+
id: Date.now().toString(), // Generate new ID
56+
createdAt: Date.now(), // Update timestamp
57+
};
58+
59+
// Import the recording
60+
importRecording(recording);
61+
62+
// Navigate back
63+
router.back();
64+
} catch (err) {
65+
if (err instanceof ZodError) {
66+
setError('Invalid recording format. Please check your JSON.');
67+
} else if (err instanceof SyntaxError) {
68+
setError('Invalid JSON format. Please check your input.');
69+
} else {
70+
setError('An error occurred while importing the recording.');
71+
}
72+
console.error('Import error:', err);
73+
}
74+
};
75+
76+
const isPresented = router.canGoBack();
77+
78+
return (
79+
<KeyboardAwareScrollView style={styles.container}>
80+
<View style={[styles.header, { paddingTop: insets.top + 16 }]}>
81+
<Text style={styles.headerTitle}>Import Recording</Text>
82+
{isPresented && (
83+
<TouchableOpacity
84+
onPress={() => router.back()}
85+
style={styles.closeButton}
86+
>
87+
<Text style={styles.closeButtonText}></Text>
88+
</TouchableOpacity>
89+
)}
90+
</View>
91+
92+
<View style={styles.content}>
93+
<View style={styles.inputGroup}>
94+
<Text style={styles.label}>Title</Text>
95+
<TextInput
96+
style={styles.titleInput}
97+
value={title}
98+
onChangeText={setTitle}
99+
placeholder="Enter recording name"
100+
placeholderTextColor="#636366"
101+
autoCapitalize="words"
102+
/>
103+
</View>
104+
105+
<View style={styles.inputGroup}>
106+
<Text style={styles.label}>JSON Data</Text>
107+
<TextInput
108+
style={styles.jsonInput}
109+
value={jsonText}
110+
onChangeText={setJsonText}
111+
placeholder="Paste recording JSON here"
112+
placeholderTextColor="#636366"
113+
multiline
114+
textAlignVertical="top"
115+
autoCapitalize="none"
116+
autoCorrect={false}
117+
/>
118+
</View>
119+
120+
{error ? <Text style={styles.errorText}>{error}</Text> : null}
121+
122+
<TouchableOpacity
123+
style={[
124+
styles.importButton,
125+
(!title.trim() || !jsonText.trim()) && styles.importButtonDisabled,
126+
]}
127+
onPress={handleImport}
128+
disabled={!title.trim() || !jsonText.trim()}
129+
>
130+
<Text style={styles.importButtonText}>Import</Text>
131+
</TouchableOpacity>
132+
133+
<Text style={styles.hint}>
134+
Paste the JSON data exported from a recording. The recording will be
135+
added to your list.
136+
</Text>
137+
</View>
138+
</KeyboardAwareScrollView>
139+
);
140+
}
141+
142+
const styles = StyleSheet.create({
143+
container: {
144+
backgroundColor: '#000000',
145+
},
146+
header: {
147+
flexDirection: 'row',
148+
alignItems: 'center',
149+
justifyContent: 'center',
150+
paddingHorizontal: 16,
151+
paddingBottom: 16,
152+
borderBottomWidth: 1,
153+
borderBottomColor: '#1C1C1E',
154+
},
155+
headerTitle: {
156+
fontSize: 20,
157+
fontWeight: '700',
158+
color: '#FFFFFF',
159+
},
160+
closeButton: {
161+
position: 'absolute',
162+
right: 16,
163+
top: 16,
164+
width: 32,
165+
height: 32,
166+
borderRadius: 16,
167+
backgroundColor: '#1C1C1E',
168+
alignItems: 'center',
169+
justifyContent: 'center',
170+
},
171+
closeButtonText: {
172+
fontSize: 18,
173+
color: '#FFFFFF',
174+
fontWeight: '600',
175+
},
176+
content: {
177+
flex: 1,
178+
padding: 16,
179+
},
180+
contentContainer: {
181+
padding: 16,
182+
},
183+
inputGroup: {
184+
marginBottom: 24,
185+
},
186+
label: {
187+
fontSize: 16,
188+
fontWeight: '600',
189+
color: '#FFFFFF',
190+
marginBottom: 8,
191+
},
192+
titleInput: {
193+
backgroundColor: '#1C1C1E',
194+
borderRadius: 12,
195+
padding: 16,
196+
fontSize: 16,
197+
color: '#FFFFFF',
198+
borderWidth: 1,
199+
borderColor: '#2C2C2E',
200+
},
201+
jsonInput: {
202+
backgroundColor: '#1C1C1E',
203+
borderRadius: 12,
204+
padding: 16,
205+
fontSize: 14,
206+
color: '#FFFFFF',
207+
minHeight: 200,
208+
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
209+
borderWidth: 1,
210+
borderColor: '#2C2C2E',
211+
height: 200,
212+
},
213+
errorText: {
214+
color: '#FF3B30',
215+
fontSize: 14,
216+
marginBottom: 16,
217+
textAlign: 'center',
218+
},
219+
importButton: {
220+
backgroundColor: '#007AFF',
221+
borderRadius: 12,
222+
padding: 16,
223+
alignItems: 'center',
224+
marginBottom: 16,
225+
},
226+
importButtonDisabled: {
227+
backgroundColor: '#1C1C1E',
228+
},
229+
importButtonText: {
230+
fontSize: 16,
231+
fontWeight: '700',
232+
color: '#FFFFFF',
233+
},
234+
hint: {
235+
fontSize: 12,
236+
color: '#8E8E93',
237+
textAlign: 'center',
238+
lineHeight: 18,
239+
},
240+
});

example/bun.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@
2323
"react-native": "0.81.5",
2424
"react-native-ahap": "*",
2525
"react-native-gesture-handler": "^2.30.0",
26+
"react-native-keyboard-controller": "1.18.5",
2627
"react-native-mmkv": "^4.1.0",
2728
"react-native-nitro-modules": "^0.32.0",
2829
"react-native-reanimated": "^4.2.1",
2930
"react-native-safe-area-context": "~5.6.0",
3031
"react-native-screens": "~4.16.0",
31-
"react-native-worklets": "^0.7.1"
32+
"react-native-worklets": "^0.7.1",
33+
"zod": "^4.3.4"
3234
},
3335
"devDependencies": {
3436
"@babel/core": "^7.25.2",

example/src/components/RecordingsList.tsx

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { View, Text, StyleSheet, FlatList } from 'react-native';
1+
import { View, Text, StyleSheet, FlatList, TouchableOpacity } from 'react-native';
22
import type { RecordedHaptic } from '../types/recording';
33
import RecordingItem from './RecordingItem';
44
import { useSafeAreaInsets } from 'react-native-safe-area-context';
5+
import { Link } from 'expo-router';
56

67
interface RecordingsListProps {
78
recordings: RecordedHaptic[];
@@ -45,7 +46,14 @@ export default function RecordingsList({
4546

4647
return (
4748
<View style={styles.container}>
48-
<Text style={styles.title}>Recordings</Text>
49+
<View style={styles.header}>
50+
<Text style={styles.title}>Recordings</Text>
51+
<Link href="/import-modal" asChild>
52+
<TouchableOpacity style={styles.addButton}>
53+
<Text style={styles.addButtonText}>+</Text>
54+
</TouchableOpacity>
55+
</Link>
56+
</View>
4957
<FlatList
5058
data={recordings}
5159
keyExtractor={(item) => item.id}
@@ -71,11 +79,30 @@ const styles = StyleSheet.create({
7179
flex: 1,
7280
paddingHorizontal: 16,
7381
},
82+
header: {
83+
flexDirection: 'row',
84+
alignItems: 'center',
85+
justifyContent: 'space-between',
86+
marginBottom: 12,
87+
},
7488
title: {
7589
fontSize: 20,
7690
fontWeight: '700',
7791
color: '#FFFFFF',
78-
marginBottom: 12,
92+
},
93+
addButton: {
94+
width: 32,
95+
height: 32,
96+
borderRadius: 16,
97+
backgroundColor: '#007AFF',
98+
alignItems: 'center',
99+
justifyContent: 'center',
100+
},
101+
addButtonText: {
102+
fontSize: 24,
103+
color: '#FFFFFF',
104+
fontWeight: '600',
105+
marginTop: -2,
79106
},
80107
emptyContainer: {
81108
flex: 1,

0 commit comments

Comments
 (0)