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.
- package/dist/commands/generate.d.ts +7 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +129 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/portfolio.d.ts +12 -0
- package/dist/commands/portfolio.d.ts.map +1 -0
- package/dist/commands/portfolio.js +292 -0
- package/dist/commands/portfolio.js.map +1 -0
- package/dist/generator/index.d.ts +3 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +572 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/portfolio-generator.d.ts +4 -0
- package/dist/generator/portfolio-generator.d.ts.map +1 -0
- package/dist/generator/portfolio-generator.js +1577 -0
- package/dist/generator/portfolio-generator.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/index.d.ts +81 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +330 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/portfolio-prompts.d.ts +60 -0
- package/dist/prompts/portfolio-prompts.d.ts.map +1 -0
- package/dist/prompts/portfolio-prompts.js +635 -0
- package/dist/prompts/portfolio-prompts.js.map +1 -0
- package/dist/themes/index.d.ts +15 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +64 -0
- package/dist/themes/index.js.map +1 -0
- package/dist/utils/file.d.ts +5 -0
- package/dist/utils/file.d.ts.map +1 -0
- package/dist/utils/file.js +33 -0
- package/dist/utils/file.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +22 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/validator.d.ts +27 -0
- package/dist/utils/validator.d.ts.map +1 -0
- package/dist/utils/validator.js +322 -0
- package/dist/utils/validator.js.map +1 -0
- package/package.json +66 -0
- package/templates/index.html +16 -0
- package/templates/package.json +37 -0
- package/templates/postcss.config.js +6 -0
- package/templates/src/App.tsx +56 -0
- package/templates/src/components/Blog.tsx +119 -0
- package/templates/src/components/Footer.tsx +206 -0
- package/templates/src/components/Hero.tsx +182 -0
- package/templates/src/components/Projects.tsx +130 -0
- package/templates/src/components/SEO.tsx +38 -0
- package/templates/src/components/Skills.tsx +107 -0
- package/templates/src/components/ThemeToggle.tsx +39 -0
- package/templates/src/config/portfolio.json +61 -0
- package/templates/src/content/blog/welcome.mdx +29 -0
- package/templates/src/lib/blog.ts +63 -0
- package/templates/src/lib/utils.ts +6 -0
- package/templates/src/main.tsx +13 -0
- package/templates/src/styles/globals.css +123 -0
- package/templates/tailwind.config.js +65 -0
- package/templates/tsconfig.json +37 -0
- package/templates/tsconfig.node.json +12 -0
- 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
|