Skip to content

Commit cbfc2d6

Browse files
cmcWebCode40claude
andcommitted
feat: add ExportModal component for log export format selection
Add modal UI for selecting export format (HAR, Postman Collection, JSON) with themed styling, loading states, and native share sheet integration. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2bd118f commit cbfc2d6

1 file changed

Lines changed: 261 additions & 0 deletions

File tree

src/components/ExportModal.tsx

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import React, { useState, useCallback } from 'react';
2+
import {
3+
Modal,
4+
View,
5+
Text,
6+
TouchableOpacity,
7+
Alert,
8+
ActivityIndicator,
9+
Share,
10+
type ViewStyle,
11+
type TextStyle,
12+
} from 'react-native';
13+
import type { NetworkLog } from '../types';
14+
import type { ThemeMode } from '../constants/colors';
15+
import { getThemeColors } from '../constants/colors';
16+
import {
17+
exportLogs,
18+
getExportFileName,
19+
type ExportFormat,
20+
} from '../utils/export';
21+
22+
interface ExportModalProps {
23+
visible: boolean;
24+
onClose: () => void;
25+
logs: NetworkLog[];
26+
theme?: ThemeMode;
27+
}
28+
29+
interface ExportOption {
30+
format: ExportFormat;
31+
label: string;
32+
description: string;
33+
icon: string;
34+
}
35+
36+
const EXPORT_OPTIONS: ExportOption[] = [
37+
{
38+
format: 'har',
39+
label: 'HAR File',
40+
description: 'HTTP Archive format - Import into browser DevTools',
41+
icon: '📦',
42+
},
43+
{
44+
format: 'postman',
45+
label: 'Postman Collection',
46+
description: 'Import directly into Postman',
47+
icon: '📮',
48+
},
49+
{
50+
format: 'json',
51+
label: 'JSON',
52+
description: 'Raw JSON data export',
53+
icon: '📄',
54+
},
55+
];
56+
57+
export const ExportModal: React.FC<ExportModalProps> = ({
58+
visible,
59+
onClose,
60+
logs,
61+
theme = 'light',
62+
}) => {
63+
const [exporting, setExporting] = useState<ExportFormat | null>(null);
64+
const themeColors = getThemeColors(theme);
65+
66+
const handleExport = useCallback(
67+
async (format: ExportFormat) => {
68+
if (logs.length === 0) {
69+
Alert.alert('No Logs', 'There are no logs to export.');
70+
return;
71+
}
72+
73+
setExporting(format);
74+
75+
try {
76+
const content = exportLogs(logs, format);
77+
const fileName = getExportFileName(format);
78+
79+
await Share.share({
80+
message: content,
81+
title: fileName,
82+
});
83+
} catch (error) {
84+
if (error instanceof Error && error.message !== 'User did not share') {
85+
Alert.alert('Export Failed', error.message);
86+
}
87+
} finally {
88+
setExporting(null);
89+
}
90+
},
91+
[logs]
92+
);
93+
94+
const themedStyles = createThemedStyles(themeColors);
95+
96+
return (
97+
<Modal
98+
visible={visible}
99+
animationType="slide"
100+
transparent={true}
101+
onRequestClose={onClose}
102+
>
103+
<View style={staticStyles.overlay}>
104+
<View style={themedStyles.modalContent}>
105+
<View style={staticStyles.header}>
106+
<Text style={themedStyles.title}>Export Logs</Text>
107+
<TouchableOpacity
108+
onPress={onClose}
109+
style={staticStyles.closeButton}
110+
>
111+
<Text style={themedStyles.closeButtonText}></Text>
112+
</TouchableOpacity>
113+
</View>
114+
115+
<Text style={themedStyles.subtitle}>
116+
{logs.length} request{logs.length !== 1 ? 's' : ''} will be exported
117+
</Text>
118+
119+
<View style={staticStyles.optionsContainer}>
120+
{EXPORT_OPTIONS.map((option) => (
121+
<TouchableOpacity
122+
key={option.format}
123+
style={themedStyles.optionButton}
124+
onPress={() => handleExport(option.format)}
125+
disabled={exporting !== null}
126+
activeOpacity={0.7}
127+
>
128+
<View style={staticStyles.optionContent}>
129+
<Text style={staticStyles.optionIcon}>{option.icon}</Text>
130+
<View style={staticStyles.optionText}>
131+
<Text style={themedStyles.optionLabel}>{option.label}</Text>
132+
<Text style={themedStyles.optionDescription}>
133+
{option.description}
134+
</Text>
135+
</View>
136+
{exporting === option.format ? (
137+
<ActivityIndicator
138+
size="small"
139+
color={themeColors.primary}
140+
/>
141+
) : (
142+
<Text style={themedStyles.chevron}></Text>
143+
)}
144+
</View>
145+
</TouchableOpacity>
146+
))}
147+
</View>
148+
149+
<TouchableOpacity
150+
style={themedStyles.cancelButton}
151+
onPress={onClose}
152+
activeOpacity={0.7}
153+
>
154+
<Text style={themedStyles.cancelButtonText}>Cancel</Text>
155+
</TouchableOpacity>
156+
</View>
157+
</View>
158+
</Modal>
159+
);
160+
};
161+
162+
interface ThemeColors {
163+
background: string;
164+
surface: string;
165+
border: string;
166+
text: string;
167+
textSecondary: string;
168+
textMuted: string;
169+
primary: string;
170+
}
171+
172+
const createThemedStyles = (themeColors: ThemeColors) => ({
173+
modalContent: {
174+
backgroundColor: themeColors.surface,
175+
borderTopLeftRadius: 20,
176+
borderTopRightRadius: 20,
177+
padding: 20,
178+
maxHeight: '80%',
179+
} as ViewStyle,
180+
title: {
181+
fontSize: 20,
182+
fontWeight: 'bold',
183+
color: themeColors.text,
184+
} as TextStyle,
185+
closeButtonText: {
186+
fontSize: 20,
187+
color: themeColors.textMuted,
188+
} as TextStyle,
189+
subtitle: {
190+
fontSize: 14,
191+
color: themeColors.textSecondary,
192+
marginBottom: 20,
193+
} as TextStyle,
194+
optionButton: {
195+
backgroundColor: themeColors.background,
196+
borderRadius: 12,
197+
padding: 16,
198+
marginBottom: 12,
199+
borderWidth: 1,
200+
borderColor: themeColors.border,
201+
} as ViewStyle,
202+
optionLabel: {
203+
fontSize: 16,
204+
fontWeight: '600',
205+
color: themeColors.text,
206+
marginBottom: 2,
207+
} as TextStyle,
208+
optionDescription: {
209+
fontSize: 12,
210+
color: themeColors.textSecondary,
211+
} as TextStyle,
212+
chevron: {
213+
fontSize: 24,
214+
color: themeColors.textMuted,
215+
} as TextStyle,
216+
cancelButton: {
217+
backgroundColor: themeColors.background,
218+
borderRadius: 12,
219+
padding: 16,
220+
alignItems: 'center',
221+
marginTop: 8,
222+
borderWidth: 1,
223+
borderColor: themeColors.border,
224+
} as ViewStyle,
225+
cancelButtonText: {
226+
fontSize: 16,
227+
fontWeight: '600',
228+
color: themeColors.primary,
229+
} as TextStyle,
230+
});
231+
232+
const staticStyles = {
233+
overlay: {
234+
flex: 1,
235+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
236+
justifyContent: 'flex-end',
237+
} as ViewStyle,
238+
header: {
239+
flexDirection: 'row',
240+
justifyContent: 'space-between',
241+
alignItems: 'center',
242+
marginBottom: 8,
243+
} as ViewStyle,
244+
closeButton: {
245+
padding: 8,
246+
} as ViewStyle,
247+
optionsContainer: {
248+
marginBottom: 8,
249+
} as ViewStyle,
250+
optionContent: {
251+
flexDirection: 'row',
252+
alignItems: 'center',
253+
} as ViewStyle,
254+
optionIcon: {
255+
fontSize: 28,
256+
marginRight: 16,
257+
} as TextStyle,
258+
optionText: {
259+
flex: 1,
260+
} as ViewStyle,
261+
};

0 commit comments

Comments
 (0)