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