portfolify 2.0.0

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.
Files changed (66) hide show
  1. package/dist/commands/generate.d.ts +7 -0
  2. package/dist/commands/generate.d.ts.map +1 -0
  3. package/dist/commands/generate.js +129 -0
  4. package/dist/commands/generate.js.map +1 -0
  5. package/dist/commands/portfolio.d.ts +12 -0
  6. package/dist/commands/portfolio.d.ts.map +1 -0
  7. package/dist/commands/portfolio.js +292 -0
  8. package/dist/commands/portfolio.js.map +1 -0
  9. package/dist/generator/index.d.ts +3 -0
  10. package/dist/generator/index.d.ts.map +1 -0
  11. package/dist/generator/index.js +572 -0
  12. package/dist/generator/index.js.map +1 -0
  13. package/dist/generator/portfolio-generator.d.ts +4 -0
  14. package/dist/generator/portfolio-generator.d.ts.map +1 -0
  15. package/dist/generator/portfolio-generator.js +1577 -0
  16. package/dist/generator/portfolio-generator.js.map +1 -0
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +40 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/prompts/index.d.ts +81 -0
  22. package/dist/prompts/index.d.ts.map +1 -0
  23. package/dist/prompts/index.js +330 -0
  24. package/dist/prompts/index.js.map +1 -0
  25. package/dist/prompts/portfolio-prompts.d.ts +60 -0
  26. package/dist/prompts/portfolio-prompts.d.ts.map +1 -0
  27. package/dist/prompts/portfolio-prompts.js +635 -0
  28. package/dist/prompts/portfolio-prompts.js.map +1 -0
  29. package/dist/themes/index.d.ts +15 -0
  30. package/dist/themes/index.d.ts.map +1 -0
  31. package/dist/themes/index.js +64 -0
  32. package/dist/themes/index.js.map +1 -0
  33. package/dist/utils/file.d.ts +5 -0
  34. package/dist/utils/file.d.ts.map +1 -0
  35. package/dist/utils/file.js +33 -0
  36. package/dist/utils/file.js.map +1 -0
  37. package/dist/utils/logger.d.ts +9 -0
  38. package/dist/utils/logger.d.ts.map +1 -0
  39. package/dist/utils/logger.js +22 -0
  40. package/dist/utils/logger.js.map +1 -0
  41. package/dist/utils/validator.d.ts +27 -0
  42. package/dist/utils/validator.d.ts.map +1 -0
  43. package/dist/utils/validator.js +322 -0
  44. package/dist/utils/validator.js.map +1 -0
  45. package/package.json +66 -0
  46. package/templates/index.html +16 -0
  47. package/templates/package.json +37 -0
  48. package/templates/postcss.config.js +6 -0
  49. package/templates/src/App.tsx +56 -0
  50. package/templates/src/components/Blog.tsx +119 -0
  51. package/templates/src/components/Footer.tsx +206 -0
  52. package/templates/src/components/Hero.tsx +182 -0
  53. package/templates/src/components/Projects.tsx +130 -0
  54. package/templates/src/components/SEO.tsx +38 -0
  55. package/templates/src/components/Skills.tsx +107 -0
  56. package/templates/src/components/ThemeToggle.tsx +39 -0
  57. package/templates/src/config/portfolio.json +61 -0
  58. package/templates/src/content/blog/welcome.mdx +29 -0
  59. package/templates/src/lib/blog.ts +63 -0
  60. package/templates/src/lib/utils.ts +6 -0
  61. package/templates/src/main.tsx +13 -0
  62. package/templates/src/styles/globals.css +123 -0
  63. package/templates/tailwind.config.js +65 -0
  64. package/templates/tsconfig.json +37 -0
  65. package/templates/tsconfig.node.json +12 -0
  66. package/templates/vite.config.ts +24 -0
