@l4yercak3/cli 1.2.15 → 1.2.18

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.
@@ -0,0 +1,1699 @@
1
+ /**
2
+ * Component Generator
3
+ * Generates React components for L4YERCAK3 features
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { ensureDir, writeFileWithBackup, checkFileOverwrite } = require('../../../utils/file-utils');
9
+
10
+ class ComponentGenerator {
11
+ /**
12
+ * Generate React 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
22
+ let outputDir;
23
+ if (fs.existsSync(path.join(projectPath, 'src'))) {
24
+ outputDir = path.join(projectPath, 'src', 'components', 'l4yercak3');
25
+ } else {
26
+ outputDir = path.join(projectPath, 'components', 'l4yercak3');
27
+ }
28
+
29
+ ensureDir(outputDir);
30
+
31
+ const ext = isTypeScript ? 'tsx' : 'jsx';
32
+
33
+ // Generate CRM components
34
+ if (features.includes('crm')) {
35
+ results.contactList = await this.generateContactList(outputDir, ext, isTypeScript);
36
+ results.contactCard = await this.generateContactCard(outputDir, ext, isTypeScript);
37
+ results.contactForm = await this.generateContactForm(outputDir, ext, isTypeScript);
38
+ }
39
+
40
+ // Generate Events components
41
+ if (features.includes('events')) {
42
+ results.eventList = await this.generateEventList(outputDir, ext, isTypeScript);
43
+ results.eventCard = await this.generateEventCard(outputDir, ext, isTypeScript);
44
+ }
45
+
46
+ // Generate Forms components
47
+ if (features.includes('forms')) {
48
+ results.dynamicForm = await this.generateDynamicForm(outputDir, ext, isTypeScript);
49
+ }
50
+
51
+ // Generate Products components
52
+ if (features.includes('products') || features.includes('checkout')) {
53
+ results.productCard = await this.generateProductCard(outputDir, ext, isTypeScript);
54
+ results.productGrid = await this.generateProductGrid(outputDir, ext, isTypeScript);
55
+ }
56
+
57
+ // Generate component index
58
+ results.index = await this.generateIndex(outputDir, ext, features);
59
+
60
+ return results;
61
+ }
62
+
63
+ async generateContactList(outputDir, ext, isTypeScript) {
64
+ const outputPath = path.join(outputDir, `ContactList.${ext}`);
65
+
66
+ const action = await checkFileOverwrite(outputPath);
67
+ if (action === 'skip') {
68
+ return null;
69
+ }
70
+
71
+ const content = isTypeScript
72
+ ? this.getContactListTS()
73
+ : this.getContactListJS();
74
+
75
+ return writeFileWithBackup(outputPath, content, action);
76
+ }
77
+
78
+ getContactListTS() {
79
+ return `/**
80
+ * ContactList Component
81
+ * Displays a searchable, filterable list of contacts
82
+ * Auto-generated by @l4yercak3/cli
83
+ */
84
+
85
+ 'use client';
86
+
87
+ import { useState } from 'react';
88
+ import { useContacts } from '@/lib/l4yercak3/hooks/use-contacts';
89
+ import { ContactCard } from './ContactCard';
90
+ import type { Contact } from '@/lib/l4yercak3/types';
91
+
92
+ interface ContactListProps {
93
+ onSelect?: (contact: Contact) => void;
94
+ className?: string;
95
+ }
96
+
97
+ export function ContactList({ onSelect, className = '' }: ContactListProps) {
98
+ const [search, setSearch] = useState('');
99
+ const [tagFilter, setTagFilter] = useState<string | null>(null);
100
+
101
+ const { data: contacts, isLoading, error } = useContacts({
102
+ search: search || undefined,
103
+ tags: tagFilter ? [tagFilter] : undefined,
104
+ });
105
+
106
+ if (isLoading) {
107
+ return (
108
+ <div className="flex items-center justify-center p-8">
109
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900" />
110
+ </div>
111
+ );
112
+ }
113
+
114
+ if (error) {
115
+ return (
116
+ <div className="p-4 bg-red-50 text-red-600 rounded-lg">
117
+ Failed to load contacts: {error.message}
118
+ </div>
119
+ );
120
+ }
121
+
122
+ return (
123
+ <div className={\`space-y-4 \${className}\`}>
124
+ {/* Search and Filter */}
125
+ <div className="flex gap-4">
126
+ <input
127
+ type="text"
128
+ placeholder="Search contacts..."
129
+ value={search}
130
+ onChange={(e) => setSearch(e.target.value)}
131
+ className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
132
+ />
133
+ <select
134
+ value={tagFilter || ''}
135
+ onChange={(e) => setTagFilter(e.target.value || null)}
136
+ className="px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
137
+ >
138
+ <option value="">All Tags</option>
139
+ <option value="customer">Customer</option>
140
+ <option value="lead">Lead</option>
141
+ <option value="partner">Partner</option>
142
+ </select>
143
+ </div>
144
+
145
+ {/* Contact Grid */}
146
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
147
+ {contacts?.map((contact) => (
148
+ <ContactCard
149
+ key={contact.id}
150
+ contact={contact}
151
+ onClick={() => onSelect?.(contact)}
152
+ />
153
+ ))}
154
+ </div>
155
+
156
+ {contacts?.length === 0 && (
157
+ <div className="text-center py-8 text-gray-500">
158
+ No contacts found
159
+ </div>
160
+ )}
161
+ </div>
162
+ );
163
+ }
164
+ `;
165
+ }
166
+
167
+ getContactListJS() {
168
+ return `/**
169
+ * ContactList Component
170
+ * Displays a searchable, filterable list of contacts
171
+ * Auto-generated by @l4yercak3/cli
172
+ */
173
+
174
+ 'use client';
175
+
176
+ import { useState } from 'react';
177
+ import { useContacts } from '@/lib/l4yercak3/hooks/use-contacts';
178
+ import { ContactCard } from './ContactCard';
179
+
180
+ export function ContactList({ onSelect, className = '' }) {
181
+ const [search, setSearch] = useState('');
182
+ const [tagFilter, setTagFilter] = useState(null);
183
+
184
+ const { data: contacts, isLoading, error } = useContacts({
185
+ search: search || undefined,
186
+ tags: tagFilter ? [tagFilter] : undefined,
187
+ });
188
+
189
+ if (isLoading) {
190
+ return (
191
+ <div className="flex items-center justify-center p-8">
192
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900" />
193
+ </div>
194
+ );
195
+ }
196
+
197
+ if (error) {
198
+ return (
199
+ <div className="p-4 bg-red-50 text-red-600 rounded-lg">
200
+ Failed to load contacts: {error.message}
201
+ </div>
202
+ );
203
+ }
204
+
205
+ return (
206
+ <div className={\`space-y-4 \${className}\`}>
207
+ {/* Search and Filter */}
208
+ <div className="flex gap-4">
209
+ <input
210
+ type="text"
211
+ placeholder="Search contacts..."
212
+ value={search}
213
+ onChange={(e) => setSearch(e.target.value)}
214
+ className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
215
+ />
216
+ <select
217
+ value={tagFilter || ''}
218
+ onChange={(e) => setTagFilter(e.target.value || null)}
219
+ className="px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
220
+ >
221
+ <option value="">All Tags</option>
222
+ <option value="customer">Customer</option>
223
+ <option value="lead">Lead</option>
224
+ <option value="partner">Partner</option>
225
+ </select>
226
+ </div>
227
+
228
+ {/* Contact Grid */}
229
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
230
+ {contacts?.map((contact) => (
231
+ <ContactCard
232
+ key={contact.id}
233
+ contact={contact}
234
+ onClick={() => onSelect?.(contact)}
235
+ />
236
+ ))}
237
+ </div>
238
+
239
+ {contacts?.length === 0 && (
240
+ <div className="text-center py-8 text-gray-500">
241
+ No contacts found
242
+ </div>
243
+ )}
244
+ </div>
245
+ );
246
+ }
247
+ `;
248
+ }
249
+
250
+ async generateContactCard(outputDir, ext, isTypeScript) {
251
+ const outputPath = path.join(outputDir, `ContactCard.${ext}`);
252
+
253
+ const action = await checkFileOverwrite(outputPath);
254
+ if (action === 'skip') {
255
+ return null;
256
+ }
257
+
258
+ const content = isTypeScript
259
+ ? this.getContactCardTS()
260
+ : this.getContactCardJS();
261
+
262
+ return writeFileWithBackup(outputPath, content, action);
263
+ }
264
+
265
+ getContactCardTS() {
266
+ return `/**
267
+ * ContactCard Component
268
+ * Displays a single contact in a card format
269
+ * Auto-generated by @l4yercak3/cli
270
+ */
271
+
272
+ 'use client';
273
+
274
+ import type { Contact } from '@/lib/l4yercak3/types';
275
+
276
+ interface ContactCardProps {
277
+ contact: Contact;
278
+ onClick?: () => void;
279
+ className?: string;
280
+ }
281
+
282
+ export function ContactCard({ contact, onClick, className = '' }: ContactCardProps) {
283
+ const initials = \`\${contact.firstName?.[0] || ''}\${contact.lastName?.[0] || ''}\`.toUpperCase();
284
+
285
+ return (
286
+ <div
287
+ onClick={onClick}
288
+ className={\`p-4 bg-white border rounded-lg shadow-sm hover:shadow-md transition-shadow cursor-pointer \${className}\`}
289
+ >
290
+ <div className="flex items-center gap-3">
291
+ {/* Avatar */}
292
+ <div className="w-12 h-12 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center font-semibold">
293
+ {initials || '?'}
294
+ </div>
295
+
296
+ {/* Info */}
297
+ <div className="flex-1 min-w-0">
298
+ <h3 className="font-medium text-gray-900 truncate">
299
+ {contact.firstName} {contact.lastName}
300
+ </h3>
301
+ {contact.email && (
302
+ <p className="text-sm text-gray-500 truncate">{contact.email}</p>
303
+ )}
304
+ </div>
305
+ </div>
306
+
307
+ {/* Tags */}
308
+ {contact.tags && contact.tags.length > 0 && (
309
+ <div className="mt-3 flex flex-wrap gap-1">
310
+ {contact.tags.slice(0, 3).map((tag) => (
311
+ <span
312
+ key={tag}
313
+ className="px-2 py-0.5 text-xs bg-gray-100 text-gray-600 rounded-full"
314
+ >
315
+ {tag}
316
+ </span>
317
+ ))}
318
+ {contact.tags.length > 3 && (
319
+ <span className="px-2 py-0.5 text-xs text-gray-400">
320
+ +{contact.tags.length - 3}
321
+ </span>
322
+ )}
323
+ </div>
324
+ )}
325
+ </div>
326
+ );
327
+ }
328
+ `;
329
+ }
330
+
331
+ getContactCardJS() {
332
+ return `/**
333
+ * ContactCard Component
334
+ * Displays a single contact in a card format
335
+ * Auto-generated by @l4yercak3/cli
336
+ */
337
+
338
+ 'use client';
339
+
340
+ export function ContactCard({ contact, onClick, className = '' }) {
341
+ const initials = \`\${contact.firstName?.[0] || ''}\${contact.lastName?.[0] || ''}\`.toUpperCase();
342
+
343
+ return (
344
+ <div
345
+ onClick={onClick}
346
+ className={\`p-4 bg-white border rounded-lg shadow-sm hover:shadow-md transition-shadow cursor-pointer \${className}\`}
347
+ >
348
+ <div className="flex items-center gap-3">
349
+ {/* Avatar */}
350
+ <div className="w-12 h-12 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center font-semibold">
351
+ {initials || '?'}
352
+ </div>
353
+
354
+ {/* Info */}
355
+ <div className="flex-1 min-w-0">
356
+ <h3 className="font-medium text-gray-900 truncate">
357
+ {contact.firstName} {contact.lastName}
358
+ </h3>
359
+ {contact.email && (
360
+ <p className="text-sm text-gray-500 truncate">{contact.email}</p>
361
+ )}
362
+ </div>
363
+ </div>
364
+
365
+ {/* Tags */}
366
+ {contact.tags && contact.tags.length > 0 && (
367
+ <div className="mt-3 flex flex-wrap gap-1">
368
+ {contact.tags.slice(0, 3).map((tag) => (
369
+ <span
370
+ key={tag}
371
+ className="px-2 py-0.5 text-xs bg-gray-100 text-gray-600 rounded-full"
372
+ >
373
+ {tag}
374
+ </span>
375
+ ))}
376
+ {contact.tags.length > 3 && (
377
+ <span className="px-2 py-0.5 text-xs text-gray-400">
378
+ +{contact.tags.length - 3}
379
+ </span>
380
+ )}
381
+ </div>
382
+ )}
383
+ </div>
384
+ );
385
+ }
386
+ `;
387
+ }
388
+
389
+ async generateContactForm(outputDir, ext, isTypeScript) {
390
+ const outputPath = path.join(outputDir, `ContactForm.${ext}`);
391
+
392
+ const action = await checkFileOverwrite(outputPath);
393
+ if (action === 'skip') {
394
+ return null;
395
+ }
396
+
397
+ const content = isTypeScript
398
+ ? this.getContactFormTS()
399
+ : this.getContactFormJS();
400
+
401
+ return writeFileWithBackup(outputPath, content, action);
402
+ }
403
+
404
+ getContactFormTS() {
405
+ return `/**
406
+ * ContactForm Component
407
+ * Form for creating or editing contacts
408
+ * Auto-generated by @l4yercak3/cli
409
+ */
410
+
411
+ 'use client';
412
+
413
+ import { useState } from 'react';
414
+ import { useCreateContact, useUpdateContact } from '@/lib/l4yercak3/hooks/use-contacts';
415
+ import type { Contact } from '@/lib/l4yercak3/types';
416
+
417
+ interface ContactFormProps {
418
+ contact?: Contact;
419
+ onSuccess?: (contact: Contact) => void;
420
+ onCancel?: () => void;
421
+ className?: string;
422
+ }
423
+
424
+ export function ContactForm({ contact, onSuccess, onCancel, className = '' }: ContactFormProps) {
425
+ const isEditing = !!contact;
426
+
427
+ const [formData, setFormData] = useState({
428
+ firstName: contact?.firstName || '',
429
+ lastName: contact?.lastName || '',
430
+ email: contact?.email || '',
431
+ phone: contact?.phone || '',
432
+ });
433
+
434
+ const createContact = useCreateContact();
435
+ const updateContact = useUpdateContact();
436
+
437
+ const isLoading = createContact.isPending || updateContact.isPending;
438
+ const error = createContact.error || updateContact.error;
439
+
440
+ const handleSubmit = async (e: React.FormEvent) => {
441
+ e.preventDefault();
442
+
443
+ try {
444
+ let result: Contact;
445
+ if (isEditing && contact) {
446
+ result = await updateContact.mutateAsync({
447
+ id: contact.id,
448
+ ...formData,
449
+ });
450
+ } else {
451
+ result = await createContact.mutateAsync(formData);
452
+ }
453
+ onSuccess?.(result);
454
+ } catch {
455
+ // Error is handled by mutation state
456
+ }
457
+ };
458
+
459
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
460
+ setFormData((prev) => ({
461
+ ...prev,
462
+ [e.target.name]: e.target.value,
463
+ }));
464
+ };
465
+
466
+ return (
467
+ <form onSubmit={handleSubmit} className={\`space-y-4 \${className}\`}>
468
+ {error && (
469
+ <div className="p-3 bg-red-50 text-red-600 rounded-lg text-sm">
470
+ {error.message}
471
+ </div>
472
+ )}
473
+
474
+ <div className="grid grid-cols-2 gap-4">
475
+ <div>
476
+ <label htmlFor="firstName" className="block text-sm font-medium text-gray-700 mb-1">
477
+ First Name
478
+ </label>
479
+ <input
480
+ type="text"
481
+ id="firstName"
482
+ name="firstName"
483
+ value={formData.firstName}
484
+ onChange={handleChange}
485
+ required
486
+ className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
487
+ />
488
+ </div>
489
+
490
+ <div>
491
+ <label htmlFor="lastName" className="block text-sm font-medium text-gray-700 mb-1">
492
+ Last Name
493
+ </label>
494
+ <input
495
+ type="text"
496
+ id="lastName"
497
+ name="lastName"
498
+ value={formData.lastName}
499
+ onChange={handleChange}
500
+ required
501
+ className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
502
+ />
503
+ </div>
504
+ </div>
505
+
506
+ <div>
507
+ <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
508
+ Email
509
+ </label>
510
+ <input
511
+ type="email"
512
+ id="email"
513
+ name="email"
514
+ value={formData.email}
515
+ onChange={handleChange}
516
+ className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
517
+ />
518
+ </div>
519
+
520
+ <div>
521
+ <label htmlFor="phone" className="block text-sm font-medium text-gray-700 mb-1">
522
+ Phone
523
+ </label>
524
+ <input
525
+ type="tel"
526
+ id="phone"
527
+ name="phone"
528
+ value={formData.phone}
529
+ onChange={handleChange}
530
+ className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
531
+ />
532
+ </div>
533
+
534
+ <div className="flex gap-3 pt-2">
535
+ {onCancel && (
536
+ <button
537
+ type="button"
538
+ onClick={onCancel}
539
+ className="flex-1 px-4 py-2 border rounded-lg hover:bg-gray-50 transition-colors"
540
+ >
541
+ Cancel
542
+ </button>
543
+ )}
544
+ <button
545
+ type="submit"
546
+ disabled={isLoading}
547
+ className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
548
+ >
549
+ {isLoading ? 'Saving...' : isEditing ? 'Update' : 'Create'}
550
+ </button>
551
+ </div>
552
+ </form>
553
+ );
554
+ }
555
+ `;
556
+ }
557
+
558
+ getContactFormJS() {
559
+ return `/**
560
+ * ContactForm Component
561
+ * Form for creating or editing contacts
562
+ * Auto-generated by @l4yercak3/cli
563
+ */
564
+
565
+ 'use client';
566
+
567
+ import { useState } from 'react';
568
+ import { useCreateContact, useUpdateContact } from '@/lib/l4yercak3/hooks/use-contacts';
569
+
570
+ export function ContactForm({ contact, onSuccess, onCancel, className = '' }) {
571
+ const isEditing = !!contact;
572
+
573
+ const [formData, setFormData] = useState({
574
+ firstName: contact?.firstName || '',
575
+ lastName: contact?.lastName || '',
576
+ email: contact?.email || '',
577
+ phone: contact?.phone || '',
578
+ });
579
+
580
+ const createContact = useCreateContact();
581
+ const updateContact = useUpdateContact();
582
+
583
+ const isLoading = createContact.isPending || updateContact.isPending;
584
+ const error = createContact.error || updateContact.error;
585
+
586
+ const handleSubmit = async (e) => {
587
+ e.preventDefault();
588
+
589
+ try {
590
+ let result;
591
+ if (isEditing && contact) {
592
+ result = await updateContact.mutateAsync({
593
+ id: contact.id,
594
+ ...formData,
595
+ });
596
+ } else {
597
+ result = await createContact.mutateAsync(formData);
598
+ }
599
+ onSuccess?.(result);
600
+ } catch {
601
+ // Error is handled by mutation state
602
+ }
603
+ };
604
+
605
+ const handleChange = (e) => {
606
+ setFormData((prev) => ({
607
+ ...prev,
608
+ [e.target.name]: e.target.value,
609
+ }));
610
+ };
611
+
612
+ return (
613
+ <form onSubmit={handleSubmit} className={\`space-y-4 \${className}\`}>
614
+ {error && (
615
+ <div className="p-3 bg-red-50 text-red-600 rounded-lg text-sm">
616
+ {error.message}
617
+ </div>
618
+ )}
619
+
620
+ <div className="grid grid-cols-2 gap-4">
621
+ <div>
622
+ <label htmlFor="firstName" className="block text-sm font-medium text-gray-700 mb-1">
623
+ First Name
624
+ </label>
625
+ <input
626
+ type="text"
627
+ id="firstName"
628
+ name="firstName"
629
+ value={formData.firstName}
630
+ onChange={handleChange}
631
+ required
632
+ className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
633
+ />
634
+ </div>
635
+
636
+ <div>
637
+ <label htmlFor="lastName" className="block text-sm font-medium text-gray-700 mb-1">
638
+ Last Name
639
+ </label>
640
+ <input
641
+ type="text"
642
+ id="lastName"
643
+ name="lastName"
644
+ value={formData.lastName}
645
+ onChange={handleChange}
646
+ required
647
+ className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
648
+ />
649
+ </div>
650
+ </div>
651
+
652
+ <div>
653
+ <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
654
+ Email
655
+ </label>
656
+ <input
657
+ type="email"
658
+ id="email"
659
+ name="email"
660
+ value={formData.email}
661
+ onChange={handleChange}
662
+ className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
663
+ />
664
+ </div>
665
+
666
+ <div>
667
+ <label htmlFor="phone" className="block text-sm font-medium text-gray-700 mb-1">
668
+ Phone
669
+ </label>
670
+ <input
671
+ type="tel"
672
+ id="phone"
673
+ name="phone"
674
+ value={formData.phone}
675
+ onChange={handleChange}
676
+ className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
677
+ />
678
+ </div>
679
+
680
+ <div className="flex gap-3 pt-2">
681
+ {onCancel && (
682
+ <button
683
+ type="button"
684
+ onClick={onCancel}
685
+ className="flex-1 px-4 py-2 border rounded-lg hover:bg-gray-50 transition-colors"
686
+ >
687
+ Cancel
688
+ </button>
689
+ )}
690
+ <button
691
+ type="submit"
692
+ disabled={isLoading}
693
+ className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
694
+ >
695
+ {isLoading ? 'Saving...' : isEditing ? 'Update' : 'Create'}
696
+ </button>
697
+ </div>
698
+ </form>
699
+ );
700
+ }
701
+ `;
702
+ }
703
+
704
+ async generateEventList(outputDir, ext, isTypeScript) {
705
+ const outputPath = path.join(outputDir, `EventList.${ext}`);
706
+
707
+ const action = await checkFileOverwrite(outputPath);
708
+ if (action === 'skip') {
709
+ return null;
710
+ }
711
+
712
+ const content = isTypeScript
713
+ ? this.getEventListTS()
714
+ : this.getEventListJS();
715
+
716
+ return writeFileWithBackup(outputPath, content, action);
717
+ }
718
+
719
+ getEventListTS() {
720
+ return `/**
721
+ * EventList Component
722
+ * Displays a list of events with filtering
723
+ * Auto-generated by @l4yercak3/cli
724
+ */
725
+
726
+ 'use client';
727
+
728
+ import { useState } from 'react';
729
+ import { useEvents } from '@/lib/l4yercak3/hooks/use-events';
730
+ import { EventCard } from './EventCard';
731
+ import type { Event } from '@/lib/l4yercak3/types';
732
+
733
+ interface EventListProps {
734
+ onSelect?: (event: Event) => void;
735
+ className?: string;
736
+ }
737
+
738
+ export function EventList({ onSelect, className = '' }: EventListProps) {
739
+ const [statusFilter, setStatusFilter] = useState<string>('upcoming');
740
+
741
+ const { data: events, isLoading, error } = useEvents({
742
+ status: statusFilter as 'upcoming' | 'past' | 'all',
743
+ });
744
+
745
+ if (isLoading) {
746
+ return (
747
+ <div className="flex items-center justify-center p-8">
748
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900" />
749
+ </div>
750
+ );
751
+ }
752
+
753
+ if (error) {
754
+ return (
755
+ <div className="p-4 bg-red-50 text-red-600 rounded-lg">
756
+ Failed to load events: {error.message}
757
+ </div>
758
+ );
759
+ }
760
+
761
+ return (
762
+ <div className={\`space-y-4 \${className}\`}>
763
+ {/* Filter */}
764
+ <div className="flex gap-2">
765
+ {['upcoming', 'past', 'all'].map((status) => (
766
+ <button
767
+ key={status}
768
+ onClick={() => setStatusFilter(status)}
769
+ className={\`px-4 py-2 rounded-lg capitalize transition-colors \${
770
+ statusFilter === status
771
+ ? 'bg-blue-600 text-white'
772
+ : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
773
+ }\`}
774
+ >
775
+ {status}
776
+ </button>
777
+ ))}
778
+ </div>
779
+
780
+ {/* Event List */}
781
+ <div className="space-y-4">
782
+ {events?.map((event) => (
783
+ <EventCard
784
+ key={event.id}
785
+ event={event}
786
+ onClick={() => onSelect?.(event)}
787
+ />
788
+ ))}
789
+ </div>
790
+
791
+ {events?.length === 0 && (
792
+ <div className="text-center py-8 text-gray-500">
793
+ No events found
794
+ </div>
795
+ )}
796
+ </div>
797
+ );
798
+ }
799
+ `;
800
+ }
801
+
802
+ getEventListJS() {
803
+ return `/**
804
+ * EventList Component
805
+ * Displays a list of events with filtering
806
+ * Auto-generated by @l4yercak3/cli
807
+ */
808
+
809
+ 'use client';
810
+
811
+ import { useState } from 'react';
812
+ import { useEvents } from '@/lib/l4yercak3/hooks/use-events';
813
+ import { EventCard } from './EventCard';
814
+
815
+ export function EventList({ onSelect, className = '' }) {
816
+ const [statusFilter, setStatusFilter] = useState('upcoming');
817
+
818
+ const { data: events, isLoading, error } = useEvents({
819
+ status: statusFilter,
820
+ });
821
+
822
+ if (isLoading) {
823
+ return (
824
+ <div className="flex items-center justify-center p-8">
825
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900" />
826
+ </div>
827
+ );
828
+ }
829
+
830
+ if (error) {
831
+ return (
832
+ <div className="p-4 bg-red-50 text-red-600 rounded-lg">
833
+ Failed to load events: {error.message}
834
+ </div>
835
+ );
836
+ }
837
+
838
+ return (
839
+ <div className={\`space-y-4 \${className}\`}>
840
+ {/* Filter */}
841
+ <div className="flex gap-2">
842
+ {['upcoming', 'past', 'all'].map((status) => (
843
+ <button
844
+ key={status}
845
+ onClick={() => setStatusFilter(status)}
846
+ className={\`px-4 py-2 rounded-lg capitalize transition-colors \${
847
+ statusFilter === status
848
+ ? 'bg-blue-600 text-white'
849
+ : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
850
+ }\`}
851
+ >
852
+ {status}
853
+ </button>
854
+ ))}
855
+ </div>
856
+
857
+ {/* Event List */}
858
+ <div className="space-y-4">
859
+ {events?.map((event) => (
860
+ <EventCard
861
+ key={event.id}
862
+ event={event}
863
+ onClick={() => onSelect?.(event)}
864
+ />
865
+ ))}
866
+ </div>
867
+
868
+ {events?.length === 0 && (
869
+ <div className="text-center py-8 text-gray-500">
870
+ No events found
871
+ </div>
872
+ )}
873
+ </div>
874
+ );
875
+ }
876
+ `;
877
+ }
878
+
879
+ async generateEventCard(outputDir, ext, isTypeScript) {
880
+ const outputPath = path.join(outputDir, `EventCard.${ext}`);
881
+
882
+ const action = await checkFileOverwrite(outputPath);
883
+ if (action === 'skip') {
884
+ return null;
885
+ }
886
+
887
+ const content = isTypeScript
888
+ ? this.getEventCardTS()
889
+ : this.getEventCardJS();
890
+
891
+ return writeFileWithBackup(outputPath, content, action);
892
+ }
893
+
894
+ getEventCardTS() {
895
+ return `/**
896
+ * EventCard Component
897
+ * Displays a single event in a card format
898
+ * Auto-generated by @l4yercak3/cli
899
+ */
900
+
901
+ 'use client';
902
+
903
+ import type { Event } from '@/lib/l4yercak3/types';
904
+
905
+ interface EventCardProps {
906
+ event: Event;
907
+ onClick?: () => void;
908
+ className?: string;
909
+ }
910
+
911
+ export function EventCard({ event, onClick, className = '' }: EventCardProps) {
912
+ const startDate = event.startDate ? new Date(event.startDate) : null;
913
+
914
+ const formatDate = (date: Date) => {
915
+ return date.toLocaleDateString('en-US', {
916
+ weekday: 'short',
917
+ month: 'short',
918
+ day: 'numeric',
919
+ hour: 'numeric',
920
+ minute: '2-digit',
921
+ });
922
+ };
923
+
924
+ return (
925
+ <div
926
+ onClick={onClick}
927
+ className={\`p-4 bg-white border rounded-lg shadow-sm hover:shadow-md transition-shadow cursor-pointer \${className}\`}
928
+ >
929
+ <div className="flex gap-4">
930
+ {/* Date Badge */}
931
+ {startDate && (
932
+ <div className="flex-shrink-0 w-16 text-center">
933
+ <div className="text-2xl font-bold text-blue-600">
934
+ {startDate.getDate()}
935
+ </div>
936
+ <div className="text-sm text-gray-500 uppercase">
937
+ {startDate.toLocaleDateString('en-US', { month: 'short' })}
938
+ </div>
939
+ </div>
940
+ )}
941
+
942
+ {/* Event Info */}
943
+ <div className="flex-1 min-w-0">
944
+ <h3 className="font-medium text-gray-900 truncate">{event.name}</h3>
945
+ {startDate && (
946
+ <p className="text-sm text-gray-500">{formatDate(startDate)}</p>
947
+ )}
948
+ {event.location && (
949
+ <p className="text-sm text-gray-500 mt-1">{event.location}</p>
950
+ )}
951
+ </div>
952
+
953
+ {/* Status Badge */}
954
+ <div>
955
+ <span
956
+ className={\`px-2 py-1 text-xs rounded-full \${
957
+ event.status === 'published'
958
+ ? 'bg-green-100 text-green-700'
959
+ : event.status === 'draft'
960
+ ? 'bg-gray-100 text-gray-600'
961
+ : 'bg-red-100 text-red-700'
962
+ }\`}
963
+ >
964
+ {event.status}
965
+ </span>
966
+ </div>
967
+ </div>
968
+ </div>
969
+ );
970
+ }
971
+ `;
972
+ }
973
+
974
+ getEventCardJS() {
975
+ return `/**
976
+ * EventCard Component
977
+ * Displays a single event in a card format
978
+ * Auto-generated by @l4yercak3/cli
979
+ */
980
+
981
+ 'use client';
982
+
983
+ export function EventCard({ event, onClick, className = '' }) {
984
+ const startDate = event.startDate ? new Date(event.startDate) : null;
985
+
986
+ const formatDate = (date) => {
987
+ return date.toLocaleDateString('en-US', {
988
+ weekday: 'short',
989
+ month: 'short',
990
+ day: 'numeric',
991
+ hour: 'numeric',
992
+ minute: '2-digit',
993
+ });
994
+ };
995
+
996
+ return (
997
+ <div
998
+ onClick={onClick}
999
+ className={\`p-4 bg-white border rounded-lg shadow-sm hover:shadow-md transition-shadow cursor-pointer \${className}\`}
1000
+ >
1001
+ <div className="flex gap-4">
1002
+ {/* Date Badge */}
1003
+ {startDate && (
1004
+ <div className="flex-shrink-0 w-16 text-center">
1005
+ <div className="text-2xl font-bold text-blue-600">
1006
+ {startDate.getDate()}
1007
+ </div>
1008
+ <div className="text-sm text-gray-500 uppercase">
1009
+ {startDate.toLocaleDateString('en-US', { month: 'short' })}
1010
+ </div>
1011
+ </div>
1012
+ )}
1013
+
1014
+ {/* Event Info */}
1015
+ <div className="flex-1 min-w-0">
1016
+ <h3 className="font-medium text-gray-900 truncate">{event.name}</h3>
1017
+ {startDate && (
1018
+ <p className="text-sm text-gray-500">{formatDate(startDate)}</p>
1019
+ )}
1020
+ {event.location && (
1021
+ <p className="text-sm text-gray-500 mt-1">{event.location}</p>
1022
+ )}
1023
+ </div>
1024
+
1025
+ {/* Status Badge */}
1026
+ <div>
1027
+ <span
1028
+ className={\`px-2 py-1 text-xs rounded-full \${
1029
+ event.status === 'published'
1030
+ ? 'bg-green-100 text-green-700'
1031
+ : event.status === 'draft'
1032
+ ? 'bg-gray-100 text-gray-600'
1033
+ : 'bg-red-100 text-red-700'
1034
+ }\`}
1035
+ >
1036
+ {event.status}
1037
+ </span>
1038
+ </div>
1039
+ </div>
1040
+ </div>
1041
+ );
1042
+ }
1043
+ `;
1044
+ }
1045
+
1046
+ async generateDynamicForm(outputDir, ext, isTypeScript) {
1047
+ const outputPath = path.join(outputDir, `DynamicForm.${ext}`);
1048
+
1049
+ const action = await checkFileOverwrite(outputPath);
1050
+ if (action === 'skip') {
1051
+ return null;
1052
+ }
1053
+
1054
+ const content = isTypeScript
1055
+ ? this.getDynamicFormTS()
1056
+ : this.getDynamicFormJS();
1057
+
1058
+ return writeFileWithBackup(outputPath, content, action);
1059
+ }
1060
+
1061
+ getDynamicFormTS() {
1062
+ return `/**
1063
+ * DynamicForm Component
1064
+ * Renders a form dynamically from L4YERCAK3 form schema
1065
+ * Auto-generated by @l4yercak3/cli
1066
+ */
1067
+
1068
+ 'use client';
1069
+
1070
+ import { useState } from 'react';
1071
+ import { useForm, useSubmitForm } from '@/lib/l4yercak3/hooks/use-forms';
1072
+ import type { FormField } from '@/lib/l4yercak3/types';
1073
+
1074
+ interface DynamicFormProps {
1075
+ formId: string;
1076
+ onSuccess?: () => void;
1077
+ className?: string;
1078
+ }
1079
+
1080
+ export function DynamicForm({ formId, onSuccess, className = '' }: DynamicFormProps) {
1081
+ const { data: form, isLoading: formLoading } = useForm(formId);
1082
+ const submitForm = useSubmitForm();
1083
+ const [formData, setFormData] = useState<Record<string, unknown>>({});
1084
+
1085
+ if (formLoading) {
1086
+ return (
1087
+ <div className="flex items-center justify-center p-8">
1088
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900" />
1089
+ </div>
1090
+ );
1091
+ }
1092
+
1093
+ if (!form) {
1094
+ return (
1095
+ <div className="p-4 bg-red-50 text-red-600 rounded-lg">
1096
+ Form not found
1097
+ </div>
1098
+ );
1099
+ }
1100
+
1101
+ const handleSubmit = async (e: React.FormEvent) => {
1102
+ e.preventDefault();
1103
+ try {
1104
+ await submitForm.mutateAsync({ formId, data: formData });
1105
+ onSuccess?.();
1106
+ } catch {
1107
+ // Error handled by mutation state
1108
+ }
1109
+ };
1110
+
1111
+ const handleChange = (fieldId: string, value: unknown) => {
1112
+ setFormData((prev) => ({ ...prev, [fieldId]: value }));
1113
+ };
1114
+
1115
+ const renderField = (field: FormField) => {
1116
+ const commonProps = {
1117
+ id: field.id,
1118
+ name: field.id,
1119
+ required: field.required,
1120
+ className: 'w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500',
1121
+ };
1122
+
1123
+ switch (field.type) {
1124
+ case 'text':
1125
+ case 'email':
1126
+ case 'phone':
1127
+ return (
1128
+ <input
1129
+ type={field.type === 'phone' ? 'tel' : field.type}
1130
+ {...commonProps}
1131
+ value={(formData[field.id] as string) || ''}
1132
+ onChange={(e) => handleChange(field.id, e.target.value)}
1133
+ />
1134
+ );
1135
+
1136
+ case 'textarea':
1137
+ return (
1138
+ <textarea
1139
+ {...commonProps}
1140
+ rows={4}
1141
+ value={(formData[field.id] as string) || ''}
1142
+ onChange={(e) => handleChange(field.id, e.target.value)}
1143
+ />
1144
+ );
1145
+
1146
+ case 'select':
1147
+ return (
1148
+ <select
1149
+ {...commonProps}
1150
+ value={(formData[field.id] as string) || ''}
1151
+ onChange={(e) => handleChange(field.id, e.target.value)}
1152
+ >
1153
+ <option value="">Select...</option>
1154
+ {field.options?.map((opt) => (
1155
+ <option key={opt.value} value={opt.value}>
1156
+ {opt.label}
1157
+ </option>
1158
+ ))}
1159
+ </select>
1160
+ );
1161
+
1162
+ case 'checkbox':
1163
+ return (
1164
+ <input
1165
+ type="checkbox"
1166
+ id={field.id}
1167
+ name={field.id}
1168
+ checked={(formData[field.id] as boolean) || false}
1169
+ onChange={(e) => handleChange(field.id, e.target.checked)}
1170
+ className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
1171
+ />
1172
+ );
1173
+
1174
+ default:
1175
+ return (
1176
+ <input
1177
+ type="text"
1178
+ {...commonProps}
1179
+ value={(formData[field.id] as string) || ''}
1180
+ onChange={(e) => handleChange(field.id, e.target.value)}
1181
+ />
1182
+ );
1183
+ }
1184
+ };
1185
+
1186
+ return (
1187
+ <form onSubmit={handleSubmit} className={\`space-y-4 \${className}\`}>
1188
+ <h2 className="text-xl font-semibold">{form.name}</h2>
1189
+ {form.description && (
1190
+ <p className="text-gray-600">{form.description}</p>
1191
+ )}
1192
+
1193
+ {submitForm.error && (
1194
+ <div className="p-3 bg-red-50 text-red-600 rounded-lg text-sm">
1195
+ {submitForm.error.message}
1196
+ </div>
1197
+ )}
1198
+
1199
+ {form.fields?.map((field) => (
1200
+ <div key={field.id}>
1201
+ <label
1202
+ htmlFor={field.id}
1203
+ className={\`block text-sm font-medium text-gray-700 mb-1 \${
1204
+ field.type === 'checkbox' ? 'inline ml-2' : ''
1205
+ }\`}
1206
+ >
1207
+ {field.label}
1208
+ {field.required && <span className="text-red-500 ml-1">*</span>}
1209
+ </label>
1210
+ {renderField(field)}
1211
+ </div>
1212
+ ))}
1213
+
1214
+ <button
1215
+ type="submit"
1216
+ disabled={submitForm.isPending}
1217
+ className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
1218
+ >
1219
+ {submitForm.isPending ? 'Submitting...' : 'Submit'}
1220
+ </button>
1221
+ </form>
1222
+ );
1223
+ }
1224
+ `;
1225
+ }
1226
+
1227
+ getDynamicFormJS() {
1228
+ return `/**
1229
+ * DynamicForm Component
1230
+ * Renders a form dynamically from L4YERCAK3 form schema
1231
+ * Auto-generated by @l4yercak3/cli
1232
+ */
1233
+
1234
+ 'use client';
1235
+
1236
+ import { useState } from 'react';
1237
+ import { useForm, useSubmitForm } from '@/lib/l4yercak3/hooks/use-forms';
1238
+
1239
+ export function DynamicForm({ formId, onSuccess, className = '' }) {
1240
+ const { data: form, isLoading: formLoading } = useForm(formId);
1241
+ const submitForm = useSubmitForm();
1242
+ const [formData, setFormData] = useState({});
1243
+
1244
+ if (formLoading) {
1245
+ return (
1246
+ <div className="flex items-center justify-center p-8">
1247
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900" />
1248
+ </div>
1249
+ );
1250
+ }
1251
+
1252
+ if (!form) {
1253
+ return (
1254
+ <div className="p-4 bg-red-50 text-red-600 rounded-lg">
1255
+ Form not found
1256
+ </div>
1257
+ );
1258
+ }
1259
+
1260
+ const handleSubmit = async (e) => {
1261
+ e.preventDefault();
1262
+ try {
1263
+ await submitForm.mutateAsync({ formId, data: formData });
1264
+ onSuccess?.();
1265
+ } catch {
1266
+ // Error handled by mutation state
1267
+ }
1268
+ };
1269
+
1270
+ const handleChange = (fieldId, value) => {
1271
+ setFormData((prev) => ({ ...prev, [fieldId]: value }));
1272
+ };
1273
+
1274
+ const renderField = (field) => {
1275
+ const commonProps = {
1276
+ id: field.id,
1277
+ name: field.id,
1278
+ required: field.required,
1279
+ className: 'w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500',
1280
+ };
1281
+
1282
+ switch (field.type) {
1283
+ case 'text':
1284
+ case 'email':
1285
+ case 'phone':
1286
+ return (
1287
+ <input
1288
+ type={field.type === 'phone' ? 'tel' : field.type}
1289
+ {...commonProps}
1290
+ value={formData[field.id] || ''}
1291
+ onChange={(e) => handleChange(field.id, e.target.value)}
1292
+ />
1293
+ );
1294
+
1295
+ case 'textarea':
1296
+ return (
1297
+ <textarea
1298
+ {...commonProps}
1299
+ rows={4}
1300
+ value={formData[field.id] || ''}
1301
+ onChange={(e) => handleChange(field.id, e.target.value)}
1302
+ />
1303
+ );
1304
+
1305
+ case 'select':
1306
+ return (
1307
+ <select
1308
+ {...commonProps}
1309
+ value={formData[field.id] || ''}
1310
+ onChange={(e) => handleChange(field.id, e.target.value)}
1311
+ >
1312
+ <option value="">Select...</option>
1313
+ {field.options?.map((opt) => (
1314
+ <option key={opt.value} value={opt.value}>
1315
+ {opt.label}
1316
+ </option>
1317
+ ))}
1318
+ </select>
1319
+ );
1320
+
1321
+ case 'checkbox':
1322
+ return (
1323
+ <input
1324
+ type="checkbox"
1325
+ id={field.id}
1326
+ name={field.id}
1327
+ checked={formData[field.id] || false}
1328
+ onChange={(e) => handleChange(field.id, e.target.checked)}
1329
+ className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
1330
+ />
1331
+ );
1332
+
1333
+ default:
1334
+ return (
1335
+ <input
1336
+ type="text"
1337
+ {...commonProps}
1338
+ value={formData[field.id] || ''}
1339
+ onChange={(e) => handleChange(field.id, e.target.value)}
1340
+ />
1341
+ );
1342
+ }
1343
+ };
1344
+
1345
+ return (
1346
+ <form onSubmit={handleSubmit} className={\`space-y-4 \${className}\`}>
1347
+ <h2 className="text-xl font-semibold">{form.name}</h2>
1348
+ {form.description && (
1349
+ <p className="text-gray-600">{form.description}</p>
1350
+ )}
1351
+
1352
+ {submitForm.error && (
1353
+ <div className="p-3 bg-red-50 text-red-600 rounded-lg text-sm">
1354
+ {submitForm.error.message}
1355
+ </div>
1356
+ )}
1357
+
1358
+ {form.fields?.map((field) => (
1359
+ <div key={field.id}>
1360
+ <label
1361
+ htmlFor={field.id}
1362
+ className={\`block text-sm font-medium text-gray-700 mb-1 \${
1363
+ field.type === 'checkbox' ? 'inline ml-2' : ''
1364
+ }\`}
1365
+ >
1366
+ {field.label}
1367
+ {field.required && <span className="text-red-500 ml-1">*</span>}
1368
+ </label>
1369
+ {renderField(field)}
1370
+ </div>
1371
+ ))}
1372
+
1373
+ <button
1374
+ type="submit"
1375
+ disabled={submitForm.isPending}
1376
+ className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
1377
+ >
1378
+ {submitForm.isPending ? 'Submitting...' : 'Submit'}
1379
+ </button>
1380
+ </form>
1381
+ );
1382
+ }
1383
+ `;
1384
+ }
1385
+
1386
+ async generateProductCard(outputDir, ext, isTypeScript) {
1387
+ const outputPath = path.join(outputDir, `ProductCard.${ext}`);
1388
+
1389
+ const action = await checkFileOverwrite(outputPath);
1390
+ if (action === 'skip') {
1391
+ return null;
1392
+ }
1393
+
1394
+ const content = isTypeScript
1395
+ ? this.getProductCardTS()
1396
+ : this.getProductCardJS();
1397
+
1398
+ return writeFileWithBackup(outputPath, content, action);
1399
+ }
1400
+
1401
+ getProductCardTS() {
1402
+ return `/**
1403
+ * ProductCard Component
1404
+ * Displays a single product in a card format
1405
+ * Auto-generated by @l4yercak3/cli
1406
+ */
1407
+
1408
+ 'use client';
1409
+
1410
+ import type { Product } from '@/lib/l4yercak3/types';
1411
+
1412
+ interface ProductCardProps {
1413
+ product: Product;
1414
+ onAddToCart?: (product: Product) => void;
1415
+ className?: string;
1416
+ }
1417
+
1418
+ export function ProductCard({ product, onAddToCart, className = '' }: ProductCardProps) {
1419
+ const formatPrice = (price: number) => {
1420
+ return new Intl.NumberFormat('en-US', {
1421
+ style: 'currency',
1422
+ currency: 'USD',
1423
+ }).format(price / 100);
1424
+ };
1425
+
1426
+ return (
1427
+ <div className={\`bg-white border rounded-lg shadow-sm overflow-hidden \${className}\`}>
1428
+ {/* Image */}
1429
+ {product.imageUrl ? (
1430
+ <img
1431
+ src={product.imageUrl}
1432
+ alt={product.name}
1433
+ className="w-full h-48 object-cover"
1434
+ />
1435
+ ) : (
1436
+ <div className="w-full h-48 bg-gray-100 flex items-center justify-center">
1437
+ <span className="text-gray-400">No image</span>
1438
+ </div>
1439
+ )}
1440
+
1441
+ {/* Content */}
1442
+ <div className="p-4">
1443
+ <h3 className="font-medium text-gray-900 truncate">{product.name}</h3>
1444
+ {product.description && (
1445
+ <p className="text-sm text-gray-500 mt-1 line-clamp-2">
1446
+ {product.description}
1447
+ </p>
1448
+ )}
1449
+
1450
+ <div className="mt-4 flex items-center justify-between">
1451
+ <span className="text-lg font-bold text-gray-900">
1452
+ {formatPrice(product.price)}
1453
+ </span>
1454
+ {onAddToCart && (
1455
+ <button
1456
+ onClick={() => onAddToCart(product)}
1457
+ className="px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors"
1458
+ >
1459
+ Add to Cart
1460
+ </button>
1461
+ )}
1462
+ </div>
1463
+ </div>
1464
+ </div>
1465
+ );
1466
+ }
1467
+ `;
1468
+ }
1469
+
1470
+ getProductCardJS() {
1471
+ return `/**
1472
+ * ProductCard Component
1473
+ * Displays a single product in a card format
1474
+ * Auto-generated by @l4yercak3/cli
1475
+ */
1476
+
1477
+ 'use client';
1478
+
1479
+ export function ProductCard({ product, onAddToCart, className = '' }) {
1480
+ const formatPrice = (price) => {
1481
+ return new Intl.NumberFormat('en-US', {
1482
+ style: 'currency',
1483
+ currency: 'USD',
1484
+ }).format(price / 100);
1485
+ };
1486
+
1487
+ return (
1488
+ <div className={\`bg-white border rounded-lg shadow-sm overflow-hidden \${className}\`}>
1489
+ {/* Image */}
1490
+ {product.imageUrl ? (
1491
+ <img
1492
+ src={product.imageUrl}
1493
+ alt={product.name}
1494
+ className="w-full h-48 object-cover"
1495
+ />
1496
+ ) : (
1497
+ <div className="w-full h-48 bg-gray-100 flex items-center justify-center">
1498
+ <span className="text-gray-400">No image</span>
1499
+ </div>
1500
+ )}
1501
+
1502
+ {/* Content */}
1503
+ <div className="p-4">
1504
+ <h3 className="font-medium text-gray-900 truncate">{product.name}</h3>
1505
+ {product.description && (
1506
+ <p className="text-sm text-gray-500 mt-1 line-clamp-2">
1507
+ {product.description}
1508
+ </p>
1509
+ )}
1510
+
1511
+ <div className="mt-4 flex items-center justify-between">
1512
+ <span className="text-lg font-bold text-gray-900">
1513
+ {formatPrice(product.price)}
1514
+ </span>
1515
+ {onAddToCart && (
1516
+ <button
1517
+ onClick={() => onAddToCart(product)}
1518
+ className="px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors"
1519
+ >
1520
+ Add to Cart
1521
+ </button>
1522
+ )}
1523
+ </div>
1524
+ </div>
1525
+ </div>
1526
+ );
1527
+ }
1528
+ `;
1529
+ }
1530
+
1531
+ async generateProductGrid(outputDir, ext, isTypeScript) {
1532
+ const outputPath = path.join(outputDir, `ProductGrid.${ext}`);
1533
+
1534
+ const action = await checkFileOverwrite(outputPath);
1535
+ if (action === 'skip') {
1536
+ return null;
1537
+ }
1538
+
1539
+ const content = isTypeScript
1540
+ ? this.getProductGridTS()
1541
+ : this.getProductGridJS();
1542
+
1543
+ return writeFileWithBackup(outputPath, content, action);
1544
+ }
1545
+
1546
+ getProductGridTS() {
1547
+ return `/**
1548
+ * ProductGrid Component
1549
+ * Displays a grid of products with optional filtering
1550
+ * Auto-generated by @l4yercak3/cli
1551
+ */
1552
+
1553
+ 'use client';
1554
+
1555
+ import { useProducts } from '@/lib/l4yercak3/hooks/use-products';
1556
+ import { ProductCard } from './ProductCard';
1557
+ import type { Product } from '@/lib/l4yercak3/types';
1558
+
1559
+ interface ProductGridProps {
1560
+ category?: string;
1561
+ onAddToCart?: (product: Product) => void;
1562
+ className?: string;
1563
+ }
1564
+
1565
+ export function ProductGrid({ category, onAddToCart, className = '' }: ProductGridProps) {
1566
+ const { data: products, isLoading, error } = useProducts({ category });
1567
+
1568
+ if (isLoading) {
1569
+ return (
1570
+ <div className="flex items-center justify-center p-8">
1571
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900" />
1572
+ </div>
1573
+ );
1574
+ }
1575
+
1576
+ if (error) {
1577
+ return (
1578
+ <div className="p-4 bg-red-50 text-red-600 rounded-lg">
1579
+ Failed to load products: {error.message}
1580
+ </div>
1581
+ );
1582
+ }
1583
+
1584
+ return (
1585
+ <div className={\`grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 \${className}\`}>
1586
+ {products?.map((product) => (
1587
+ <ProductCard
1588
+ key={product.id}
1589
+ product={product}
1590
+ onAddToCart={onAddToCart}
1591
+ />
1592
+ ))}
1593
+
1594
+ {products?.length === 0 && (
1595
+ <div className="col-span-full text-center py-8 text-gray-500">
1596
+ No products found
1597
+ </div>
1598
+ )}
1599
+ </div>
1600
+ );
1601
+ }
1602
+ `;
1603
+ }
1604
+
1605
+ getProductGridJS() {
1606
+ return `/**
1607
+ * ProductGrid Component
1608
+ * Displays a grid of products with optional filtering
1609
+ * Auto-generated by @l4yercak3/cli
1610
+ */
1611
+
1612
+ 'use client';
1613
+
1614
+ import { useProducts } from '@/lib/l4yercak3/hooks/use-products';
1615
+ import { ProductCard } from './ProductCard';
1616
+
1617
+ export function ProductGrid({ category, onAddToCart, className = '' }) {
1618
+ const { data: products, isLoading, error } = useProducts({ category });
1619
+
1620
+ if (isLoading) {
1621
+ return (
1622
+ <div className="flex items-center justify-center p-8">
1623
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900" />
1624
+ </div>
1625
+ );
1626
+ }
1627
+
1628
+ if (error) {
1629
+ return (
1630
+ <div className="p-4 bg-red-50 text-red-600 rounded-lg">
1631
+ Failed to load products: {error.message}
1632
+ </div>
1633
+ );
1634
+ }
1635
+
1636
+ return (
1637
+ <div className={\`grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 \${className}\`}>
1638
+ {products?.map((product) => (
1639
+ <ProductCard
1640
+ key={product.id}
1641
+ product={product}
1642
+ onAddToCart={onAddToCart}
1643
+ />
1644
+ ))}
1645
+
1646
+ {products?.length === 0 && (
1647
+ <div className="col-span-full text-center py-8 text-gray-500">
1648
+ No products found
1649
+ </div>
1650
+ )}
1651
+ </div>
1652
+ );
1653
+ }
1654
+ `;
1655
+ }
1656
+
1657
+ async generateIndex(outputDir, ext, features) {
1658
+ const outputPath = path.join(outputDir, `index.${ext === 'tsx' ? 'ts' : 'js'}`);
1659
+
1660
+ const action = await checkFileOverwrite(outputPath);
1661
+ if (action === 'skip') {
1662
+ return null;
1663
+ }
1664
+
1665
+ const exports = [];
1666
+
1667
+ if (features.includes('crm')) {
1668
+ exports.push("export { ContactList } from './ContactList';");
1669
+ exports.push("export { ContactCard } from './ContactCard';");
1670
+ exports.push("export { ContactForm } from './ContactForm';");
1671
+ }
1672
+
1673
+ if (features.includes('events')) {
1674
+ exports.push("export { EventList } from './EventList';");
1675
+ exports.push("export { EventCard } from './EventCard';");
1676
+ }
1677
+
1678
+ if (features.includes('forms')) {
1679
+ exports.push("export { DynamicForm } from './DynamicForm';");
1680
+ }
1681
+
1682
+ if (features.includes('products') || features.includes('checkout')) {
1683
+ exports.push("export { ProductCard } from './ProductCard';");
1684
+ exports.push("export { ProductGrid } from './ProductGrid';");
1685
+ }
1686
+
1687
+ const content = `/**
1688
+ * L4YERCAK3 Components
1689
+ * Auto-generated by @l4yercak3/cli
1690
+ */
1691
+
1692
+ ${exports.join('\n')}
1693
+ `;
1694
+
1695
+ return writeFileWithBackup(outputPath, content, action);
1696
+ }
1697
+ }
1698
+
1699
+ module.exports = new ComponentGenerator();