@l4yercak3/cli 1.2.16 → 1.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +3 -1
- package/docs/CRM-PIPELINES-SEQUENCES-SPEC.md +429 -0
- package/docs/INTEGRATION_PATHS_ARCHITECTURE.md +1543 -0
- package/package.json +1 -1
- package/src/commands/login.js +26 -7
- package/src/commands/spread.js +251 -10
- package/src/detectors/database-detector.js +245 -0
- package/src/detectors/expo-detector.js +4 -4
- package/src/detectors/index.js +17 -4
- package/src/generators/api-only/client.js +683 -0
- package/src/generators/api-only/index.js +96 -0
- package/src/generators/api-only/types.js +618 -0
- package/src/generators/api-only/webhooks.js +377 -0
- package/src/generators/env-generator.js +23 -8
- package/src/generators/expo-auth-generator.js +1009 -0
- package/src/generators/index.js +88 -2
- package/src/generators/mcp-guide-generator.js +256 -0
- package/src/generators/quickstart/components/index.js +1699 -0
- package/src/generators/quickstart/components-mobile/index.js +1440 -0
- package/src/generators/quickstart/database/convex.js +1257 -0
- package/src/generators/quickstart/database/index.js +34 -0
- package/src/generators/quickstart/database/supabase.js +1132 -0
- package/src/generators/quickstart/hooks/index.js +1065 -0
- package/src/generators/quickstart/index.js +177 -0
- package/src/generators/quickstart/pages/index.js +1466 -0
- package/src/generators/quickstart/screens/index.js +1498 -0
- package/src/mcp/registry/domains/benefits.js +798 -0
- package/src/mcp/registry/index.js +2 -0
- package/tests/database-detector.test.js +221 -0
- package/tests/expo-detector.test.js +3 -4
- package/tests/generators-index.test.js +215 -3
|
@@ -0,0 +1,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();
|