@l4yercak3/cli 1.2.16 → 1.2.19
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.
- package/.claude/settings.local.json +3 -1
- package/docs/CRM-PIPELINES-SEQUENCES-SPEC.md +429 -0
- package/docs/INTEGRATION_PATHS_ARCHITECTURE.md +1543 -0
- package/package.json +1 -1
- package/src/commands/login.js +26 -7
- package/src/commands/spread.js +251 -10
- package/src/detectors/database-detector.js +245 -0
- package/src/detectors/expo-detector.js +4 -4
- package/src/detectors/index.js +17 -4
- package/src/generators/api-only/client.js +683 -0
- package/src/generators/api-only/index.js +96 -0
- package/src/generators/api-only/types.js +618 -0
- package/src/generators/api-only/webhooks.js +377 -0
- package/src/generators/env-generator.js +23 -8
- package/src/generators/expo-auth-generator.js +1009 -0
- package/src/generators/index.js +88 -2
- package/src/generators/mcp-guide-generator.js +256 -0
- package/src/generators/quickstart/components/index.js +1699 -0
- package/src/generators/quickstart/components-mobile/index.js +1440 -0
- package/src/generators/quickstart/database/convex.js +1257 -0
- package/src/generators/quickstart/database/index.js +34 -0
- package/src/generators/quickstart/database/supabase.js +1132 -0
- package/src/generators/quickstart/hooks/index.js +1065 -0
- package/src/generators/quickstart/index.js +177 -0
- package/src/generators/quickstart/pages/index.js +1466 -0
- package/src/generators/quickstart/screens/index.js +1498 -0
- package/src/mcp/registry/domains/benefits.js +798 -0
- package/src/mcp/registry/index.js +2 -0
- package/tests/database-detector.test.js +221 -0
- package/tests/expo-detector.test.js +3 -4
- package/tests/generators-index.test.js +215 -3
|
@@ -0,0 +1,1498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expo Screens Generator
|
|
3
|
+
* Generates React Native screens for Expo/React Native projects
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { ensureDir, writeFileWithBackup, checkFileOverwrite } = require('../../../utils/file-utils');
|
|
9
|
+
|
|
10
|
+
class ScreensGenerator {
|
|
11
|
+
/**
|
|
12
|
+
* Generate Expo screens based on selected features
|
|
13
|
+
* @param {Object} options - Generation options
|
|
14
|
+
* @returns {Promise<Object>} - Generated file paths
|
|
15
|
+
*/
|
|
16
|
+
async generate(options) {
|
|
17
|
+
const { projectPath, features = [], isTypeScript } = options;
|
|
18
|
+
|
|
19
|
+
const results = {};
|
|
20
|
+
|
|
21
|
+
// Detect if using Expo Router or React Navigation
|
|
22
|
+
const hasExpoRouter = this.hasExpoRouter(projectPath);
|
|
23
|
+
|
|
24
|
+
// Determine output directory
|
|
25
|
+
let outputDir;
|
|
26
|
+
if (hasExpoRouter) {
|
|
27
|
+
// Expo Router uses app/ directory with file-based routing
|
|
28
|
+
outputDir = path.join(projectPath, 'app', '(tabs)', 'l4yercak3');
|
|
29
|
+
} else if (fs.existsSync(path.join(projectPath, 'src', 'screens'))) {
|
|
30
|
+
outputDir = path.join(projectPath, 'src', 'screens', 'l4yercak3');
|
|
31
|
+
} else if (fs.existsSync(path.join(projectPath, 'screens'))) {
|
|
32
|
+
outputDir = path.join(projectPath, 'screens', 'l4yercak3');
|
|
33
|
+
} else {
|
|
34
|
+
outputDir = path.join(projectPath, 'src', 'screens', 'l4yercak3');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ensureDir(outputDir);
|
|
38
|
+
|
|
39
|
+
const ext = isTypeScript ? 'tsx' : 'jsx';
|
|
40
|
+
|
|
41
|
+
// Generate CRM screens
|
|
42
|
+
if (features.includes('crm')) {
|
|
43
|
+
results.contactsScreen = await this.generateContactsScreen(outputDir, ext, isTypeScript, hasExpoRouter);
|
|
44
|
+
results.contactDetailScreen = await this.generateContactDetailScreen(outputDir, ext, isTypeScript, hasExpoRouter);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Generate Events screens
|
|
48
|
+
if (features.includes('events')) {
|
|
49
|
+
results.eventsScreen = await this.generateEventsScreen(outputDir, ext, isTypeScript, hasExpoRouter);
|
|
50
|
+
results.eventDetailScreen = await this.generateEventDetailScreen(outputDir, ext, isTypeScript, hasExpoRouter);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Generate Products screens
|
|
54
|
+
if (features.includes('products') || features.includes('checkout')) {
|
|
55
|
+
results.productsScreen = await this.generateProductsScreen(outputDir, ext, isTypeScript, hasExpoRouter);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Generate layout file for Expo Router
|
|
59
|
+
if (hasExpoRouter) {
|
|
60
|
+
results.layout = await this.generateExpoRouterLayout(outputDir, ext, isTypeScript, features);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if project uses Expo Router
|
|
68
|
+
*/
|
|
69
|
+
hasExpoRouter(projectPath) {
|
|
70
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
71
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
77
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
78
|
+
return !!deps['expo-router'];
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async generateContactsScreen(outputDir, ext, isTypeScript, hasExpoRouter) {
|
|
85
|
+
const fileName = hasExpoRouter ? 'contacts' : 'ContactsScreen';
|
|
86
|
+
const outputPath = path.join(outputDir, `${fileName}.${ext}`);
|
|
87
|
+
|
|
88
|
+
const action = await checkFileOverwrite(outputPath);
|
|
89
|
+
if (action === 'skip') {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const content = isTypeScript
|
|
94
|
+
? this.getContactsScreenTS(hasExpoRouter)
|
|
95
|
+
: this.getContactsScreenJS(hasExpoRouter);
|
|
96
|
+
|
|
97
|
+
return writeFileWithBackup(outputPath, content, action);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getContactsScreenTS(hasExpoRouter) {
|
|
101
|
+
const navigation = hasExpoRouter
|
|
102
|
+
? `import { useRouter } from 'expo-router';`
|
|
103
|
+
: `import { useNavigation } from '@react-navigation/native';
|
|
104
|
+
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';`;
|
|
105
|
+
|
|
106
|
+
const navHook = hasExpoRouter
|
|
107
|
+
? `const router = useRouter();`
|
|
108
|
+
: `const navigation = useNavigation<NativeStackNavigationProp<any>>();`;
|
|
109
|
+
|
|
110
|
+
const navAction = hasExpoRouter
|
|
111
|
+
? `router.push(\`/l4yercak3/contact/\${contact.id}\`);`
|
|
112
|
+
: `navigation.navigate('ContactDetail', { id: contact.id });`;
|
|
113
|
+
|
|
114
|
+
return `/**
|
|
115
|
+
* Contacts Screen (Expo)
|
|
116
|
+
* Displays list of contacts with search
|
|
117
|
+
* Auto-generated by @l4yercak3/cli
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
import React from 'react';
|
|
121
|
+
import { SafeAreaView, StyleSheet } from 'react-native';
|
|
122
|
+
${navigation}
|
|
123
|
+
import { ContactList } from '../../components/l4yercak3';
|
|
124
|
+
import type { Contact } from '../../lib/l4yercak3/types';
|
|
125
|
+
|
|
126
|
+
export default function ContactsScreen() {
|
|
127
|
+
${navHook}
|
|
128
|
+
|
|
129
|
+
const handleSelectContact = (contact: Contact) => {
|
|
130
|
+
${navAction}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<SafeAreaView style={styles.container}>
|
|
135
|
+
<ContactList onSelect={handleSelectContact} />
|
|
136
|
+
</SafeAreaView>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const styles = StyleSheet.create({
|
|
141
|
+
container: {
|
|
142
|
+
flex: 1,
|
|
143
|
+
backgroundColor: '#F9FAFB',
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getContactsScreenJS(hasExpoRouter) {
|
|
150
|
+
const navigation = hasExpoRouter
|
|
151
|
+
? `import { useRouter } from 'expo-router';`
|
|
152
|
+
: `import { useNavigation } from '@react-navigation/native';`;
|
|
153
|
+
|
|
154
|
+
const navHook = hasExpoRouter
|
|
155
|
+
? `const router = useRouter();`
|
|
156
|
+
: `const navigation = useNavigation();`;
|
|
157
|
+
|
|
158
|
+
const navAction = hasExpoRouter
|
|
159
|
+
? `router.push(\`/l4yercak3/contact/\${contact.id}\`);`
|
|
160
|
+
: `navigation.navigate('ContactDetail', { id: contact.id });`;
|
|
161
|
+
|
|
162
|
+
return `/**
|
|
163
|
+
* Contacts Screen (Expo)
|
|
164
|
+
* Displays list of contacts with search
|
|
165
|
+
* Auto-generated by @l4yercak3/cli
|
|
166
|
+
*/
|
|
167
|
+
|
|
168
|
+
import React from 'react';
|
|
169
|
+
import { SafeAreaView, StyleSheet } from 'react-native';
|
|
170
|
+
${navigation}
|
|
171
|
+
import { ContactList } from '../../components/l4yercak3';
|
|
172
|
+
|
|
173
|
+
export default function ContactsScreen() {
|
|
174
|
+
${navHook}
|
|
175
|
+
|
|
176
|
+
const handleSelectContact = (contact) => {
|
|
177
|
+
${navAction}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<SafeAreaView style={styles.container}>
|
|
182
|
+
<ContactList onSelect={handleSelectContact} />
|
|
183
|
+
</SafeAreaView>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const styles = StyleSheet.create({
|
|
188
|
+
container: {
|
|
189
|
+
flex: 1,
|
|
190
|
+
backgroundColor: '#F9FAFB',
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async generateContactDetailScreen(outputDir, ext, isTypeScript, hasExpoRouter) {
|
|
197
|
+
const fileName = hasExpoRouter ? 'contact/[id]' : 'ContactDetailScreen';
|
|
198
|
+
|
|
199
|
+
// For Expo Router, create the subdirectory
|
|
200
|
+
let outputPath;
|
|
201
|
+
if (hasExpoRouter) {
|
|
202
|
+
const contactDir = path.join(outputDir, 'contact');
|
|
203
|
+
ensureDir(contactDir);
|
|
204
|
+
outputPath = path.join(contactDir, `[id].${ext}`);
|
|
205
|
+
} else {
|
|
206
|
+
outputPath = path.join(outputDir, `${fileName}.${ext}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const action = await checkFileOverwrite(outputPath);
|
|
210
|
+
if (action === 'skip') {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const content = isTypeScript
|
|
215
|
+
? this.getContactDetailScreenTS(hasExpoRouter)
|
|
216
|
+
: this.getContactDetailScreenJS(hasExpoRouter);
|
|
217
|
+
|
|
218
|
+
return writeFileWithBackup(outputPath, content, action);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
getContactDetailScreenTS(hasExpoRouter) {
|
|
222
|
+
const params = hasExpoRouter
|
|
223
|
+
? `import { useLocalSearchParams } from 'expo-router';`
|
|
224
|
+
: `import { useRoute } from '@react-navigation/native';
|
|
225
|
+
import type { RouteProp } from '@react-navigation/native';`;
|
|
226
|
+
|
|
227
|
+
const paramsHook = hasExpoRouter
|
|
228
|
+
? `const { id } = useLocalSearchParams<{ id: string }>();`
|
|
229
|
+
: `const route = useRoute<RouteProp<{ params: { id: string } }>>();
|
|
230
|
+
const { id } = route.params;`;
|
|
231
|
+
|
|
232
|
+
return `/**
|
|
233
|
+
* Contact Detail Screen (Expo)
|
|
234
|
+
* Displays individual contact details
|
|
235
|
+
* Auto-generated by @l4yercak3/cli
|
|
236
|
+
*/
|
|
237
|
+
|
|
238
|
+
import React from 'react';
|
|
239
|
+
import {
|
|
240
|
+
View,
|
|
241
|
+
Text,
|
|
242
|
+
ScrollView,
|
|
243
|
+
ActivityIndicator,
|
|
244
|
+
StyleSheet,
|
|
245
|
+
SafeAreaView,
|
|
246
|
+
} from 'react-native';
|
|
247
|
+
${params}
|
|
248
|
+
import { useContact } from '../../lib/l4yercak3/hooks/use-contacts';
|
|
249
|
+
|
|
250
|
+
export default function ContactDetailScreen() {
|
|
251
|
+
${paramsHook}
|
|
252
|
+
|
|
253
|
+
const { data: contact, isLoading, error } = useContact(id);
|
|
254
|
+
|
|
255
|
+
if (isLoading) {
|
|
256
|
+
return (
|
|
257
|
+
<View style={styles.centered}>
|
|
258
|
+
<ActivityIndicator size="large" color="#3B82F6" />
|
|
259
|
+
</View>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (error || !contact) {
|
|
264
|
+
return (
|
|
265
|
+
<View style={styles.centered}>
|
|
266
|
+
<Text style={styles.errorText}>Failed to load contact</Text>
|
|
267
|
+
</View>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<SafeAreaView style={styles.container}>
|
|
273
|
+
<ScrollView contentContainerStyle={styles.content}>
|
|
274
|
+
{/* Avatar */}
|
|
275
|
+
<View style={styles.avatarContainer}>
|
|
276
|
+
<View style={styles.avatar}>
|
|
277
|
+
<Text style={styles.avatarText}>
|
|
278
|
+
{(contact.firstName?.[0] || '') + (contact.lastName?.[0] || '')}
|
|
279
|
+
</Text>
|
|
280
|
+
</View>
|
|
281
|
+
<Text style={styles.name}>
|
|
282
|
+
{contact.firstName} {contact.lastName}
|
|
283
|
+
</Text>
|
|
284
|
+
{contact.email && (
|
|
285
|
+
<Text style={styles.email}>{contact.email}</Text>
|
|
286
|
+
)}
|
|
287
|
+
</View>
|
|
288
|
+
|
|
289
|
+
{/* Details */}
|
|
290
|
+
<View style={styles.section}>
|
|
291
|
+
<Text style={styles.sectionTitle}>Details</Text>
|
|
292
|
+
|
|
293
|
+
{contact.phone && (
|
|
294
|
+
<View style={styles.row}>
|
|
295
|
+
<Text style={styles.label}>Phone</Text>
|
|
296
|
+
<Text style={styles.value}>{contact.phone}</Text>
|
|
297
|
+
</View>
|
|
298
|
+
)}
|
|
299
|
+
|
|
300
|
+
{contact.company && (
|
|
301
|
+
<View style={styles.row}>
|
|
302
|
+
<Text style={styles.label}>Company</Text>
|
|
303
|
+
<Text style={styles.value}>{contact.company}</Text>
|
|
304
|
+
</View>
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
{contact.status && (
|
|
308
|
+
<View style={styles.row}>
|
|
309
|
+
<Text style={styles.label}>Status</Text>
|
|
310
|
+
<Text style={styles.value}>{contact.status}</Text>
|
|
311
|
+
</View>
|
|
312
|
+
)}
|
|
313
|
+
</View>
|
|
314
|
+
|
|
315
|
+
{/* Tags */}
|
|
316
|
+
{contact.tags && contact.tags.length > 0 && (
|
|
317
|
+
<View style={styles.section}>
|
|
318
|
+
<Text style={styles.sectionTitle}>Tags</Text>
|
|
319
|
+
<View style={styles.tags}>
|
|
320
|
+
{contact.tags.map((tag: string) => (
|
|
321
|
+
<View key={tag} style={styles.tag}>
|
|
322
|
+
<Text style={styles.tagText}>{tag}</Text>
|
|
323
|
+
</View>
|
|
324
|
+
))}
|
|
325
|
+
</View>
|
|
326
|
+
</View>
|
|
327
|
+
)}
|
|
328
|
+
</ScrollView>
|
|
329
|
+
</SafeAreaView>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const styles = StyleSheet.create({
|
|
334
|
+
container: {
|
|
335
|
+
flex: 1,
|
|
336
|
+
backgroundColor: '#F9FAFB',
|
|
337
|
+
},
|
|
338
|
+
centered: {
|
|
339
|
+
flex: 1,
|
|
340
|
+
justifyContent: 'center',
|
|
341
|
+
alignItems: 'center',
|
|
342
|
+
},
|
|
343
|
+
errorText: {
|
|
344
|
+
color: '#EF4444',
|
|
345
|
+
fontSize: 16,
|
|
346
|
+
},
|
|
347
|
+
content: {
|
|
348
|
+
padding: 16,
|
|
349
|
+
},
|
|
350
|
+
avatarContainer: {
|
|
351
|
+
alignItems: 'center',
|
|
352
|
+
paddingVertical: 24,
|
|
353
|
+
},
|
|
354
|
+
avatar: {
|
|
355
|
+
width: 96,
|
|
356
|
+
height: 96,
|
|
357
|
+
borderRadius: 48,
|
|
358
|
+
backgroundColor: '#DBEAFE',
|
|
359
|
+
justifyContent: 'center',
|
|
360
|
+
alignItems: 'center',
|
|
361
|
+
marginBottom: 16,
|
|
362
|
+
},
|
|
363
|
+
avatarText: {
|
|
364
|
+
color: '#3B82F6',
|
|
365
|
+
fontSize: 32,
|
|
366
|
+
fontWeight: '600',
|
|
367
|
+
},
|
|
368
|
+
name: {
|
|
369
|
+
fontSize: 24,
|
|
370
|
+
fontWeight: '700',
|
|
371
|
+
color: '#111827',
|
|
372
|
+
},
|
|
373
|
+
email: {
|
|
374
|
+
fontSize: 16,
|
|
375
|
+
color: '#6B7280',
|
|
376
|
+
marginTop: 4,
|
|
377
|
+
},
|
|
378
|
+
section: {
|
|
379
|
+
backgroundColor: '#FFFFFF',
|
|
380
|
+
borderRadius: 12,
|
|
381
|
+
padding: 16,
|
|
382
|
+
marginTop: 16,
|
|
383
|
+
},
|
|
384
|
+
sectionTitle: {
|
|
385
|
+
fontSize: 18,
|
|
386
|
+
fontWeight: '600',
|
|
387
|
+
color: '#111827',
|
|
388
|
+
marginBottom: 12,
|
|
389
|
+
},
|
|
390
|
+
row: {
|
|
391
|
+
flexDirection: 'row',
|
|
392
|
+
justifyContent: 'space-between',
|
|
393
|
+
paddingVertical: 8,
|
|
394
|
+
borderBottomWidth: 1,
|
|
395
|
+
borderBottomColor: '#F3F4F6',
|
|
396
|
+
},
|
|
397
|
+
label: {
|
|
398
|
+
fontSize: 14,
|
|
399
|
+
color: '#6B7280',
|
|
400
|
+
},
|
|
401
|
+
value: {
|
|
402
|
+
fontSize: 14,
|
|
403
|
+
color: '#111827',
|
|
404
|
+
fontWeight: '500',
|
|
405
|
+
},
|
|
406
|
+
tags: {
|
|
407
|
+
flexDirection: 'row',
|
|
408
|
+
flexWrap: 'wrap',
|
|
409
|
+
gap: 8,
|
|
410
|
+
},
|
|
411
|
+
tag: {
|
|
412
|
+
backgroundColor: '#F3F4F6',
|
|
413
|
+
paddingHorizontal: 12,
|
|
414
|
+
paddingVertical: 6,
|
|
415
|
+
borderRadius: 16,
|
|
416
|
+
},
|
|
417
|
+
tagText: {
|
|
418
|
+
fontSize: 14,
|
|
419
|
+
color: '#6B7280',
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
getContactDetailScreenJS(hasExpoRouter) {
|
|
426
|
+
const params = hasExpoRouter
|
|
427
|
+
? `import { useLocalSearchParams } from 'expo-router';`
|
|
428
|
+
: `import { useRoute } from '@react-navigation/native';`;
|
|
429
|
+
|
|
430
|
+
const paramsHook = hasExpoRouter
|
|
431
|
+
? `const { id } = useLocalSearchParams();`
|
|
432
|
+
: `const route = useRoute();
|
|
433
|
+
const { id } = route.params;`;
|
|
434
|
+
|
|
435
|
+
return `/**
|
|
436
|
+
* Contact Detail Screen (Expo)
|
|
437
|
+
* Displays individual contact details
|
|
438
|
+
* Auto-generated by @l4yercak3/cli
|
|
439
|
+
*/
|
|
440
|
+
|
|
441
|
+
import React from 'react';
|
|
442
|
+
import {
|
|
443
|
+
View,
|
|
444
|
+
Text,
|
|
445
|
+
ScrollView,
|
|
446
|
+
ActivityIndicator,
|
|
447
|
+
StyleSheet,
|
|
448
|
+
SafeAreaView,
|
|
449
|
+
} from 'react-native';
|
|
450
|
+
${params}
|
|
451
|
+
import { useContact } from '../../lib/l4yercak3/hooks/use-contacts';
|
|
452
|
+
|
|
453
|
+
export default function ContactDetailScreen() {
|
|
454
|
+
${paramsHook}
|
|
455
|
+
|
|
456
|
+
const { data: contact, isLoading, error } = useContact(id);
|
|
457
|
+
|
|
458
|
+
if (isLoading) {
|
|
459
|
+
return (
|
|
460
|
+
<View style={styles.centered}>
|
|
461
|
+
<ActivityIndicator size="large" color="#3B82F6" />
|
|
462
|
+
</View>
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (error || !contact) {
|
|
467
|
+
return (
|
|
468
|
+
<View style={styles.centered}>
|
|
469
|
+
<Text style={styles.errorText}>Failed to load contact</Text>
|
|
470
|
+
</View>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return (
|
|
475
|
+
<SafeAreaView style={styles.container}>
|
|
476
|
+
<ScrollView contentContainerStyle={styles.content}>
|
|
477
|
+
{/* Avatar */}
|
|
478
|
+
<View style={styles.avatarContainer}>
|
|
479
|
+
<View style={styles.avatar}>
|
|
480
|
+
<Text style={styles.avatarText}>
|
|
481
|
+
{(contact.firstName?.[0] || '') + (contact.lastName?.[0] || '')}
|
|
482
|
+
</Text>
|
|
483
|
+
</View>
|
|
484
|
+
<Text style={styles.name}>
|
|
485
|
+
{contact.firstName} {contact.lastName}
|
|
486
|
+
</Text>
|
|
487
|
+
{contact.email && (
|
|
488
|
+
<Text style={styles.email}>{contact.email}</Text>
|
|
489
|
+
)}
|
|
490
|
+
</View>
|
|
491
|
+
|
|
492
|
+
{/* Details */}
|
|
493
|
+
<View style={styles.section}>
|
|
494
|
+
<Text style={styles.sectionTitle}>Details</Text>
|
|
495
|
+
|
|
496
|
+
{contact.phone && (
|
|
497
|
+
<View style={styles.row}>
|
|
498
|
+
<Text style={styles.label}>Phone</Text>
|
|
499
|
+
<Text style={styles.value}>{contact.phone}</Text>
|
|
500
|
+
</View>
|
|
501
|
+
)}
|
|
502
|
+
|
|
503
|
+
{contact.company && (
|
|
504
|
+
<View style={styles.row}>
|
|
505
|
+
<Text style={styles.label}>Company</Text>
|
|
506
|
+
<Text style={styles.value}>{contact.company}</Text>
|
|
507
|
+
</View>
|
|
508
|
+
)}
|
|
509
|
+
|
|
510
|
+
{contact.status && (
|
|
511
|
+
<View style={styles.row}>
|
|
512
|
+
<Text style={styles.label}>Status</Text>
|
|
513
|
+
<Text style={styles.value}>{contact.status}</Text>
|
|
514
|
+
</View>
|
|
515
|
+
)}
|
|
516
|
+
</View>
|
|
517
|
+
|
|
518
|
+
{/* Tags */}
|
|
519
|
+
{contact.tags && contact.tags.length > 0 && (
|
|
520
|
+
<View style={styles.section}>
|
|
521
|
+
<Text style={styles.sectionTitle}>Tags</Text>
|
|
522
|
+
<View style={styles.tags}>
|
|
523
|
+
{contact.tags.map((tag) => (
|
|
524
|
+
<View key={tag} style={styles.tag}>
|
|
525
|
+
<Text style={styles.tagText}>{tag}</Text>
|
|
526
|
+
</View>
|
|
527
|
+
))}
|
|
528
|
+
</View>
|
|
529
|
+
</View>
|
|
530
|
+
)}
|
|
531
|
+
</ScrollView>
|
|
532
|
+
</SafeAreaView>
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const styles = StyleSheet.create({
|
|
537
|
+
container: {
|
|
538
|
+
flex: 1,
|
|
539
|
+
backgroundColor: '#F9FAFB',
|
|
540
|
+
},
|
|
541
|
+
centered: {
|
|
542
|
+
flex: 1,
|
|
543
|
+
justifyContent: 'center',
|
|
544
|
+
alignItems: 'center',
|
|
545
|
+
},
|
|
546
|
+
errorText: {
|
|
547
|
+
color: '#EF4444',
|
|
548
|
+
fontSize: 16,
|
|
549
|
+
},
|
|
550
|
+
content: {
|
|
551
|
+
padding: 16,
|
|
552
|
+
},
|
|
553
|
+
avatarContainer: {
|
|
554
|
+
alignItems: 'center',
|
|
555
|
+
paddingVertical: 24,
|
|
556
|
+
},
|
|
557
|
+
avatar: {
|
|
558
|
+
width: 96,
|
|
559
|
+
height: 96,
|
|
560
|
+
borderRadius: 48,
|
|
561
|
+
backgroundColor: '#DBEAFE',
|
|
562
|
+
justifyContent: 'center',
|
|
563
|
+
alignItems: 'center',
|
|
564
|
+
marginBottom: 16,
|
|
565
|
+
},
|
|
566
|
+
avatarText: {
|
|
567
|
+
color: '#3B82F6',
|
|
568
|
+
fontSize: 32,
|
|
569
|
+
fontWeight: '600',
|
|
570
|
+
},
|
|
571
|
+
name: {
|
|
572
|
+
fontSize: 24,
|
|
573
|
+
fontWeight: '700',
|
|
574
|
+
color: '#111827',
|
|
575
|
+
},
|
|
576
|
+
email: {
|
|
577
|
+
fontSize: 16,
|
|
578
|
+
color: '#6B7280',
|
|
579
|
+
marginTop: 4,
|
|
580
|
+
},
|
|
581
|
+
section: {
|
|
582
|
+
backgroundColor: '#FFFFFF',
|
|
583
|
+
borderRadius: 12,
|
|
584
|
+
padding: 16,
|
|
585
|
+
marginTop: 16,
|
|
586
|
+
},
|
|
587
|
+
sectionTitle: {
|
|
588
|
+
fontSize: 18,
|
|
589
|
+
fontWeight: '600',
|
|
590
|
+
color: '#111827',
|
|
591
|
+
marginBottom: 12,
|
|
592
|
+
},
|
|
593
|
+
row: {
|
|
594
|
+
flexDirection: 'row',
|
|
595
|
+
justifyContent: 'space-between',
|
|
596
|
+
paddingVertical: 8,
|
|
597
|
+
borderBottomWidth: 1,
|
|
598
|
+
borderBottomColor: '#F3F4F6',
|
|
599
|
+
},
|
|
600
|
+
label: {
|
|
601
|
+
fontSize: 14,
|
|
602
|
+
color: '#6B7280',
|
|
603
|
+
},
|
|
604
|
+
value: {
|
|
605
|
+
fontSize: 14,
|
|
606
|
+
color: '#111827',
|
|
607
|
+
fontWeight: '500',
|
|
608
|
+
},
|
|
609
|
+
tags: {
|
|
610
|
+
flexDirection: 'row',
|
|
611
|
+
flexWrap: 'wrap',
|
|
612
|
+
gap: 8,
|
|
613
|
+
},
|
|
614
|
+
tag: {
|
|
615
|
+
backgroundColor: '#F3F4F6',
|
|
616
|
+
paddingHorizontal: 12,
|
|
617
|
+
paddingVertical: 6,
|
|
618
|
+
borderRadius: 16,
|
|
619
|
+
},
|
|
620
|
+
tagText: {
|
|
621
|
+
fontSize: 14,
|
|
622
|
+
color: '#6B7280',
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
`;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async generateEventsScreen(outputDir, ext, isTypeScript, hasExpoRouter) {
|
|
629
|
+
const fileName = hasExpoRouter ? 'events' : 'EventsScreen';
|
|
630
|
+
const outputPath = path.join(outputDir, `${fileName}.${ext}`);
|
|
631
|
+
|
|
632
|
+
const action = await checkFileOverwrite(outputPath);
|
|
633
|
+
if (action === 'skip') {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const content = isTypeScript
|
|
638
|
+
? this.getEventsScreenTS(hasExpoRouter)
|
|
639
|
+
: this.getEventsScreenJS(hasExpoRouter);
|
|
640
|
+
|
|
641
|
+
return writeFileWithBackup(outputPath, content, action);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
getEventsScreenTS(hasExpoRouter) {
|
|
645
|
+
const navigation = hasExpoRouter
|
|
646
|
+
? `import { useRouter } from 'expo-router';`
|
|
647
|
+
: `import { useNavigation } from '@react-navigation/native';
|
|
648
|
+
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';`;
|
|
649
|
+
|
|
650
|
+
const navHook = hasExpoRouter
|
|
651
|
+
? `const router = useRouter();`
|
|
652
|
+
: `const navigation = useNavigation<NativeStackNavigationProp<any>>();`;
|
|
653
|
+
|
|
654
|
+
const navAction = hasExpoRouter
|
|
655
|
+
? `router.push(\`/l4yercak3/event/\${event.id}\`);`
|
|
656
|
+
: `navigation.navigate('EventDetail', { id: event.id });`;
|
|
657
|
+
|
|
658
|
+
return `/**
|
|
659
|
+
* Events Screen (Expo)
|
|
660
|
+
* Displays list of events with filtering
|
|
661
|
+
* Auto-generated by @l4yercak3/cli
|
|
662
|
+
*/
|
|
663
|
+
|
|
664
|
+
import React from 'react';
|
|
665
|
+
import { SafeAreaView, StyleSheet } from 'react-native';
|
|
666
|
+
${navigation}
|
|
667
|
+
import { EventList } from '../../components/l4yercak3';
|
|
668
|
+
import type { Event } from '../../lib/l4yercak3/types';
|
|
669
|
+
|
|
670
|
+
export default function EventsScreen() {
|
|
671
|
+
${navHook}
|
|
672
|
+
|
|
673
|
+
const handleSelectEvent = (event: Event) => {
|
|
674
|
+
${navAction}
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
return (
|
|
678
|
+
<SafeAreaView style={styles.container}>
|
|
679
|
+
<EventList onSelect={handleSelectEvent} />
|
|
680
|
+
</SafeAreaView>
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const styles = StyleSheet.create({
|
|
685
|
+
container: {
|
|
686
|
+
flex: 1,
|
|
687
|
+
backgroundColor: '#F9FAFB',
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
`;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
getEventsScreenJS(hasExpoRouter) {
|
|
694
|
+
const navigation = hasExpoRouter
|
|
695
|
+
? `import { useRouter } from 'expo-router';`
|
|
696
|
+
: `import { useNavigation } from '@react-navigation/native';`;
|
|
697
|
+
|
|
698
|
+
const navHook = hasExpoRouter
|
|
699
|
+
? `const router = useRouter();`
|
|
700
|
+
: `const navigation = useNavigation();`;
|
|
701
|
+
|
|
702
|
+
const navAction = hasExpoRouter
|
|
703
|
+
? `router.push(\`/l4yercak3/event/\${event.id}\`);`
|
|
704
|
+
: `navigation.navigate('EventDetail', { id: event.id });`;
|
|
705
|
+
|
|
706
|
+
return `/**
|
|
707
|
+
* Events Screen (Expo)
|
|
708
|
+
* Displays list of events with filtering
|
|
709
|
+
* Auto-generated by @l4yercak3/cli
|
|
710
|
+
*/
|
|
711
|
+
|
|
712
|
+
import React from 'react';
|
|
713
|
+
import { SafeAreaView, StyleSheet } from 'react-native';
|
|
714
|
+
${navigation}
|
|
715
|
+
import { EventList } from '../../components/l4yercak3';
|
|
716
|
+
|
|
717
|
+
export default function EventsScreen() {
|
|
718
|
+
${navHook}
|
|
719
|
+
|
|
720
|
+
const handleSelectEvent = (event) => {
|
|
721
|
+
${navAction}
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
return (
|
|
725
|
+
<SafeAreaView style={styles.container}>
|
|
726
|
+
<EventList onSelect={handleSelectEvent} />
|
|
727
|
+
</SafeAreaView>
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const styles = StyleSheet.create({
|
|
732
|
+
container: {
|
|
733
|
+
flex: 1,
|
|
734
|
+
backgroundColor: '#F9FAFB',
|
|
735
|
+
},
|
|
736
|
+
});
|
|
737
|
+
`;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async generateEventDetailScreen(outputDir, ext, isTypeScript, hasExpoRouter) {
|
|
741
|
+
let outputPath;
|
|
742
|
+
if (hasExpoRouter) {
|
|
743
|
+
const eventDir = path.join(outputDir, 'event');
|
|
744
|
+
ensureDir(eventDir);
|
|
745
|
+
outputPath = path.join(eventDir, `[id].${ext}`);
|
|
746
|
+
} else {
|
|
747
|
+
outputPath = path.join(outputDir, `EventDetailScreen.${ext}`);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const action = await checkFileOverwrite(outputPath);
|
|
751
|
+
if (action === 'skip') {
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const content = isTypeScript
|
|
756
|
+
? this.getEventDetailScreenTS(hasExpoRouter)
|
|
757
|
+
: this.getEventDetailScreenJS(hasExpoRouter);
|
|
758
|
+
|
|
759
|
+
return writeFileWithBackup(outputPath, content, action);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
getEventDetailScreenTS(hasExpoRouter) {
|
|
763
|
+
const params = hasExpoRouter
|
|
764
|
+
? `import { useLocalSearchParams } from 'expo-router';`
|
|
765
|
+
: `import { useRoute } from '@react-navigation/native';
|
|
766
|
+
import type { RouteProp } from '@react-navigation/native';`;
|
|
767
|
+
|
|
768
|
+
const paramsHook = hasExpoRouter
|
|
769
|
+
? `const { id } = useLocalSearchParams<{ id: string }>();`
|
|
770
|
+
: `const route = useRoute<RouteProp<{ params: { id: string } }>>();
|
|
771
|
+
const { id } = route.params;`;
|
|
772
|
+
|
|
773
|
+
return `/**
|
|
774
|
+
* Event Detail Screen (Expo)
|
|
775
|
+
* Displays individual event details
|
|
776
|
+
* Auto-generated by @l4yercak3/cli
|
|
777
|
+
*/
|
|
778
|
+
|
|
779
|
+
import React from 'react';
|
|
780
|
+
import {
|
|
781
|
+
View,
|
|
782
|
+
Text,
|
|
783
|
+
ScrollView,
|
|
784
|
+
ActivityIndicator,
|
|
785
|
+
StyleSheet,
|
|
786
|
+
SafeAreaView,
|
|
787
|
+
} from 'react-native';
|
|
788
|
+
${params}
|
|
789
|
+
import { useEvent } from '../../lib/l4yercak3/hooks/use-events';
|
|
790
|
+
|
|
791
|
+
export default function EventDetailScreen() {
|
|
792
|
+
${paramsHook}
|
|
793
|
+
|
|
794
|
+
const { data: event, isLoading, error } = useEvent(id);
|
|
795
|
+
|
|
796
|
+
if (isLoading) {
|
|
797
|
+
return (
|
|
798
|
+
<View style={styles.centered}>
|
|
799
|
+
<ActivityIndicator size="large" color="#3B82F6" />
|
|
800
|
+
</View>
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (error || !event) {
|
|
805
|
+
return (
|
|
806
|
+
<View style={styles.centered}>
|
|
807
|
+
<Text style={styles.errorText}>Failed to load event</Text>
|
|
808
|
+
</View>
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const startDate = event.startDate ? new Date(event.startDate) : null;
|
|
813
|
+
const endDate = event.endDate ? new Date(event.endDate) : null;
|
|
814
|
+
|
|
815
|
+
const formatDateTime = (date: Date) => {
|
|
816
|
+
return date.toLocaleDateString('en-US', {
|
|
817
|
+
weekday: 'long',
|
|
818
|
+
month: 'long',
|
|
819
|
+
day: 'numeric',
|
|
820
|
+
year: 'numeric',
|
|
821
|
+
hour: 'numeric',
|
|
822
|
+
minute: '2-digit',
|
|
823
|
+
});
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
return (
|
|
827
|
+
<SafeAreaView style={styles.container}>
|
|
828
|
+
<ScrollView contentContainerStyle={styles.content}>
|
|
829
|
+
{/* Header */}
|
|
830
|
+
<View style={styles.header}>
|
|
831
|
+
<View style={styles.statusBadge}>
|
|
832
|
+
<Text style={styles.statusText}>{event.status}</Text>
|
|
833
|
+
</View>
|
|
834
|
+
<Text style={styles.title}>{event.name}</Text>
|
|
835
|
+
{event.description && (
|
|
836
|
+
<Text style={styles.description}>{event.description}</Text>
|
|
837
|
+
)}
|
|
838
|
+
</View>
|
|
839
|
+
|
|
840
|
+
{/* Date & Time */}
|
|
841
|
+
<View style={styles.section}>
|
|
842
|
+
<Text style={styles.sectionTitle}>Date & Time</Text>
|
|
843
|
+
{startDate && (
|
|
844
|
+
<View style={styles.row}>
|
|
845
|
+
<Text style={styles.label}>Starts</Text>
|
|
846
|
+
<Text style={styles.value}>{formatDateTime(startDate)}</Text>
|
|
847
|
+
</View>
|
|
848
|
+
)}
|
|
849
|
+
{endDate && (
|
|
850
|
+
<View style={styles.row}>
|
|
851
|
+
<Text style={styles.label}>Ends</Text>
|
|
852
|
+
<Text style={styles.value}>{formatDateTime(endDate)}</Text>
|
|
853
|
+
</View>
|
|
854
|
+
)}
|
|
855
|
+
</View>
|
|
856
|
+
|
|
857
|
+
{/* Location */}
|
|
858
|
+
{event.location && (
|
|
859
|
+
<View style={styles.section}>
|
|
860
|
+
<Text style={styles.sectionTitle}>Location</Text>
|
|
861
|
+
<Text style={styles.locationText}>{event.location}</Text>
|
|
862
|
+
</View>
|
|
863
|
+
)}
|
|
864
|
+
</ScrollView>
|
|
865
|
+
</SafeAreaView>
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const styles = StyleSheet.create({
|
|
870
|
+
container: {
|
|
871
|
+
flex: 1,
|
|
872
|
+
backgroundColor: '#F9FAFB',
|
|
873
|
+
},
|
|
874
|
+
centered: {
|
|
875
|
+
flex: 1,
|
|
876
|
+
justifyContent: 'center',
|
|
877
|
+
alignItems: 'center',
|
|
878
|
+
},
|
|
879
|
+
errorText: {
|
|
880
|
+
color: '#EF4444',
|
|
881
|
+
fontSize: 16,
|
|
882
|
+
},
|
|
883
|
+
content: {
|
|
884
|
+
padding: 16,
|
|
885
|
+
},
|
|
886
|
+
header: {
|
|
887
|
+
backgroundColor: '#FFFFFF',
|
|
888
|
+
borderRadius: 12,
|
|
889
|
+
padding: 20,
|
|
890
|
+
},
|
|
891
|
+
statusBadge: {
|
|
892
|
+
alignSelf: 'flex-start',
|
|
893
|
+
backgroundColor: '#D1FAE5',
|
|
894
|
+
paddingHorizontal: 12,
|
|
895
|
+
paddingVertical: 4,
|
|
896
|
+
borderRadius: 12,
|
|
897
|
+
marginBottom: 12,
|
|
898
|
+
},
|
|
899
|
+
statusText: {
|
|
900
|
+
color: '#047857',
|
|
901
|
+
fontSize: 12,
|
|
902
|
+
fontWeight: '600',
|
|
903
|
+
textTransform: 'capitalize',
|
|
904
|
+
},
|
|
905
|
+
title: {
|
|
906
|
+
fontSize: 24,
|
|
907
|
+
fontWeight: '700',
|
|
908
|
+
color: '#111827',
|
|
909
|
+
marginBottom: 8,
|
|
910
|
+
},
|
|
911
|
+
description: {
|
|
912
|
+
fontSize: 16,
|
|
913
|
+
color: '#6B7280',
|
|
914
|
+
lineHeight: 24,
|
|
915
|
+
},
|
|
916
|
+
section: {
|
|
917
|
+
backgroundColor: '#FFFFFF',
|
|
918
|
+
borderRadius: 12,
|
|
919
|
+
padding: 16,
|
|
920
|
+
marginTop: 16,
|
|
921
|
+
},
|
|
922
|
+
sectionTitle: {
|
|
923
|
+
fontSize: 18,
|
|
924
|
+
fontWeight: '600',
|
|
925
|
+
color: '#111827',
|
|
926
|
+
marginBottom: 12,
|
|
927
|
+
},
|
|
928
|
+
row: {
|
|
929
|
+
paddingVertical: 8,
|
|
930
|
+
borderBottomWidth: 1,
|
|
931
|
+
borderBottomColor: '#F3F4F6',
|
|
932
|
+
},
|
|
933
|
+
label: {
|
|
934
|
+
fontSize: 12,
|
|
935
|
+
color: '#6B7280',
|
|
936
|
+
marginBottom: 4,
|
|
937
|
+
},
|
|
938
|
+
value: {
|
|
939
|
+
fontSize: 14,
|
|
940
|
+
color: '#111827',
|
|
941
|
+
fontWeight: '500',
|
|
942
|
+
},
|
|
943
|
+
locationText: {
|
|
944
|
+
fontSize: 14,
|
|
945
|
+
color: '#111827',
|
|
946
|
+
},
|
|
947
|
+
});
|
|
948
|
+
`;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
getEventDetailScreenJS(hasExpoRouter) {
|
|
952
|
+
const params = hasExpoRouter
|
|
953
|
+
? `import { useLocalSearchParams } from 'expo-router';`
|
|
954
|
+
: `import { useRoute } from '@react-navigation/native';`;
|
|
955
|
+
|
|
956
|
+
const paramsHook = hasExpoRouter
|
|
957
|
+
? `const { id } = useLocalSearchParams();`
|
|
958
|
+
: `const route = useRoute();
|
|
959
|
+
const { id } = route.params;`;
|
|
960
|
+
|
|
961
|
+
return `/**
|
|
962
|
+
* Event Detail Screen (Expo)
|
|
963
|
+
* Displays individual event details
|
|
964
|
+
* Auto-generated by @l4yercak3/cli
|
|
965
|
+
*/
|
|
966
|
+
|
|
967
|
+
import React from 'react';
|
|
968
|
+
import {
|
|
969
|
+
View,
|
|
970
|
+
Text,
|
|
971
|
+
ScrollView,
|
|
972
|
+
ActivityIndicator,
|
|
973
|
+
StyleSheet,
|
|
974
|
+
SafeAreaView,
|
|
975
|
+
} from 'react-native';
|
|
976
|
+
${params}
|
|
977
|
+
import { useEvent } from '../../lib/l4yercak3/hooks/use-events';
|
|
978
|
+
|
|
979
|
+
export default function EventDetailScreen() {
|
|
980
|
+
${paramsHook}
|
|
981
|
+
|
|
982
|
+
const { data: event, isLoading, error } = useEvent(id);
|
|
983
|
+
|
|
984
|
+
if (isLoading) {
|
|
985
|
+
return (
|
|
986
|
+
<View style={styles.centered}>
|
|
987
|
+
<ActivityIndicator size="large" color="#3B82F6" />
|
|
988
|
+
</View>
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
if (error || !event) {
|
|
993
|
+
return (
|
|
994
|
+
<View style={styles.centered}>
|
|
995
|
+
<Text style={styles.errorText}>Failed to load event</Text>
|
|
996
|
+
</View>
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const startDate = event.startDate ? new Date(event.startDate) : null;
|
|
1001
|
+
const endDate = event.endDate ? new Date(event.endDate) : null;
|
|
1002
|
+
|
|
1003
|
+
const formatDateTime = (date) => {
|
|
1004
|
+
return date.toLocaleDateString('en-US', {
|
|
1005
|
+
weekday: 'long',
|
|
1006
|
+
month: 'long',
|
|
1007
|
+
day: 'numeric',
|
|
1008
|
+
year: 'numeric',
|
|
1009
|
+
hour: 'numeric',
|
|
1010
|
+
minute: '2-digit',
|
|
1011
|
+
});
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
return (
|
|
1015
|
+
<SafeAreaView style={styles.container}>
|
|
1016
|
+
<ScrollView contentContainerStyle={styles.content}>
|
|
1017
|
+
{/* Header */}
|
|
1018
|
+
<View style={styles.header}>
|
|
1019
|
+
<View style={styles.statusBadge}>
|
|
1020
|
+
<Text style={styles.statusText}>{event.status}</Text>
|
|
1021
|
+
</View>
|
|
1022
|
+
<Text style={styles.title}>{event.name}</Text>
|
|
1023
|
+
{event.description && (
|
|
1024
|
+
<Text style={styles.description}>{event.description}</Text>
|
|
1025
|
+
)}
|
|
1026
|
+
</View>
|
|
1027
|
+
|
|
1028
|
+
{/* Date & Time */}
|
|
1029
|
+
<View style={styles.section}>
|
|
1030
|
+
<Text style={styles.sectionTitle}>Date & Time</Text>
|
|
1031
|
+
{startDate && (
|
|
1032
|
+
<View style={styles.row}>
|
|
1033
|
+
<Text style={styles.label}>Starts</Text>
|
|
1034
|
+
<Text style={styles.value}>{formatDateTime(startDate)}</Text>
|
|
1035
|
+
</View>
|
|
1036
|
+
)}
|
|
1037
|
+
{endDate && (
|
|
1038
|
+
<View style={styles.row}>
|
|
1039
|
+
<Text style={styles.label}>Ends</Text>
|
|
1040
|
+
<Text style={styles.value}>{formatDateTime(endDate)}</Text>
|
|
1041
|
+
</View>
|
|
1042
|
+
)}
|
|
1043
|
+
</View>
|
|
1044
|
+
|
|
1045
|
+
{/* Location */}
|
|
1046
|
+
{event.location && (
|
|
1047
|
+
<View style={styles.section}>
|
|
1048
|
+
<Text style={styles.sectionTitle}>Location</Text>
|
|
1049
|
+
<Text style={styles.locationText}>{event.location}</Text>
|
|
1050
|
+
</View>
|
|
1051
|
+
)}
|
|
1052
|
+
</ScrollView>
|
|
1053
|
+
</SafeAreaView>
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
const styles = StyleSheet.create({
|
|
1058
|
+
container: {
|
|
1059
|
+
flex: 1,
|
|
1060
|
+
backgroundColor: '#F9FAFB',
|
|
1061
|
+
},
|
|
1062
|
+
centered: {
|
|
1063
|
+
flex: 1,
|
|
1064
|
+
justifyContent: 'center',
|
|
1065
|
+
alignItems: 'center',
|
|
1066
|
+
},
|
|
1067
|
+
errorText: {
|
|
1068
|
+
color: '#EF4444',
|
|
1069
|
+
fontSize: 16,
|
|
1070
|
+
},
|
|
1071
|
+
content: {
|
|
1072
|
+
padding: 16,
|
|
1073
|
+
},
|
|
1074
|
+
header: {
|
|
1075
|
+
backgroundColor: '#FFFFFF',
|
|
1076
|
+
borderRadius: 12,
|
|
1077
|
+
padding: 20,
|
|
1078
|
+
},
|
|
1079
|
+
statusBadge: {
|
|
1080
|
+
alignSelf: 'flex-start',
|
|
1081
|
+
backgroundColor: '#D1FAE5',
|
|
1082
|
+
paddingHorizontal: 12,
|
|
1083
|
+
paddingVertical: 4,
|
|
1084
|
+
borderRadius: 12,
|
|
1085
|
+
marginBottom: 12,
|
|
1086
|
+
},
|
|
1087
|
+
statusText: {
|
|
1088
|
+
color: '#047857',
|
|
1089
|
+
fontSize: 12,
|
|
1090
|
+
fontWeight: '600',
|
|
1091
|
+
textTransform: 'capitalize',
|
|
1092
|
+
},
|
|
1093
|
+
title: {
|
|
1094
|
+
fontSize: 24,
|
|
1095
|
+
fontWeight: '700',
|
|
1096
|
+
color: '#111827',
|
|
1097
|
+
marginBottom: 8,
|
|
1098
|
+
},
|
|
1099
|
+
description: {
|
|
1100
|
+
fontSize: 16,
|
|
1101
|
+
color: '#6B7280',
|
|
1102
|
+
lineHeight: 24,
|
|
1103
|
+
},
|
|
1104
|
+
section: {
|
|
1105
|
+
backgroundColor: '#FFFFFF',
|
|
1106
|
+
borderRadius: 12,
|
|
1107
|
+
padding: 16,
|
|
1108
|
+
marginTop: 16,
|
|
1109
|
+
},
|
|
1110
|
+
sectionTitle: {
|
|
1111
|
+
fontSize: 18,
|
|
1112
|
+
fontWeight: '600',
|
|
1113
|
+
color: '#111827',
|
|
1114
|
+
marginBottom: 12,
|
|
1115
|
+
},
|
|
1116
|
+
row: {
|
|
1117
|
+
paddingVertical: 8,
|
|
1118
|
+
borderBottomWidth: 1,
|
|
1119
|
+
borderBottomColor: '#F3F4F6',
|
|
1120
|
+
},
|
|
1121
|
+
label: {
|
|
1122
|
+
fontSize: 12,
|
|
1123
|
+
color: '#6B7280',
|
|
1124
|
+
marginBottom: 4,
|
|
1125
|
+
},
|
|
1126
|
+
value: {
|
|
1127
|
+
fontSize: 14,
|
|
1128
|
+
color: '#111827',
|
|
1129
|
+
fontWeight: '500',
|
|
1130
|
+
},
|
|
1131
|
+
locationText: {
|
|
1132
|
+
fontSize: 14,
|
|
1133
|
+
color: '#111827',
|
|
1134
|
+
},
|
|
1135
|
+
});
|
|
1136
|
+
`;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
async generateProductsScreen(outputDir, ext, isTypeScript, hasExpoRouter) {
|
|
1140
|
+
const fileName = hasExpoRouter ? 'products' : 'ProductsScreen';
|
|
1141
|
+
const outputPath = path.join(outputDir, `${fileName}.${ext}`);
|
|
1142
|
+
|
|
1143
|
+
const action = await checkFileOverwrite(outputPath);
|
|
1144
|
+
if (action === 'skip') {
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const content = isTypeScript
|
|
1149
|
+
? this.getProductsScreenTS(hasExpoRouter)
|
|
1150
|
+
: this.getProductsScreenJS(hasExpoRouter);
|
|
1151
|
+
|
|
1152
|
+
return writeFileWithBackup(outputPath, content, action);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
getProductsScreenTS(_hasExpoRouter) {
|
|
1156
|
+
return `/**
|
|
1157
|
+
* Products Screen (Expo)
|
|
1158
|
+
* Displays list of products with grid layout
|
|
1159
|
+
* Auto-generated by @l4yercak3/cli
|
|
1160
|
+
*/
|
|
1161
|
+
|
|
1162
|
+
import React, { useState } from 'react';
|
|
1163
|
+
import {
|
|
1164
|
+
View,
|
|
1165
|
+
FlatList,
|
|
1166
|
+
ActivityIndicator,
|
|
1167
|
+
StyleSheet,
|
|
1168
|
+
SafeAreaView,
|
|
1169
|
+
Text,
|
|
1170
|
+
Alert,
|
|
1171
|
+
RefreshControl,
|
|
1172
|
+
} from 'react-native';
|
|
1173
|
+
import { useProducts } from '../../lib/l4yercak3/hooks/use-products';
|
|
1174
|
+
import { ProductCard } from '../../components/l4yercak3';
|
|
1175
|
+
import type { Product } from '../../lib/l4yercak3/types';
|
|
1176
|
+
|
|
1177
|
+
export default function ProductsScreen() {
|
|
1178
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
1179
|
+
const { data, isLoading, error, refetch } = useProducts();
|
|
1180
|
+
|
|
1181
|
+
const products = data?.products || [];
|
|
1182
|
+
|
|
1183
|
+
const handleRefresh = async () => {
|
|
1184
|
+
setRefreshing(true);
|
|
1185
|
+
await refetch();
|
|
1186
|
+
setRefreshing(false);
|
|
1187
|
+
};
|
|
1188
|
+
|
|
1189
|
+
const handleAddToCart = (product: Product) => {
|
|
1190
|
+
Alert.alert(
|
|
1191
|
+
'Added to Cart',
|
|
1192
|
+
\`\${product.name} has been added to your cart.\`,
|
|
1193
|
+
[{ text: 'OK' }]
|
|
1194
|
+
);
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
if (isLoading && !refreshing) {
|
|
1198
|
+
return (
|
|
1199
|
+
<View style={styles.centered}>
|
|
1200
|
+
<ActivityIndicator size="large" color="#3B82F6" />
|
|
1201
|
+
</View>
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
if (error) {
|
|
1206
|
+
return (
|
|
1207
|
+
<View style={styles.centered}>
|
|
1208
|
+
<Text style={styles.errorText}>Failed to load products</Text>
|
|
1209
|
+
</View>
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
return (
|
|
1214
|
+
<SafeAreaView style={styles.container}>
|
|
1215
|
+
<FlatList
|
|
1216
|
+
data={products}
|
|
1217
|
+
keyExtractor={(item) => item.id}
|
|
1218
|
+
numColumns={2}
|
|
1219
|
+
renderItem={({ item }) => (
|
|
1220
|
+
<View style={styles.cardContainer}>
|
|
1221
|
+
<ProductCard
|
|
1222
|
+
product={item}
|
|
1223
|
+
onAddToCart={handleAddToCart}
|
|
1224
|
+
/>
|
|
1225
|
+
</View>
|
|
1226
|
+
)}
|
|
1227
|
+
refreshControl={
|
|
1228
|
+
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
|
1229
|
+
}
|
|
1230
|
+
contentContainerStyle={styles.list}
|
|
1231
|
+
columnWrapperStyle={styles.row}
|
|
1232
|
+
ListEmptyComponent={
|
|
1233
|
+
<View style={styles.emptyContainer}>
|
|
1234
|
+
<Text style={styles.emptyText}>No products available</Text>
|
|
1235
|
+
</View>
|
|
1236
|
+
}
|
|
1237
|
+
/>
|
|
1238
|
+
</SafeAreaView>
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const styles = StyleSheet.create({
|
|
1243
|
+
container: {
|
|
1244
|
+
flex: 1,
|
|
1245
|
+
backgroundColor: '#F9FAFB',
|
|
1246
|
+
},
|
|
1247
|
+
centered: {
|
|
1248
|
+
flex: 1,
|
|
1249
|
+
justifyContent: 'center',
|
|
1250
|
+
alignItems: 'center',
|
|
1251
|
+
},
|
|
1252
|
+
errorText: {
|
|
1253
|
+
color: '#EF4444',
|
|
1254
|
+
fontSize: 16,
|
|
1255
|
+
},
|
|
1256
|
+
list: {
|
|
1257
|
+
padding: 8,
|
|
1258
|
+
},
|
|
1259
|
+
row: {
|
|
1260
|
+
justifyContent: 'space-between',
|
|
1261
|
+
},
|
|
1262
|
+
cardContainer: {
|
|
1263
|
+
flex: 0.48,
|
|
1264
|
+
marginBottom: 16,
|
|
1265
|
+
},
|
|
1266
|
+
emptyContainer: {
|
|
1267
|
+
padding: 32,
|
|
1268
|
+
alignItems: 'center',
|
|
1269
|
+
},
|
|
1270
|
+
emptyText: {
|
|
1271
|
+
color: '#6B7280',
|
|
1272
|
+
fontSize: 16,
|
|
1273
|
+
},
|
|
1274
|
+
});
|
|
1275
|
+
`;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
getProductsScreenJS(_hasExpoRouter) {
|
|
1279
|
+
return `/**
|
|
1280
|
+
* Products Screen (Expo)
|
|
1281
|
+
* Displays list of products with grid layout
|
|
1282
|
+
* Auto-generated by @l4yercak3/cli
|
|
1283
|
+
*/
|
|
1284
|
+
|
|
1285
|
+
import React, { useState } from 'react';
|
|
1286
|
+
import {
|
|
1287
|
+
View,
|
|
1288
|
+
FlatList,
|
|
1289
|
+
ActivityIndicator,
|
|
1290
|
+
StyleSheet,
|
|
1291
|
+
SafeAreaView,
|
|
1292
|
+
Text,
|
|
1293
|
+
Alert,
|
|
1294
|
+
RefreshControl,
|
|
1295
|
+
} from 'react-native';
|
|
1296
|
+
import { useProducts } from '../../lib/l4yercak3/hooks/use-products';
|
|
1297
|
+
import { ProductCard } from '../../components/l4yercak3';
|
|
1298
|
+
|
|
1299
|
+
export default function ProductsScreen() {
|
|
1300
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
1301
|
+
const { data, isLoading, error, refetch } = useProducts();
|
|
1302
|
+
|
|
1303
|
+
const products = data?.products || [];
|
|
1304
|
+
|
|
1305
|
+
const handleRefresh = async () => {
|
|
1306
|
+
setRefreshing(true);
|
|
1307
|
+
await refetch();
|
|
1308
|
+
setRefreshing(false);
|
|
1309
|
+
};
|
|
1310
|
+
|
|
1311
|
+
const handleAddToCart = (product) => {
|
|
1312
|
+
Alert.alert(
|
|
1313
|
+
'Added to Cart',
|
|
1314
|
+
\`\${product.name} has been added to your cart.\`,
|
|
1315
|
+
[{ text: 'OK' }]
|
|
1316
|
+
);
|
|
1317
|
+
};
|
|
1318
|
+
|
|
1319
|
+
if (isLoading && !refreshing) {
|
|
1320
|
+
return (
|
|
1321
|
+
<View style={styles.centered}>
|
|
1322
|
+
<ActivityIndicator size="large" color="#3B82F6" />
|
|
1323
|
+
</View>
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
if (error) {
|
|
1328
|
+
return (
|
|
1329
|
+
<View style={styles.centered}>
|
|
1330
|
+
<Text style={styles.errorText}>Failed to load products</Text>
|
|
1331
|
+
</View>
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
return (
|
|
1336
|
+
<SafeAreaView style={styles.container}>
|
|
1337
|
+
<FlatList
|
|
1338
|
+
data={products}
|
|
1339
|
+
keyExtractor={(item) => item.id}
|
|
1340
|
+
numColumns={2}
|
|
1341
|
+
renderItem={({ item }) => (
|
|
1342
|
+
<View style={styles.cardContainer}>
|
|
1343
|
+
<ProductCard
|
|
1344
|
+
product={item}
|
|
1345
|
+
onAddToCart={handleAddToCart}
|
|
1346
|
+
/>
|
|
1347
|
+
</View>
|
|
1348
|
+
)}
|
|
1349
|
+
refreshControl={
|
|
1350
|
+
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
|
1351
|
+
}
|
|
1352
|
+
contentContainerStyle={styles.list}
|
|
1353
|
+
columnWrapperStyle={styles.row}
|
|
1354
|
+
ListEmptyComponent={
|
|
1355
|
+
<View style={styles.emptyContainer}>
|
|
1356
|
+
<Text style={styles.emptyText}>No products available</Text>
|
|
1357
|
+
</View>
|
|
1358
|
+
}
|
|
1359
|
+
/>
|
|
1360
|
+
</SafeAreaView>
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const styles = StyleSheet.create({
|
|
1365
|
+
container: {
|
|
1366
|
+
flex: 1,
|
|
1367
|
+
backgroundColor: '#F9FAFB',
|
|
1368
|
+
},
|
|
1369
|
+
centered: {
|
|
1370
|
+
flex: 1,
|
|
1371
|
+
justifyContent: 'center',
|
|
1372
|
+
alignItems: 'center',
|
|
1373
|
+
},
|
|
1374
|
+
errorText: {
|
|
1375
|
+
color: '#EF4444',
|
|
1376
|
+
fontSize: 16,
|
|
1377
|
+
},
|
|
1378
|
+
list: {
|
|
1379
|
+
padding: 8,
|
|
1380
|
+
},
|
|
1381
|
+
row: {
|
|
1382
|
+
justifyContent: 'space-between',
|
|
1383
|
+
},
|
|
1384
|
+
cardContainer: {
|
|
1385
|
+
flex: 0.48,
|
|
1386
|
+
marginBottom: 16,
|
|
1387
|
+
},
|
|
1388
|
+
emptyContainer: {
|
|
1389
|
+
padding: 32,
|
|
1390
|
+
alignItems: 'center',
|
|
1391
|
+
},
|
|
1392
|
+
emptyText: {
|
|
1393
|
+
color: '#6B7280',
|
|
1394
|
+
fontSize: 16,
|
|
1395
|
+
},
|
|
1396
|
+
});
|
|
1397
|
+
`;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
async generateExpoRouterLayout(outputDir, ext, isTypeScript, features) {
|
|
1401
|
+
const outputPath = path.join(outputDir, `_layout.${ext}`);
|
|
1402
|
+
|
|
1403
|
+
const action = await checkFileOverwrite(outputPath);
|
|
1404
|
+
if (action === 'skip') {
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
const content = isTypeScript
|
|
1409
|
+
? this.getExpoRouterLayoutTS(features)
|
|
1410
|
+
: this.getExpoRouterLayoutJS(features);
|
|
1411
|
+
|
|
1412
|
+
return writeFileWithBackup(outputPath, content, action);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
getExpoRouterLayoutTS(features) {
|
|
1416
|
+
const screens = [];
|
|
1417
|
+
if (features.includes('crm')) {
|
|
1418
|
+
screens.push(` <Stack.Screen name="contacts" options={{ title: 'Contacts' }} />`);
|
|
1419
|
+
screens.push(` <Stack.Screen name="contact/[id]" options={{ title: 'Contact Details' }} />`);
|
|
1420
|
+
}
|
|
1421
|
+
if (features.includes('events')) {
|
|
1422
|
+
screens.push(` <Stack.Screen name="events" options={{ title: 'Events' }} />`);
|
|
1423
|
+
screens.push(` <Stack.Screen name="event/[id]" options={{ title: 'Event Details' }} />`);
|
|
1424
|
+
}
|
|
1425
|
+
if (features.includes('products') || features.includes('checkout')) {
|
|
1426
|
+
screens.push(` <Stack.Screen name="products" options={{ title: 'Products' }} />`);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
return `/**
|
|
1430
|
+
* L4YERCAK3 Expo Router Layout
|
|
1431
|
+
* Auto-generated by @l4yercak3/cli
|
|
1432
|
+
*/
|
|
1433
|
+
|
|
1434
|
+
import { Stack } from 'expo-router';
|
|
1435
|
+
|
|
1436
|
+
export default function L4yercak3Layout() {
|
|
1437
|
+
return (
|
|
1438
|
+
<Stack
|
|
1439
|
+
screenOptions={{
|
|
1440
|
+
headerStyle: {
|
|
1441
|
+
backgroundColor: '#3B82F6',
|
|
1442
|
+
},
|
|
1443
|
+
headerTintColor: '#fff',
|
|
1444
|
+
headerTitleStyle: {
|
|
1445
|
+
fontWeight: '600',
|
|
1446
|
+
},
|
|
1447
|
+
}}
|
|
1448
|
+
>
|
|
1449
|
+
${screens.join('\n')}
|
|
1450
|
+
</Stack>
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
`;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
getExpoRouterLayoutJS(features) {
|
|
1457
|
+
const screens = [];
|
|
1458
|
+
if (features.includes('crm')) {
|
|
1459
|
+
screens.push(` <Stack.Screen name="contacts" options={{ title: 'Contacts' }} />`);
|
|
1460
|
+
screens.push(` <Stack.Screen name="contact/[id]" options={{ title: 'Contact Details' }} />`);
|
|
1461
|
+
}
|
|
1462
|
+
if (features.includes('events')) {
|
|
1463
|
+
screens.push(` <Stack.Screen name="events" options={{ title: 'Events' }} />`);
|
|
1464
|
+
screens.push(` <Stack.Screen name="event/[id]" options={{ title: 'Event Details' }} />`);
|
|
1465
|
+
}
|
|
1466
|
+
if (features.includes('products') || features.includes('checkout')) {
|
|
1467
|
+
screens.push(` <Stack.Screen name="products" options={{ title: 'Products' }} />`);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
return `/**
|
|
1471
|
+
* L4YERCAK3 Expo Router Layout
|
|
1472
|
+
* Auto-generated by @l4yercak3/cli
|
|
1473
|
+
*/
|
|
1474
|
+
|
|
1475
|
+
import { Stack } from 'expo-router';
|
|
1476
|
+
|
|
1477
|
+
export default function L4yercak3Layout() {
|
|
1478
|
+
return (
|
|
1479
|
+
<Stack
|
|
1480
|
+
screenOptions={{
|
|
1481
|
+
headerStyle: {
|
|
1482
|
+
backgroundColor: '#3B82F6',
|
|
1483
|
+
},
|
|
1484
|
+
headerTintColor: '#fff',
|
|
1485
|
+
headerTitleStyle: {
|
|
1486
|
+
fontWeight: '600',
|
|
1487
|
+
},
|
|
1488
|
+
}}
|
|
1489
|
+
>
|
|
1490
|
+
${screens.join('\n')}
|
|
1491
|
+
</Stack>
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
`;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
module.exports = new ScreensGenerator();
|