@@ -0,0 +1,1577 @@
1
+ import path from 'path';
2
+ import fs from 'fs-extra';
3
+ import { logger } from '../utils/logger.js';
4
+ export async function generatePortfolioProject(projectName, config, targetDir, options) {
5
+ // Create project directory
6
+ await fs.ensureDir(targetDir);
7
+ // Generate based on framework
8
+ switch (config.framework) {
9
+ case 'react-vite':
10
+ await generateReactViteProject(projectName, config, targetDir, options);
11
+ break;
12
+ case 'nextjs':
13
+ await generateNextJsProject(projectName, config, targetDir, options);
14
+ break;
15
+ case 'sveltekit':
16
+ await generateSvelteKitProject(projectName, config, targetDir, options);
17
+ break;
18
+ default:
19
+ await generateReactViteProject(projectName, config, targetDir, options);
20
+ }
21
+ // Copy custom assets if provided
22
+ if (options.customAssets) {
23
+ await copyCustomAssets(options.customAssets, targetDir);
24
+ }
25
+ // Generate deploy-ready files if requested
26
+ if (options.deployReady) {
27
+ await generateDeployFiles(targetDir, config);
28
+ }
29
+ }
30
+ // =====================================================
31
+ // REACT + VITE GENERATOR
32
+ // =====================================================
33
+ async function generateReactViteProject(projectName, config, targetDir, options) {
34
+ // Create directory structure
35
+ const dirs = [
36
+ 'src/components',
37
+ 'src/pages',
38
+ 'src/styles',
39
+ 'src/assets',
40
+ 'src/lib',
41
+ 'src/config',
42
+ 'public'
43
+ ];
44
+ for (const dir of dirs) {
45
+ await fs.ensureDir(path.join(targetDir, dir));
46
+ }
47
+ // Generate files
48
+ await Promise.all([
49
+ generatePackageJson(targetDir, projectName, config, 'react-vite'),
50
+ generateTsConfig(targetDir, 'react-vite'),
51
+ generateViteConfig(targetDir),
52
+ generateIndexHtml(targetDir, config),
53
+ generateGlobalStyles(targetDir, config),
54
+ generateTailwindConfig(targetDir),
55
+ generatePostCSSConfig(targetDir),
56
+ generateMainTsx(targetDir, config),
57
+ generateAppTsx(targetDir, config),
58
+ generateComponents(targetDir, config),
59
+ generatePortfolioConfig(targetDir, config),
60
+ generateReadme(targetDir, projectName, config),
61
+ generateEnvExample(targetDir),
62
+ generateGitignore(targetDir),
63
+ generateESLintConfig(targetDir, 'react-vite'),
64
+ generatePrettierConfig(targetDir)
65
+ ]);
66
+ // Generate optional features
67
+ if (config.features.blog) {
68
+ await generateBlogFeature(targetDir, config, 'react-vite');
69
+ }
70
+ if (config.features.gallery) {
71
+ await generateGalleryFeature(targetDir, config);
72
+ }
73
+ if (config.features.contactForm) {
74
+ await generateContactFormFeature(targetDir, config);
75
+ }
76
+ if (config.features.testimonials) {
77
+ await generateTestimonialsFeature(targetDir, config);
78
+ }
79
+ }
80
+ // =====================================================
81
+ // NEXT.JS GENERATOR
82
+ // =====================================================
83
+ async function generateNextJsProject(projectName, config, targetDir, options) {
84
+ // Create directory structure
85
+ const dirs = [
86
+ 'src/app',
87
+ 'src/components',
88
+ 'src/styles',
89
+ 'src/lib',
90
+ 'public'
91
+ ];
92
+ for (const dir of dirs) {
93
+ await fs.ensureDir(path.join(targetDir, dir));
94
+ }
95
+ // Generate files
96
+ await Promise.all([
97
+ generatePackageJson(targetDir, projectName, config, 'nextjs'),
98
+ generateTsConfig(targetDir, 'nextjs'),
99
+ generateNextConfig(targetDir),
100
+ generateGlobalStyles(targetDir, config),
101
+ generateTailwindConfig(targetDir),
102
+ generatePostCSSConfig(targetDir),
103
+ generateNextLayout(targetDir, config),
104
+ generateNextPage(targetDir, config),
105
+ generateNextComponents(targetDir, config),
106
+ generateReadme(targetDir, projectName, config),
107
+ generateEnvExample(targetDir),
108
+ generateGitignore(targetDir),
109
+ generateESLintConfig(targetDir, 'nextjs'),
110
+ generatePrettierConfig(targetDir)
111
+ ]);
112
+ // Generate optional features
113
+ if (config.features.blog && config.layout === 'multi-page') {
114
+ await generateNextBlogFeature(targetDir, config);
115
+ }
116
+ }
117
+ // =====================================================
118
+ // SVELTEKIT GENERATOR
119
+ // =====================================================
120
+ async function generateSvelteKitProject(projectName, config, targetDir, options) {
121
+ // Create directory structure
122
+ const dirs = [
123
+ 'src/routes',
124
+ 'src/lib/components',
125
+ 'src/lib/styles',
126
+ 'static'
127
+ ];
128
+ for (const dir of dirs) {
129
+ await fs.ensureDir(path.join(targetDir, dir));
130
+ }
131
+ // Generate files
132
+ await Promise.all([
133
+ generatePackageJson(targetDir, projectName, config, 'sveltekit'),
134
+ generateTsConfig(targetDir, 'sveltekit'),
135
+ generateSvelteConfig(targetDir),
136
+ generateSvelteGlobalStyles(targetDir, config),
137
+ generateTailwindConfig(targetDir),
138
+ generatePostCSSConfig(targetDir),
139
+ generateSvelteLayout(targetDir, config),
140
+ generateSveltePage(targetDir, config),
141
+ generateSvelteComponents(targetDir, config),
142
+ generateReadme(targetDir, projectName, config),
143
+ generateEnvExample(targetDir),
144
+ generateGitignore(targetDir),
145
+ generatePrettierConfig(targetDir)
146
+ ]);
147
+ }
148
+ // =====================================================
149
+ // FILE GENERATORS
150
+ // =====================================================
151
+ async function generatePackageJson(targetDir, projectName, config, framework) {
152
+ let packageJson;
153
+ if (framework === 'react-vite') {
154
+ packageJson = {
155
+ name: projectName,
156
+ private: true,
157
+ version: '0.1.0',
158
+ type: 'module',
159
+ scripts: {
160
+ dev: 'vite',
161
+ build: 'tsc && vite build',
162
+ lint: 'eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0',
163
+ preview: 'vite preview',
164
+ format: 'prettier --write "src/**/*.{ts,tsx,css}"'
165
+ },
166
+ dependencies: {
167
+ 'react': '^18.2.0',
168
+ 'react-dom': '^18.2.0',
169
+ 'framer-motion': '^10.16.16',
170
+ 'lucide-react': '^0.303.0',
171
+ 'clsx': '^2.1.0',
172
+ 'tailwind-merge': '^2.2.0'
173
+ },
174
+ devDependencies: {
175
+ '@types/react': '^18.2.47',
176
+ '@types/react-dom': '^18.2.18',
177
+ '@vitejs/plugin-react': '^4.2.1',
178
+ 'autoprefixer': '^10.4.16',
179
+ 'eslint': '^8.56.0',
180
+ 'eslint-plugin-react-hooks': '^4.6.0',
181
+ 'eslint-plugin-react-refresh': '^0.4.5',
182
+ '@typescript-eslint/eslint-plugin': '^6.18.1',
183
+ '@typescript-eslint/parser': '^6.18.1',
184
+ 'postcss': '^8.4.33',
185
+ 'prettier': '^3.2.2',
186
+ 'tailwindcss': '^3.4.1',
187
+ 'typescript': '^5.3.3',
188
+ 'vite': '^5.0.11'
189
+ }
190
+ };
191
+ }
192
+ else if (framework === 'nextjs') {
193
+ packageJson = {
194
+ name: projectName,
195
+ private: true,
196
+ version: '0.1.0',
197
+ scripts: {
198
+ dev: 'next dev',
199
+ build: 'next build',
200
+ start: 'next start',
201
+ lint: 'next lint',
202
+ format: 'prettier --write "src/**/*.{ts,tsx,css}"'
203
+ },
204
+ dependencies: {
205
+ 'next': '^14.1.0',
206
+ 'react': '^18.2.0',
207
+ 'react-dom': '^18.2.0',
208
+ 'framer-motion': '^10.16.16',
209
+ 'lucide-react': '^0.303.0',
210
+ 'clsx': '^2.1.0',
211
+ 'tailwind-merge': '^2.2.0'
212
+ },
213
+ devDependencies: {
214
+ '@types/node': '^20.11.0',
215
+ '@types/react': '^18.2.47',
216
+ '@types/react-dom': '^18.2.18',
217
+ 'autoprefixer': '^10.4.16',
218
+ 'eslint': '^8.56.0',
219
+ 'eslint-config-next': '^14.1.0',
220
+ 'postcss': '^8.4.33',
221
+ 'prettier': '^3.2.2',
222
+ 'tailwindcss': '^3.4.1',
223
+ 'typescript': '^5.3.3'
224
+ }
225
+ };
226
+ }
227
+ else {
228
+ packageJson = {
229
+ name: projectName,
230
+ private: true,
231
+ version: '0.1.0',
232
+ type: 'module',
233
+ scripts: {
234
+ dev: 'vite dev',
235
+ build: 'vite build',
236
+ preview: 'vite preview',
237
+ format: 'prettier --write "src/**/*.{ts,svelte,css}"'
238
+ },
239
+ dependencies: {
240
+ 'lucide-svelte': '^0.303.0'
241
+ },
242
+ devDependencies: {
243
+ '@sveltejs/adapter-auto': '^3.1.0',
244
+ '@sveltejs/kit': '^2.0.6',
245
+ '@sveltejs/vite-plugin-svelte': '^3.0.1',
246
+ 'autoprefixer': '^10.4.16',
247
+ 'postcss': '^8.4.33',
248
+ 'prettier': '^3.2.2',
249
+ 'prettier-plugin-svelte': '^3.1.2',
250
+ 'svelte': '^4.2.8',
251
+ 'tailwindcss': '^3.4.1',
252
+ 'typescript': '^5.3.3',
253
+ 'vite': '^5.0.11'
254
+ }
255
+ };
256
+ }
257
+ await fs.writeJson(path.join(targetDir, 'package.json'), packageJson, { spaces: 2 });
258
+ }
259
+ async function generateTsConfig(targetDir, framework) {
260
+ let tsConfig;
261
+ if (framework === 'react-vite') {
262
+ tsConfig = {
263
+ compilerOptions: {
264
+ target: 'ES2020',
265
+ useDefineForClassFields: true,
266
+ lib: ['ES2020', 'DOM', 'DOM.Iterable'],
267
+ module: 'ESNext',
268
+ skipLibCheck: true,
269
+ moduleResolution: 'bundler',
270
+ allowImportingTsExtensions: true,
271
+ resolveJsonModule: true,
272
+ isolatedModules: true,
273
+ noEmit: true,
274
+ jsx: 'react-jsx',
275
+ strict: true,
276
+ noUnusedLocals: true,
277
+ noUnusedParameters: true,
278
+ noFallthroughCasesInSwitch: true,
279
+ baseUrl: '.',
280
+ paths: {
281
+ '@/*': ['./src/*']
282
+ }
283
+ },
284
+ include: ['src'],
285
+ references: [{ path: './tsconfig.node.json' }]
286
+ };
287
+ // Also create tsconfig.node.json
288
+ const tsConfigNode = {
289
+ compilerOptions: {
290
+ composite: true,
291
+ skipLibCheck: true,
292
+ module: 'ESNext',
293
+ moduleResolution: 'bundler',
294
+ allowSyntheticDefaultImports: true
295
+ },
296
+ include: ['vite.config.ts']
297
+ };
298
+ await fs.writeJson(path.join(targetDir, 'tsconfig.node.json'), tsConfigNode, { spaces: 2 });
299
+ }
300
+ else if (framework === 'nextjs') {
301
+ tsConfig = {
302
+ compilerOptions: {
303
+ target: 'es5',
304
+ lib: ['dom', 'dom.iterable', 'esnext'],
305
+ allowJs: true,
306
+ skipLibCheck: true,
307
+ strict: true,
308
+ noEmit: true,
309
+ esModuleInterop: true,
310
+ module: 'esnext',
311
+ moduleResolution: 'bundler',
312
+ resolveJsonModule: true,
313
+ isolatedModules: true,
314
+ jsx: 'preserve',
315
+ incremental: true,
316
+ plugins: [{ name: 'next' }],
317
+ paths: {
318
+ '@/*': ['./src/*']
319
+ }
320
+ },
321
+ include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts'],
322
+ exclude: ['node_modules']
323
+ };
324
+ }
325
+ else {
326
+ tsConfig = {
327
+ extends: './.svelte-kit/tsconfig.json',
328
+ compilerOptions: {
329
+ allowJs: true,
330
+ checkJs: true,
331
+ esModuleInterop: true,
332
+ forceConsistentCasingInFileNames: true,
333
+ resolveJsonModule: true,
334
+ skipLibCheck: true,
335
+ sourceMap: true,
336
+ strict: true
337
+ }
338
+ };
339
+ }
340
+ await fs.writeJson(path.join(targetDir, 'tsconfig.json'), tsConfig, { spaces: 2 });
341
+ }
342
+ async function generateViteConfig(targetDir) {
343
+ const config = `import { defineConfig } from 'vite';
344
+ import react from '@vitejs/plugin-react';
345
+ import path from 'path';
346
+
347
+ export default defineConfig({
348
+ plugins: [react()],
349
+ resolve: {
350
+ alias: {
351
+ '@': path.resolve(__dirname, './src'),
352
+ },
353
+ },
354
+ });
355
+ `;
356
+ await fs.writeFile(path.join(targetDir, 'vite.config.ts'), config);
357
+ }
358
+ async function generateNextConfig(targetDir) {
359
+ const config = `/** @type {import('next').NextConfig} */
360
+ const nextConfig = {
361
+ reactStrictMode: true,
362
+ };
363
+
364
+ module.exports = nextConfig;
365
+ `;
366
+ await fs.writeFile(path.join(targetDir, 'next.config.js'), config);
367
+ }
368
+ async function generateSvelteConfig(targetDir) {
369
+ const config = `import adapter from '@sveltejs/adapter-auto';
370
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
371
+
372
+ /** @type {import('@sveltejs/kit').Config} */
373
+ const config = {
374
+ preprocess: vitePreprocess(),
375
+ kit: {
376
+ adapter: adapter()
377
+ }
378
+ };
379
+
380
+ export default config;
381
+ `;
382
+ await fs.writeFile(path.join(targetDir, 'svelte.config.js'), config);
383
+ }
384
+ async function generateIndexHtml(targetDir, config) {
385
+ const html = `<!DOCTYPE html>
386
+ <html lang="en">
387
+ <head>
388
+ <meta charset="UTF-8" />
389
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
390
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
391
+ <meta name="description" content="${config.userData.bio}" />
392
+ <meta name="author" content="${config.userData.name}" />
393
+ <title>${config.userData.name} | ${config.userData.role}</title>
394
+ </head>
395
+ <body>
396
+ <div id="root"></div>
397
+ <script type="module" src="/src/main.tsx"></script>
398
+ </body>
399
+ </html>
400
+ `;
401
+ await fs.writeFile(path.join(targetDir, 'index.html'), html);
402
+ }
403
+ async function generateGlobalStyles(targetDir, config) {
404
+ const colors = config.colorScheme.colors;
405
+ const css = `@tailwind base;
406
+ @tailwind components;
407
+ @tailwind utilities;
408
+
409
+ @layer base {
410
+ :root {
411
+ --primary: ${colors.primary};
412
+ --secondary: ${colors.secondary};
413
+ --accent: ${colors.accent};
414
+ --background: ${colors.background};
415
+ --foreground: ${colors.foreground};
416
+ --muted: ${colors.muted};
417
+ }
418
+
419
+ .dark {
420
+ --background: ${colors.background};
421
+ --foreground: ${colors.foreground};
422
+ }
423
+
424
+ .light {
425
+ --background: 0 0% 100%;
426
+ --foreground: ${colors.background};
427
+ }
428
+ }
429
+
430
+ @layer base {
431
+ * {
432
+ @apply border-border;
433
+ }
434
+
435
+ body {
436
+ @apply bg-background text-foreground;
437
+ font-feature-settings: "rlig" 1, "calt" 1;
438
+ }
439
+ }
440
+
441
+ /* Smooth scrolling */
442
+ html {
443
+ scroll-behavior: smooth;
444
+ }
445
+
446
+ /* Custom scrollbar */
447
+ ::-webkit-scrollbar {
448
+ width: 8px;
449
+ }
450
+
451
+ ::-webkit-scrollbar-track {
452
+ background: hsl(var(--muted));
453
+ }
454
+
455
+ ::-webkit-scrollbar-thumb {
456
+ background: hsl(var(--primary));
457
+ border-radius: 4px;
458
+ }
459
+
460
+ ::-webkit-scrollbar-thumb:hover {
461
+ background: hsl(var(--secondary));
462
+ }
463
+
464
+ /* Selection color */
465
+ ::selection {
466
+ background: hsl(var(--primary) / 0.3);
467
+ }
468
+ `;
469
+ await fs.writeFile(path.join(targetDir, 'src/styles/globals.css'), css);
470
+ }
471
+ async function generateSvelteGlobalStyles(targetDir, config) {
472
+ const colors = config.colorScheme.colors;
473
+ const css = `@tailwind base;
474
+ @tailwind components;
475
+ @tailwind utilities;
476
+
477
+ :root {
478
+ --primary: ${colors.primary};
479
+ --secondary: ${colors.secondary};
480
+ --accent: ${colors.accent};
481
+ --background: ${colors.background};
482
+ --foreground: ${colors.foreground};
483
+ --muted: ${colors.muted};
484
+ }
485
+
486
+ html {
487
+ scroll-behavior: smooth;
488
+ }
489
+
490
+ body {
491
+ background-color: hsl(var(--background));
492
+ color: hsl(var(--foreground));
493
+ }
494
+ `;
495
+ await fs.writeFile(path.join(targetDir, 'src/lib/styles/app.css'), css);
496
+ }
497
+ async function generateTailwindConfig(targetDir) {
498
+ const config = `/** @type {import('tailwindcss').Config} */
499
+ export default {
500
+ content: [
501
+ './index.html',
502
+ './src/**/*.{js,ts,jsx,tsx,svelte}',
503
+ ],
504
+ darkMode: 'class',
505
+ theme: {
506
+ extend: {
507
+ colors: {
508
+ border: 'hsl(var(--muted))',
509
+ background: 'hsl(var(--background))',
510
+ foreground: 'hsl(var(--foreground))',
511
+ primary: {
512
+ DEFAULT: 'hsl(var(--primary))',
513
+ foreground: 'hsl(var(--background))',
514
+ },
515
+ secondary: {
516
+ DEFAULT: 'hsl(var(--secondary))',
517
+ foreground: 'hsl(var(--background))',
518
+ },
519
+ accent: {
520
+ DEFAULT: 'hsl(var(--accent))',
521
+ foreground: 'hsl(var(--background))',
522
+ },
523
+ muted: {
524
+ DEFAULT: 'hsl(var(--muted))',
525
+ foreground: 'hsl(var(--foreground) / 0.7)',
526
+ },
527
+ },
528
+ animation: {
529
+ 'fade-in': 'fadeIn 0.5s ease-out',
530
+ 'slide-up': 'slideUp 0.5s ease-out',
531
+ },
532
+ keyframes: {
533
+ fadeIn: {
534
+ '0%': { opacity: '0' },
535
+ '100%': { opacity: '1' },
536
+ },
537
+ slideUp: {
538
+ '0%': { opacity: '0', transform: 'translateY(20px)' },
539
+ '100%': { opacity: '1', transform: 'translateY(0)' },
540
+ },
541
+ },
542
+ },
543
+ },
544
+ plugins: [],
545
+ };
546
+ `;
547
+ await fs.writeFile(path.join(targetDir, 'tailwind.config.js'), config);
548
+ }
549
+ async function generatePostCSSConfig(targetDir) {
550
+ const config = `export default {
551
+ plugins: {
552
+ tailwindcss: {},
553
+ autoprefixer: {},
554
+ },
555
+ };
556
+ `;
557
+ await fs.writeFile(path.join(targetDir, 'postcss.config.js'), config);
558
+ }
559
+ async function generateMainTsx(targetDir, config) {
560
+ const content = `import React from 'react';
561
+ import ReactDOM from 'react-dom/client';
562
+ import App from './App';
563
+ import './styles/globals.css';
564
+
565
+ ReactDOM.createRoot(document.getElementById('root')!).render(
566
+ <React.StrictMode>
567
+ <App />
568
+ </React.StrictMode>
569
+ );
570
+ `;
571
+ await fs.writeFile(path.join(targetDir, 'src/main.tsx'), content);
572
+ }
573
+ async function generateAppTsx(targetDir, config) {
574
+ const components = ['Hero'];
575
+ if (config.features.gallery)
576
+ components.push('Gallery');
577
+ if (config.features.testimonials)
578
+ components.push('Testimonials');
579
+ if (config.features.blog)
580
+ components.push('Blog');
581
+ if (config.features.contactForm)
582
+ components.push('Contact');
583
+ components.push('Footer');
584
+ const imports = components.map(c => `import ${c} from './components/${c}';`).join('\n');
585
+ const jsx = components.map(c => ` <${c} />`).join('\n');
586
+ const content = `import { useState, useEffect } from 'react';
587
+ import ThemeToggle from './components/ThemeToggle';
588
+ ${imports}
589
+
590
+ function App() {
591
+ const [darkMode, setDarkMode] = useState(true);
592
+
593
+ useEffect(() => {
594
+ const savedTheme = localStorage.getItem('theme');
595
+ if (savedTheme) {
596
+ setDarkMode(savedTheme === 'dark');
597
+ }
598
+ }, []);
599
+
600
+ useEffect(() => {
601
+ document.documentElement.classList.toggle('dark', darkMode);
602
+ document.documentElement.classList.toggle('light', !darkMode);
603
+ localStorage.setItem('theme', darkMode ? 'dark' : 'light');
604
+ }, [darkMode]);
605
+
606
+ return (
607
+ <div className="min-h-screen bg-background text-foreground transition-colors duration-300">
608
+ <ThemeToggle darkMode={darkMode} setDarkMode={setDarkMode} />
609
+ <main>
610
+ ${jsx}
611
+ </main>
612
+ </div>
613
+ );
614
+ }
615
+
616
+ export default App;
617
+ `;
618
+ await fs.writeFile(path.join(targetDir, 'src/App.tsx'), content);
619
+ }
620
+ async function generateComponents(targetDir, config) {
621
+ // Generate Hero component
622
+ await generateHeroComponent(targetDir, config);
623
+ // Generate ThemeToggle
624
+ await generateThemeToggle(targetDir);
625
+ // Generate Footer
626
+ await generateFooterComponent(targetDir, config);
627
+ }
628
+ async function generateHeroComponent(targetDir, config) {
629
+ const content = `import { motion } from 'framer-motion';
630
+
631
+ const Hero = () => {
632
+ return (
633
+ <section className="min-h-screen flex items-center justify-center px-4 py-20">
634
+ <div className="max-w-4xl mx-auto text-center">
635
+ <motion.div
636
+ initial={{ opacity: 0, y: 20 }}
637
+ animate={{ opacity: 1, y: 0 }}
638
+ transition={{ duration: 0.6 }}
639
+ >
640
+ <span className="text-6xl mb-6 block">${config.type.emoji}</span>
641
+ <h1 className="text-4xl md:text-6xl font-bold mb-4 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
642
+ ${config.userData.name}
643
+ </h1>
644
+ <h2 className="text-xl md:text-2xl text-muted-foreground mb-6">
645
+ ${config.userData.role}
646
+ </h2>
647
+ <p className="text-lg text-muted-foreground max-w-2xl mx-auto mb-8">
648
+ ${config.userData.bio}
649
+ </p>
650
+ </motion.div>
651
+
652
+ <motion.div
653
+ initial={{ opacity: 0 }}
654
+ animate={{ opacity: 1 }}
655
+ transition={{ delay: 0.3, duration: 0.6 }}
656
+ className="flex flex-wrap justify-center gap-3 mb-12"
657
+ >
658
+ {${JSON.stringify(config.userData.skills)}.map((skill, index) => (
659
+ <span
660
+ key={index}
661
+ className="px-4 py-2 bg-muted rounded-full text-sm font-medium hover:bg-primary hover:text-primary-foreground transition-colors"
662
+ >
663
+ {skill}
664
+ </span>
665
+ ))}
666
+ </motion.div>
667
+
668
+ <motion.div
669
+ initial={{ opacity: 0, y: 20 }}
670
+ animate={{ opacity: 1, y: 0 }}
671
+ transition={{ delay: 0.5, duration: 0.6 }}
672
+ className="flex justify-center gap-4"
673
+ >
674
+ <a
675
+ href="#contact"
676
+ className="px-8 py-3 bg-primary text-primary-foreground rounded-lg font-medium hover:opacity-90 transition-opacity"
677
+ >
678
+ Get in Touch
679
+ </a>
680
+ <a
681
+ href="#portfolio"
682
+ className="px-8 py-3 border border-primary text-primary rounded-lg font-medium hover:bg-primary hover:text-primary-foreground transition-colors"
683
+ >
684
+ View Work
685
+ </a>
686
+ </motion.div>
687
+ </div>
688
+ </section>
689
+ );
690
+ };
691
+
692
+ export default Hero;
693
+ `;
694
+ await fs.writeFile(path.join(targetDir, 'src/components/Hero.tsx'), content);
695
+ }
696
+ async function generateThemeToggle(targetDir) {
697
+ const content = `import { motion } from 'framer-motion';
698
+ import { Sun, Moon } from 'lucide-react';
699
+
700
+ interface ThemeToggleProps {
701
+ darkMode: boolean;
702
+ setDarkMode: (value: boolean) => void;
703
+ }
704
+
705
+ const ThemeToggle = ({ darkMode, setDarkMode }: ThemeToggleProps) => {
706
+ return (
707
+ <motion.button
708
+ initial={{ opacity: 0, scale: 0.8 }}
709
+ animate={{ opacity: 1, scale: 1 }}
710
+ whileHover={{ scale: 1.1 }}
711
+ whileTap={{ scale: 0.9 }}
712
+ onClick={() => setDarkMode(!darkMode)}
713
+ className="fixed top-6 right-6 z-50 p-3 rounded-full bg-muted hover:bg-primary transition-colors"
714
+ aria-label="Toggle theme"
715
+ >
716
+ {darkMode ? (
717
+ <Sun className="w-5 h-5 text-foreground" />
718
+ ) : (
719
+ <Moon className="w-5 h-5 text-foreground" />
720
+ )}
721
+ </motion.button>
722
+ );
723
+ };
724
+
725
+ export default ThemeToggle;
726
+ `;
727
+ await fs.writeFile(path.join(targetDir, 'src/components/ThemeToggle.tsx'), content);
728
+ }
729
+ async function generateFooterComponent(targetDir, config) {
730
+ const content = `import { motion } from 'framer-motion';
731
+ import { Heart } from 'lucide-react';
732
+
733
+ const Footer = () => {
734
+ const currentYear = new Date().getFullYear();
735
+
736
+ return (
737
+ <footer className="py-8 px-4 border-t border-muted">
738
+ <div className="max-w-4xl mx-auto text-center">
739
+ <motion.div
740
+ initial={{ opacity: 0 }}
741
+ whileInView={{ opacity: 1 }}
742
+ transition={{ duration: 0.5 }}
743
+ viewport={{ once: true }}
744
+ >
745
+ <p className="text-muted-foreground flex items-center justify-center gap-2">
746
+ Made with <Heart className="w-4 h-4 text-red-500" /> by ${config.userData.name}
747
+ </p>
748
+ <p className="text-sm text-muted-foreground mt-2">
749
+ © {currentYear} All rights reserved.
750
+ </p>
751
+ </motion.div>
752
+ </div>
753
+ </footer>
754
+ );
755
+ };
756
+
757
+ export default Footer;
758
+ `;
759
+ await fs.writeFile(path.join(targetDir, 'src/components/Footer.tsx'), content);
760
+ }
761
+ async function generatePortfolioConfig(targetDir, config) {
762
+ const portfolioData = {
763
+ name: config.userData.name,
764
+ role: config.userData.role,
765
+ bio: config.userData.bio,
766
+ skills: config.userData.skills,
767
+ email: config.userData.email,
768
+ social: config.userData.social || {},
769
+ theme: config.type.value,
770
+ colorScheme: config.colorScheme.value
771
+ };
772
+ await fs.writeJson(path.join(targetDir, 'src/config/portfolio.json'), portfolioData, { spaces: 2 });
773
+ }
774
+ async function generateReadme(targetDir, projectName, config) {
775
+ const content = `# ${config.userData.name} - Portfolio
776
+
777
+ ${config.type.emoji} **${config.type.name}** portfolio built with ${getFrameworkName(config.framework)}.
778
+
779
+ ## 🚀 Getting Started
780
+
781
+ \`\`\`bash
782
+ # Install dependencies
783
+ npm install
784
+
785
+ # Start development server
786
+ npm run dev
787
+
788
+ # Build for production
789
+ npm run build
790
+ \`\`\`
791
+
792
+ ## ✨ Features
793
+
794
+ ${config.features.contactForm ? '- 📬 Contact Form\n' : ''}${config.features.gallery ? '- 🖼️ Gallery/Portfolio Showcase\n' : ''}${config.features.blog ? '- 📝 Blog Section\n' : ''}${config.features.testimonials ? '- ⭐ Testimonials\n' : ''}- 🌙 Dark/Light Mode Toggle
795
+ - 📱 Fully Responsive
796
+ - ⚡ Fast & Optimized
797
+
798
+ ## 🛠️ Tech Stack
799
+
800
+ - ${getFrameworkName(config.framework)}
801
+ - TypeScript
802
+ - Tailwind CSS
803
+ - Framer Motion
804
+
805
+ ## 📁 Project Structure
806
+
807
+ \`\`\`
808
+ ${projectName}/
809
+ ├── src/
810
+ │ ├── components/ # React components
811
+ │ ├── styles/ # Global styles
812
+ │ ├── config/ # Configuration files
813
+ │ └── assets/ # Static assets
814
+ ├── public/ # Public files
815
+ └── package.json
816
+ \`\`\`
817
+
818
+ ## 🚀 Deployment
819
+
820
+ ### Vercel
821
+ \`\`\`bash
822
+ npm install -g vercel
823
+ vercel
824
+ \`\`\`
825
+
826
+ ### Netlify
827
+ \`\`\`bash
828
+ npm run build
829
+ # Upload dist folder to Netlify
830
+ \`\`\`
831
+
832
+ ---
833
+
834
+ Built with ❤️ using [ForgeStack Portfolio Generator](https://github.com/forgestack)
835
+ `;
836
+ await fs.writeFile(path.join(targetDir, 'README.md'), content);
837
+ }
838
+ async function generateEnvExample(targetDir) {
839
+ const content = `# Environment Variables
840
+ # Copy this file to .env and fill in your values
841
+
842
+ # Site Configuration
843
+ VITE_SITE_URL=http://localhost:5173
844
+
845
+ # Contact Form (optional)
846
+ # VITE_CONTACT_API_URL=
847
+
848
+ # Analytics (optional)
849
+ # VITE_GA_ID=
850
+ `;
851
+ await fs.writeFile(path.join(targetDir, '.env.example'), content);
852
+ }
853
+ async function generateGitignore(targetDir) {
854
+ const content = `# Dependencies
855
+ node_modules/
856
+
857
+ # Build outputs
858
+ dist/
859
+ .next/
860
+ .svelte-kit/
861
+ build/
862
+
863
+ # Environment files
864
+ .env
865
+ .env.local
866
+ .env.*.local
867
+
868
+ # IDE
869
+ .vscode/
870
+ .idea/
871
+
872
+ # OS
873
+ .DS_Store
874
+ Thumbs.db
875
+
876
+ # Logs
877
+ *.log
878
+ npm-debug.log*
879
+
880
+ # TypeScript
881
+ *.tsbuildinfo
882
+ `;
883
+ await fs.writeFile(path.join(targetDir, '.gitignore'), content);
884
+ }
885
+ async function generateESLintConfig(targetDir, framework) {
886
+ let config;
887
+ if (framework === 'react-vite') {
888
+ config = {
889
+ root: true,
890
+ env: { browser: true, es2020: true },
891
+ extends: [
892
+ 'eslint:recommended',
893
+ 'plugin:@typescript-eslint/recommended',
894
+ 'plugin:react-hooks/recommended'
895
+ ],
896
+ ignorePatterns: ['dist', '.eslintrc.json'],
897
+ parser: '@typescript-eslint/parser',
898
+ plugins: ['react-refresh'],
899
+ rules: {
900
+ 'react-refresh/only-export-components': [
901
+ 'warn',
902
+ { allowConstantExport: true }
903
+ ]
904
+ }
905
+ };
906
+ }
907
+ else if (framework === 'nextjs') {
908
+ config = {
909
+ extends: ['next/core-web-vitals']
910
+ };
911
+ }
912
+ else {
913
+ // SvelteKit uses different linting
914
+ return;
915
+ }
916
+ await fs.writeJson(path.join(targetDir, '.eslintrc.json'), config, { spaces: 2 });
917
+ }
918
+ async function generatePrettierConfig(targetDir) {
919
+ const config = {
920
+ semi: true,
921
+ singleQuote: true,
922
+ tabWidth: 4,
923
+ trailingComma: 'es5',
924
+ printWidth: 100
925
+ };
926
+ await fs.writeJson(path.join(targetDir, '.prettierrc'), config, { spaces: 2 });
927
+ }
928
+ // Feature generators
929
+ async function generateGalleryFeature(targetDir, config) {
930
+ const content = `import { motion } from 'framer-motion';
931
+ import { ExternalLink, Image } from 'lucide-react';
932
+
933
+ const galleryItems = [
934
+ {
935
+ id: 1,
936
+ title: 'Project One',
937
+ description: 'A beautiful project showcasing creative work',
938
+ image: '/placeholder-1.jpg',
939
+ link: '#'
940
+ },
941
+ {
942
+ id: 2,
943
+ title: 'Project Two',
944
+ description: 'Another amazing project with great attention to detail',
945
+ image: '/placeholder-2.jpg',
946
+ link: '#'
947
+ },
948
+ {
949
+ id: 3,
950
+ title: 'Project Three',
951
+ description: 'Innovative solution with modern design',
952
+ image: '/placeholder-3.jpg',
953
+ link: '#'
954
+ },
955
+ {
956
+ id: 4,
957
+ title: 'Project Four',
958
+ description: 'Creative exploration and experimentation',
959
+ image: '/placeholder-4.jpg',
960
+ link: '#'
961
+ }
962
+ ];
963
+
964
+ const Gallery = () => {
965
+ return (
966
+ <section id="portfolio" className="py-20 px-4">
967
+ <div className="max-w-6xl mx-auto">
968
+ <motion.div
969
+ initial={{ opacity: 0, y: 20 }}
970
+ whileInView={{ opacity: 1, y: 0 }}
971
+ transition={{ duration: 0.6 }}
972
+ viewport={{ once: true }}
973
+ className="text-center mb-12"
974
+ >
975
+ <h2 className="text-3xl md:text-4xl font-bold mb-4">
976
+ My <span className="text-primary">Work</span>
977
+ </h2>
978
+ <p className="text-muted-foreground max-w-2xl mx-auto">
979
+ Explore my latest projects and creative endeavors
980
+ </p>
981
+ </motion.div>
982
+
983
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-6">
984
+ {galleryItems.map((item, index) => (
985
+ <motion.div
986
+ key={item.id}
987
+ initial={{ opacity: 0, y: 20 }}
988
+ whileInView={{ opacity: 1, y: 0 }}
989
+ transition={{ duration: 0.5, delay: index * 0.1 }}
990
+ viewport={{ once: true }}
991
+ className="group relative bg-muted rounded-xl overflow-hidden"
992
+ >
993
+ <div className="aspect-video bg-gradient-to-br from-primary/20 to-secondary/20 flex items-center justify-center">
994
+ <Image className="w-16 h-16 text-muted-foreground" />
995
+ </div>
996
+ <div className="absolute inset-0 bg-background/90 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
997
+ <div className="text-center p-6">
998
+ <h3 className="text-xl font-bold mb-2">{item.title}</h3>
999
+ <p className="text-muted-foreground mb-4">{item.description}</p>
1000
+ <a
1001
+ href={item.link}
1002
+ className="inline-flex items-center gap-2 text-primary hover:underline"
1003
+ >
1004
+ View Project <ExternalLink className="w-4 h-4" />
1005
+ </a>
1006
+ </div>
1007
+ </div>
1008
+ </motion.div>
1009
+ ))}
1010
+ </div>
1011
+ </div>
1012
+ </section>
1013
+ );
1014
+ };
1015
+
1016
+ export default Gallery;
1017
+ `;
1018
+ await fs.writeFile(path.join(targetDir, 'src/components/Gallery.tsx'), content);
1019
+ }
1020
+ async function generateContactFormFeature(targetDir, config) {
1021
+ const content = `import { useState } from 'react';
1022
+ import { motion } from 'framer-motion';
1023
+ import { Send, Mail, MapPin } from 'lucide-react';
1024
+
1025
+ const Contact = () => {
1026
+ const [formData, setFormData] = useState({
1027
+ name: '',
1028
+ email: '',
1029
+ message: ''
1030
+ });
1031
+ const [isSubmitting, setIsSubmitting] = useState(false);
1032
+
1033
+ const handleSubmit = async (e: React.FormEvent) => {
1034
+ e.preventDefault();
1035
+ setIsSubmitting(true);
1036
+
1037
+ // Simulate form submission
1038
+ await new Promise(resolve => setTimeout(resolve, 1000));
1039
+
1040
+ alert('Thank you for your message! I will get back to you soon.');
1041
+ setFormData({ name: '', email: '', message: '' });
1042
+ setIsSubmitting(false);
1043
+ };
1044
+
1045
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
1046
+ setFormData(prev => ({
1047
+ ...prev,
1048
+ [e.target.name]: e.target.value
1049
+ }));
1050
+ };
1051
+
1052
+ return (
1053
+ <section id="contact" className="py-20 px-4">
1054
+ <div className="max-w-4xl mx-auto">
1055
+ <motion.div
1056
+ initial={{ opacity: 0, y: 20 }}
1057
+ whileInView={{ opacity: 1, y: 0 }}
1058
+ transition={{ duration: 0.6 }}
1059
+ viewport={{ once: true }}
1060
+ className="text-center mb-12"
1061
+ >
1062
+ <h2 className="text-3xl md:text-4xl font-bold mb-4">
1063
+ Get in <span className="text-primary">Touch</span>
1064
+ </h2>
1065
+ <p className="text-muted-foreground max-w-2xl mx-auto">
1066
+ Have a project in mind? Let's work together to bring your ideas to life.
1067
+ </p>
1068
+ </motion.div>
1069
+
1070
+ <div className="grid md:grid-cols-2 gap-12">
1071
+ <motion.div
1072
+ initial={{ opacity: 0, x: -20 }}
1073
+ whileInView={{ opacity: 1, x: 0 }}
1074
+ transition={{ duration: 0.5 }}
1075
+ viewport={{ once: true }}
1076
+ >
1077
+ <h3 className="text-xl font-bold mb-6">Contact Info</h3>
1078
+ <div className="space-y-4">
1079
+ <div className="flex items-center gap-4">
1080
+ <div className="p-3 bg-primary/10 rounded-lg">
1081
+ <Mail className="w-5 h-5 text-primary" />
1082
+ </div>
1083
+ <div>
1084
+ <p className="text-sm text-muted-foreground">Email</p>
1085
+ <p className="font-medium">${config.userData.email}</p>
1086
+ </div>
1087
+ </div>
1088
+ <div className="flex items-center gap-4">
1089
+ <div className="p-3 bg-primary/10 rounded-lg">
1090
+ <MapPin className="w-5 h-5 text-primary" />
1091
+ </div>
1092
+ <div>
1093
+ <p className="text-sm text-muted-foreground">Location</p>
1094
+ <p className="font-medium">${config.userData.location || 'Available Worldwide'}</p>
1095
+ </div>
1096
+ </div>
1097
+ </div>
1098
+ </motion.div>
1099
+
1100
+ <motion.form
1101
+ initial={{ opacity: 0, x: 20 }}
1102
+ whileInView={{ opacity: 1, x: 0 }}
1103
+ transition={{ duration: 0.5 }}
1104
+ viewport={{ once: true }}
1105
+ onSubmit={handleSubmit}
1106
+ className="space-y-6"
1107
+ >
1108
+ <div>
1109
+ <label htmlFor="name" className="block text-sm font-medium mb-2">
1110
+ Name
1111
+ </label>
1112
+ <input
1113
+ type="text"
1114
+ id="name"
1115
+ name="name"
1116
+ value={formData.name}
1117
+ onChange={handleChange}
1118
+ required
1119
+ className="w-full px-4 py-3 bg-muted rounded-lg border border-muted focus:border-primary focus:outline-none transition-colors"
1120
+ placeholder="Your name"
1121
+ />
1122
+ </div>
1123
+ <div>
1124
+ <label htmlFor="email" className="block text-sm font-medium mb-2">
1125
+ Email
1126
+ </label>
1127
+ <input
1128
+ type="email"
1129
+ id="email"
1130
+ name="email"
1131
+ value={formData.email}
1132
+ onChange={handleChange}
1133
+ required
1134
+ className="w-full px-4 py-3 bg-muted rounded-lg border border-muted focus:border-primary focus:outline-none transition-colors"
1135
+ placeholder="your@email.com"
1136
+ />
1137
+ </div>
1138
+ <div>
1139
+ <label htmlFor="message" className="block text-sm font-medium mb-2">
1140
+ Message
1141
+ </label>
1142
+ <textarea
1143
+ id="message"
1144
+ name="message"
1145
+ value={formData.message}
1146
+ onChange={handleChange}
1147
+ required
1148
+ rows={5}
1149
+ className="w-full px-4 py-3 bg-muted rounded-lg border border-muted focus:border-primary focus:outline-none transition-colors resize-none"
1150
+ placeholder="Tell me about your project..."
1151
+ />
1152
+ </div>
1153
+ <button
1154
+ type="submit"
1155
+ disabled={isSubmitting}
1156
+ className="w-full py-3 bg-primary text-primary-foreground rounded-lg font-medium hover:opacity-90 transition-opacity disabled:opacity-50 flex items-center justify-center gap-2"
1157
+ >
1158
+ {isSubmitting ? 'Sending...' : (
1159
+ <>
1160
+ Send Message <Send className="w-4 h-4" />
1161
+ </>
1162
+ )}
1163
+ </button>
1164
+ </motion.form>
1165
+ </div>
1166
+ </div>
1167
+ </section>
1168
+ );
1169
+ };
1170
+
1171
+ export default Contact;
1172
+ `;
1173
+ await fs.writeFile(path.join(targetDir, 'src/components/Contact.tsx'), content);
1174
+ }
1175
+ async function generateTestimonialsFeature(targetDir, config) {
1176
+ const content = `import { motion } from 'framer-motion';
1177
+ import { Quote, Star } from 'lucide-react';
1178
+
1179
+ const testimonials = [
1180
+ {
1181
+ id: 1,
1182
+ name: 'Sarah Johnson',
1183
+ role: 'CEO, TechStart',
1184
+ content: 'Absolutely amazing work! The attention to detail and professionalism exceeded our expectations.',
1185
+ rating: 5
1186
+ },
1187
+ {
1188
+ id: 2,
1189
+ name: 'Michael Chen',
1190
+ role: 'Founder, DesignCo',
1191
+ content: 'A pleasure to work with. Delivered on time and the quality was outstanding.',
1192
+ rating: 5
1193
+ },
1194
+ {
1195
+ id: 3,
1196
+ name: 'Emily Brown',
1197
+ role: 'Marketing Director',
1198
+ content: 'Creative, reliable, and truly understands client needs. Highly recommended!',
1199
+ rating: 5
1200
+ }
1201
+ ];
1202
+
1203
+ const Testimonials = () => {
1204
+ return (
1205
+ <section className="py-20 px-4 bg-muted/30">
1206
+ <div className="max-w-6xl mx-auto">
1207
+ <motion.div
1208
+ initial={{ opacity: 0, y: 20 }}
1209
+ whileInView={{ opacity: 1, y: 0 }}
1210
+ transition={{ duration: 0.6 }}
1211
+ viewport={{ once: true }}
1212
+ className="text-center mb-12"
1213
+ >
1214
+ <h2 className="text-3xl md:text-4xl font-bold mb-4">
1215
+ What Clients <span className="text-primary">Say</span>
1216
+ </h2>
1217
+ <p className="text-muted-foreground max-w-2xl mx-auto">
1218
+ Hear from the people I've had the pleasure of working with
1219
+ </p>
1220
+ </motion.div>
1221
+
1222
+ <div className="grid md:grid-cols-3 gap-6">
1223
+ {testimonials.map((testimonial, index) => (
1224
+ <motion.div
1225
+ key={testimonial.id}
1226
+ initial={{ opacity: 0, y: 20 }}
1227
+ whileInView={{ opacity: 1, y: 0 }}
1228
+ transition={{ duration: 0.5, delay: index * 0.1 }}
1229
+ viewport={{ once: true }}
1230
+ className="bg-background p-6 rounded-xl border border-muted"
1231
+ >
1232
+ <Quote className="w-8 h-8 text-primary mb-4" />
1233
+ <p className="text-muted-foreground mb-6 italic">
1234
+ "{testimonial.content}"
1235
+ </p>
1236
+ <div className="flex items-center gap-1 mb-4">
1237
+ {[...Array(testimonial.rating)].map((_, i) => (
1238
+ <Star key={i} className="w-4 h-4 fill-primary text-primary" />
1239
+ ))}
1240
+ </div>
1241
+ <div>
1242
+ <p className="font-bold">{testimonial.name}</p>
1243
+ <p className="text-sm text-muted-foreground">{testimonial.role}</p>
1244
+ </div>
1245
+ </motion.div>
1246
+ ))}
1247
+ </div>
1248
+ </div>
1249
+ </section>
1250
+ );
1251
+ };
1252
+
1253
+ export default Testimonials;
1254
+ `;
1255
+ await fs.writeFile(path.join(targetDir, 'src/components/Testimonials.tsx'), content);
1256
+ }
1257
+ async function generateBlogFeature(targetDir, config, framework) {
1258
+ const content = `import { motion } from 'framer-motion';
1259
+ import { Calendar, Clock, ArrowRight } from 'lucide-react';
1260
+
1261
+ const blogPosts = [
1262
+ {
1263
+ id: 1,
1264
+ title: 'Getting Started with Modern Web Development',
1265
+ excerpt: 'A comprehensive guide to starting your journey in web development with the latest tools and frameworks.',
1266
+ date: '2024-01-15',
1267
+ readTime: '5 min',
1268
+ slug: 'getting-started'
1269
+ },
1270
+ {
1271
+ id: 2,
1272
+ title: 'Best Practices for Clean Code',
1273
+ excerpt: 'Learn how to write maintainable, readable, and efficient code that your future self will thank you for.',
1274
+ date: '2024-01-10',
1275
+ readTime: '8 min',
1276
+ slug: 'clean-code'
1277
+ },
1278
+ {
1279
+ id: 3,
1280
+ title: 'The Future of AI in Development',
1281
+ excerpt: 'Exploring how artificial intelligence is shaping the future of software development and what it means for developers.',
1282
+ date: '2024-01-05',
1283
+ readTime: '6 min',
1284
+ slug: 'ai-future'
1285
+ }
1286
+ ];
1287
+
1288
+ const Blog = () => {
1289
+ return (
1290
+ <section id="blog" className="py-20 px-4">
1291
+ <div className="max-w-6xl mx-auto">
1292
+ <motion.div
1293
+ initial={{ opacity: 0, y: 20 }}
1294
+ whileInView={{ opacity: 1, y: 0 }}
1295
+ transition={{ duration: 0.6 }}
1296
+ viewport={{ once: true }}
1297
+ className="text-center mb-12"
1298
+ >
1299
+ <h2 className="text-3xl md:text-4xl font-bold mb-4">
1300
+ Latest <span className="text-primary">Articles</span>
1301
+ </h2>
1302
+ <p className="text-muted-foreground max-w-2xl mx-auto">
1303
+ Thoughts, tutorials, and insights from my journey
1304
+ </p>
1305
+ </motion.div>
1306
+
1307
+ <div className="grid md:grid-cols-3 gap-6">
1308
+ {blogPosts.map((post, index) => (
1309
+ <motion.article
1310
+ key={post.id}
1311
+ initial={{ opacity: 0, y: 20 }}
1312
+ whileInView={{ opacity: 1, y: 0 }}
1313
+ transition={{ duration: 0.5, delay: index * 0.1 }}
1314
+ viewport={{ once: true }}
1315
+ className="bg-muted/50 rounded-xl overflow-hidden hover:bg-muted transition-colors"
1316
+ >
1317
+ <div className="p-6">
1318
+ <div className="flex items-center gap-4 text-sm text-muted-foreground mb-4">
1319
+ <span className="flex items-center gap-1">
1320
+ <Calendar className="w-4 h-4" />
1321
+ {new Date(post.date).toLocaleDateString('en-US', {
1322
+ month: 'short',
1323
+ day: 'numeric',
1324
+ year: 'numeric'
1325
+ })}
1326
+ </span>
1327
+ <span className="flex items-center gap-1">
1328
+ <Clock className="w-4 h-4" />
1329
+ {post.readTime}
1330
+ </span>
1331
+ </div>
1332
+ <h3 className="text-xl font-bold mb-3 hover:text-primary transition-colors">
1333
+ {post.title}
1334
+ </h3>
1335
+ <p className="text-muted-foreground mb-4">
1336
+ {post.excerpt}
1337
+ </p>
1338
+ <a
1339
+ href={\`/blog/\${post.slug}\`}
1340
+ className="inline-flex items-center gap-2 text-primary hover:underline font-medium"
1341
+ >
1342
+ Read More <ArrowRight className="w-4 h-4" />
1343
+ </a>
1344
+ </div>
1345
+ </motion.article>
1346
+ ))}
1347
+ </div>
1348
+ </div>
1349
+ </section>
1350
+ );
1351
+ };
1352
+
1353
+ export default Blog;
1354
+ `;
1355
+ await fs.writeFile(path.join(targetDir, 'src/components/Blog.tsx'), content);
1356
+ }
1357
+ // Next.js specific generators
1358
+ async function generateNextLayout(targetDir, config) {
1359
+ const content = `import type { Metadata } from 'next';
1360
+ import { Inter } from 'next/font/google';
1361
+ import './globals.css';
1362
+
1363
+ const inter = Inter({ subsets: ['latin'] });
1364
+
1365
+ export const metadata: Metadata = {
1366
+ title: '${config.userData.name} | ${config.userData.role}',
1367
+ description: '${config.userData.bio}',
1368
+ };
1369
+
1370
+ export default function RootLayout({
1371
+ children,
1372
+ }: {
1373
+ children: React.ReactNode;
1374
+ }) {
1375
+ return (
1376
+ <html lang="en" className="dark">
1377
+ <body className={inter.className}>{children}</body>
1378
+ </html>
1379
+ );
1380
+ }
1381
+ `;
1382
+ await fs.writeFile(path.join(targetDir, 'src/app/layout.tsx'), content);
1383
+ // Also create globals.css in app folder
1384
+ const colors = config.colorScheme.colors;
1385
+ const css = `@tailwind base;
1386
+ @tailwind components;
1387
+ @tailwind utilities;
1388
+
1389
+ @layer base {
1390
+ :root {
1391
+ --primary: ${colors.primary};
1392
+ --secondary: ${colors.secondary};
1393
+ --accent: ${colors.accent};
1394
+ --background: ${colors.background};
1395
+ --foreground: ${colors.foreground};
1396
+ --muted: ${colors.muted};
1397
+ }
1398
+ }
1399
+
1400
+ body {
1401
+ @apply bg-background text-foreground;
1402
+ }
1403
+ `;
1404
+ await fs.writeFile(path.join(targetDir, 'src/app/globals.css'), css);
1405
+ }
1406
+ async function generateNextPage(targetDir, config) {
1407
+ const content = `'use client';
1408
+
1409
+ import { useState, useEffect } from 'react';
1410
+ import Hero from '@/components/Hero';
1411
+ import Footer from '@/components/Footer';
1412
+
1413
+ export default function Home() {
1414
+ return (
1415
+ <main className="min-h-screen">
1416
+ <Hero />
1417
+ <Footer />
1418
+ </main>
1419
+ );
1420
+ }
1421
+ `;
1422
+ await fs.writeFile(path.join(targetDir, 'src/app/page.tsx'), content);
1423
+ }
1424
+ async function generateNextComponents(targetDir, config) {
1425
+ await generateHeroComponent(targetDir, config);
1426
+ await generateThemeToggle(targetDir);
1427
+ await generateFooterComponent(targetDir, config);
1428
+ }
1429
+ async function generateNextBlogFeature(targetDir, config) {
1430
+ await fs.ensureDir(path.join(targetDir, 'src/app/blog'));
1431
+ await generateBlogFeature(targetDir, config, 'nextjs');
1432
+ }
1433
+ // SvelteKit specific generators
1434
+ async function generateSvelteLayout(targetDir, config) {
1435
+ const content = `<script>
1436
+ import '../lib/styles/app.css';
1437
+ </script>
1438
+
1439
+ <slot />
1440
+ `;
1441
+ await fs.writeFile(path.join(targetDir, 'src/routes/+layout.svelte'), content);
1442
+ }
1443
+ async function generateSveltePage(targetDir, config) {
1444
+ const content = `<script lang="ts">
1445
+ import Hero from '$lib/components/Hero.svelte';
1446
+ import Footer from '$lib/components/Footer.svelte';
1447
+ </script>
1448
+
1449
+ <svelte:head>
1450
+ <title>${config.userData.name} | ${config.userData.role}</title>
1451
+ <meta name="description" content="${config.userData.bio}" />
1452
+ </svelte:head>
1453
+
1454
+ <main class="min-h-screen bg-background text-foreground">
1455
+ <Hero />
1456
+ <Footer />
1457
+ </main>
1458
+ `;
1459
+ await fs.writeFile(path.join(targetDir, 'src/routes/+page.svelte'), content);
1460
+ }
1461
+ async function generateSvelteComponents(targetDir, config) {
1462
+ // Hero component
1463
+ const heroContent = `<script lang="ts">
1464
+ const name = '${config.userData.name}';
1465
+ const role = '${config.userData.role}';
1466
+ const bio = '${config.userData.bio}';
1467
+ const skills = ${JSON.stringify(config.userData.skills)};
1468
+ </script>
1469
+
1470
+ <section class="min-h-screen flex items-center justify-center px-4 py-20">
1471
+ <div class="max-w-4xl mx-auto text-center">
1472
+ <span class="text-6xl mb-6 block">${config.type.emoji}</span>
1473
+ <h1 class="text-4xl md:text-6xl font-bold mb-4 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
1474
+ {name}
1475
+ </h1>
1476
+ <h2 class="text-xl md:text-2xl text-muted-foreground mb-6">
1477
+ {role}
1478
+ </h2>
1479
+ <p class="text-lg text-muted-foreground max-w-2xl mx-auto mb-8">
1480
+ {bio}
1481
+ </p>
1482
+ <div class="flex flex-wrap justify-center gap-3 mb-12">
1483
+ {#each skills as skill}
1484
+ <span class="px-4 py-2 bg-muted rounded-full text-sm font-medium">
1485
+ {skill}
1486
+ </span>
1487
+ {/each}
1488
+ </div>
1489
+ <div class="flex justify-center gap-4">
1490
+ <a
1491
+ href="#contact"
1492
+ class="px-8 py-3 bg-primary text-primary-foreground rounded-lg font-medium hover:opacity-90 transition-opacity"
1493
+ >
1494
+ Get in Touch
1495
+ </a>
1496
+ </div>
1497
+ </div>
1498
+ </section>
1499
+
1500
+ <style>
1501
+ .bg-clip-text {
1502
+ -webkit-background-clip: text;
1503
+ background-clip: text;
1504
+ color: transparent;
1505
+ }
1506
+ </style>
1507
+ `;
1508
+ await fs.writeFile(path.join(targetDir, 'src/lib/components/Hero.svelte'), heroContent);
1509
+ // Footer component
1510
+ const footerContent = `<script lang="ts">
1511
+ const currentYear = new Date().getFullYear();
1512
+ const name = '${config.userData.name}';
1513
+ </script>
1514
+
1515
+ <footer class="py-8 px-4 border-t border-muted">
1516
+ <div class="max-w-4xl mx-auto text-center">
1517
+ <p class="text-muted-foreground">
1518
+ Made with ❤️ by {name}
1519
+ </p>
1520
+ <p class="text-sm text-muted-foreground mt-2">
1521
+ © {currentYear} All rights reserved.
1522
+ </p>
1523
+ </div>
1524
+ </footer>
1525
+ `;
1526
+ await fs.writeFile(path.join(targetDir, 'src/lib/components/Footer.svelte'), footerContent);
1527
+ }
1528
+ // Helper functions
1529
+ async function copyCustomAssets(assetsPath, targetDir) {
1530
+ const sourceDir = path.resolve(assetsPath);
1531
+ const destDir = path.join(targetDir, 'src', 'assets');
1532
+ if (await fs.pathExists(sourceDir)) {
1533
+ await fs.copy(sourceDir, destDir);
1534
+ logger.success('Custom assets copied');
1535
+ }
1536
+ }
1537
+ async function generateDeployFiles(targetDir, config) {
1538
+ // Vercel config
1539
+ const vercelConfig = {
1540
+ framework: config.framework === 'nextjs' ? 'nextjs' : 'vite',
1541
+ buildCommand: 'npm run build',
1542
+ outputDirectory: config.framework === 'nextjs' ? '.next' : 'dist'
1543
+ };
1544
+ await fs.writeJson(path.join(targetDir, 'vercel.json'), vercelConfig, { spaces: 2 });
1545
+ // Netlify config
1546
+ const netlifyConfig = `[build]
1547
+ command = "npm run build"
1548
+ publish = "${config.framework === 'nextjs' ? '.next' : 'dist'}"
1549
+
1550
+ [[redirects]]
1551
+ from = "/*"
1552
+ to = "/index.html"
1553
+ status = 200
1554
+ `;
1555
+ await fs.writeFile(path.join(targetDir, 'netlify.toml'), netlifyConfig);
1556
+ logger.success('Deploy configuration files created');
1557
+ }
1558
+ function getFrameworkName(framework) {
1559
+ const names = {
1560
+ 'react-vite': 'React + Vite',
1561
+ 'nextjs': 'Next.js',
1562
+ 'sveltekit': 'SvelteKit'
1563
+ };
1564
+ return names[framework] || framework;
1565
+ }
1566
+ function getHeroIcons(category) {
1567
+ const icons = {
1568
+ 'Professional': 'Code, Terminal, Briefcase',
1569
+ 'Creative': 'Palette, Camera, PenTool',
1570
+ 'Business': 'Building, TrendingUp, Users',
1571
+ 'Beauty': 'Sparkles, Heart, Star',
1572
+ 'Food': 'UtensilsCrossed, Coffee, ChefHat',
1573
+ 'Personal': 'User, Award, Target'
1574
+ };
1575
+ return icons[category] || 'Star, Heart, Zap';
1576
+ }
1577
+ //# sourceMappingURL=portfolio-generator.js.map