devfolio-page 0.2.1 → 0.2.3
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/cli/commands/dev.js +110 -11
- package/dist/cli/commands/init.js +78 -28
- package/dist/cli/schemas/portfolio.schema.js +33 -33
- package/dist/generator/builder.js +2 -1
- package/dist/generator/themes/dark-academia/styles.css +1 -1
- package/dist/generator/themes/modern/styles.css +1 -1
- package/dist/generator/themes/srcl/styles.css +1 -1
- package/package.json +1 -1
- package/src/generator/themes/dark-academia/styles.css +1 -1
- package/src/generator/themes/modern/styles.css +1 -1
- package/src/generator/themes/srcl/styles.css +1 -1
package/dist/cli/commands/dev.js
CHANGED
|
@@ -29,11 +29,37 @@ async function rebuild(file, options) {
|
|
|
29
29
|
siteCache = result.files;
|
|
30
30
|
console.log();
|
|
31
31
|
console.log(chalk.green('✓') + ` Rebuilt ${result.fileList.length} files in memory`);
|
|
32
|
+
// Show available pages (HTML only)
|
|
33
|
+
const htmlFiles = Array.from(siteCache.keys()).filter(key => key.endsWith('.html'));
|
|
34
|
+
if (htmlFiles.length > 0) {
|
|
35
|
+
console.log(chalk.dim(' Available pages:'));
|
|
36
|
+
htmlFiles.forEach((key) => {
|
|
37
|
+
// Convert file path to route
|
|
38
|
+
let route = '/' + key.replace(/\.html$/, '').replace(/\/index$/, '');
|
|
39
|
+
if (route === '/')
|
|
40
|
+
route = '/ (home)';
|
|
41
|
+
console.log(chalk.dim(` ${route}`));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
32
44
|
console.log();
|
|
33
45
|
}
|
|
34
46
|
catch (error) {
|
|
35
47
|
console.error(chalk.red('✗') + ' Build failed:');
|
|
36
|
-
|
|
48
|
+
// Show detailed validation errors if available
|
|
49
|
+
if (error instanceof Error && 'errors' in error) {
|
|
50
|
+
const validationError = error;
|
|
51
|
+
console.error(chalk.red(error.message));
|
|
52
|
+
console.log();
|
|
53
|
+
validationError.errors?.forEach((err) => {
|
|
54
|
+
console.error(chalk.yellow(` ${err.path}`) + `: ${err.message}`);
|
|
55
|
+
if (err.hint) {
|
|
56
|
+
console.error(chalk.dim(` Example: ${err.hint}`));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
62
|
+
}
|
|
37
63
|
console.log();
|
|
38
64
|
}
|
|
39
65
|
finally {
|
|
@@ -52,16 +78,62 @@ function startServer(port) {
|
|
|
52
78
|
let filePath = req.url === '/' ? 'index.html' : (req.url || 'index.html');
|
|
53
79
|
// Remove leading slash and query string
|
|
54
80
|
filePath = filePath.replace(/^\//, '').split('?')[0];
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
81
|
+
// Debug: log the request
|
|
82
|
+
console.log(chalk.dim(`\n Request: ${req.url} → normalized: ${filePath}`));
|
|
83
|
+
// Try different path variations to find the file
|
|
84
|
+
let content;
|
|
85
|
+
let matchedPath;
|
|
86
|
+
// 1. Try exact path
|
|
87
|
+
const tryExact = filePath;
|
|
88
|
+
console.log(chalk.dim(` [1] Exact: ${tryExact} ${siteCache.has(tryExact) ? chalk.green('✓') : chalk.red('✗')}`));
|
|
89
|
+
if (siteCache.has(tryExact)) {
|
|
90
|
+
content = siteCache.get(tryExact);
|
|
91
|
+
matchedPath = tryExact;
|
|
92
|
+
}
|
|
93
|
+
// 2. Try adding .html extension
|
|
94
|
+
if (!content) {
|
|
95
|
+
const tryHtml = filePath + '.html';
|
|
96
|
+
console.log(chalk.dim(` [2] +.html: ${tryHtml} ${siteCache.has(tryHtml) ? chalk.green('✓') : chalk.red('✗')}`));
|
|
97
|
+
if (siteCache.has(tryHtml)) {
|
|
98
|
+
content = siteCache.get(tryHtml);
|
|
99
|
+
matchedPath = tryHtml;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// 3. Try as directory with index.html
|
|
103
|
+
if (!content) {
|
|
104
|
+
const tryDir = filePath + '/index.html';
|
|
105
|
+
console.log(chalk.dim(` [3] +/index.html: ${tryDir} ${siteCache.has(tryDir) ? chalk.green('✓') : chalk.red('✗')}`));
|
|
106
|
+
if (siteCache.has(tryDir)) {
|
|
107
|
+
content = siteCache.get(tryDir);
|
|
108
|
+
matchedPath = tryDir;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// 4. Try removing trailing slash and adding index.html
|
|
112
|
+
if (!content && filePath.endsWith('/')) {
|
|
113
|
+
const trySlash = filePath + 'index.html';
|
|
114
|
+
console.log(chalk.dim(` [4] trailing/: ${trySlash} ${siteCache.has(trySlash) ? chalk.green('✓') : chalk.red('✗')}`));
|
|
115
|
+
if (siteCache.has(trySlash)) {
|
|
116
|
+
content = siteCache.get(trySlash);
|
|
117
|
+
matchedPath = trySlash;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// If still not found, return 404
|
|
121
|
+
if (!content) {
|
|
122
|
+
console.log(chalk.dim(` 404: ${req.url}`));
|
|
123
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
58
124
|
res.end('Not found');
|
|
59
125
|
return;
|
|
60
126
|
}
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
127
|
+
// Log successful requests (excluding assets)
|
|
128
|
+
if (matchedPath && matchedPath.endsWith('.html')) {
|
|
129
|
+
console.log(chalk.dim(` 200: ${req.url}`) + chalk.green(` → ${matchedPath}`));
|
|
130
|
+
}
|
|
131
|
+
// Determine content type from original request or file path
|
|
132
|
+
let ext = path.extname(req.url || '');
|
|
133
|
+
// If no extension in URL, check if we're serving HTML
|
|
134
|
+
if (!ext && content) {
|
|
135
|
+
ext = '.html';
|
|
136
|
+
}
|
|
65
137
|
const contentTypes = {
|
|
66
138
|
'.html': 'text/html',
|
|
67
139
|
'.css': 'text/css',
|
|
@@ -82,8 +154,8 @@ function startServer(port) {
|
|
|
82
154
|
res.end(content);
|
|
83
155
|
}
|
|
84
156
|
catch (error) {
|
|
85
|
-
console.error('Server error:', error);
|
|
86
|
-
res.writeHead(500);
|
|
157
|
+
console.error(chalk.red('✗') + ' Server error:', error);
|
|
158
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
87
159
|
res.end('Internal server error');
|
|
88
160
|
}
|
|
89
161
|
});
|
|
@@ -115,10 +187,37 @@ export async function devCommand(file = 'portfolio.yaml', options = {}) {
|
|
|
115
187
|
});
|
|
116
188
|
siteCache = result.files;
|
|
117
189
|
console.log(chalk.green(' ✓') + ` Built ${result.fileList.length} files`);
|
|
190
|
+
console.log();
|
|
191
|
+
// Show available pages (HTML only)
|
|
192
|
+
const htmlFiles = Array.from(siteCache.keys()).filter(key => key.endsWith('.html'));
|
|
193
|
+
if (htmlFiles.length > 0) {
|
|
194
|
+
console.log(chalk.dim(' Available pages:'));
|
|
195
|
+
htmlFiles.forEach((key) => {
|
|
196
|
+
// Convert file path to route
|
|
197
|
+
let route = '/' + key.replace(/\.html$/, '').replace(/\/index$/, '');
|
|
198
|
+
if (route === '/')
|
|
199
|
+
route = '/ (home)';
|
|
200
|
+
console.log(chalk.dim(` ${route}`));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
118
203
|
}
|
|
119
204
|
catch (error) {
|
|
120
205
|
console.error(chalk.red('✗') + ' Initial build failed:');
|
|
121
|
-
|
|
206
|
+
// Show detailed validation errors if available
|
|
207
|
+
if (error instanceof Error && 'errors' in error) {
|
|
208
|
+
const validationError = error;
|
|
209
|
+
console.error(chalk.red(error.message));
|
|
210
|
+
console.log();
|
|
211
|
+
validationError.errors?.forEach((err) => {
|
|
212
|
+
console.error(chalk.yellow(` ${err.path}`) + `: ${err.message}`);
|
|
213
|
+
if (err.hint) {
|
|
214
|
+
console.error(chalk.dim(` Example: ${err.hint}`));
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
220
|
+
}
|
|
122
221
|
process.exit(1);
|
|
123
222
|
}
|
|
124
223
|
// Start server
|
|
@@ -35,6 +35,47 @@ function question(rl, prompt) {
|
|
|
35
35
|
});
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
+
function isValidEmail(email) {
|
|
39
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
40
|
+
return emailRegex.test(email);
|
|
41
|
+
}
|
|
42
|
+
function isValidUrl(url) {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = new URL(url);
|
|
45
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function isValidUsername(username) {
|
|
52
|
+
// Basic validation: alphanumeric, hyphens, underscores
|
|
53
|
+
const usernameRegex = /^[a-zA-Z0-9_-]+$/;
|
|
54
|
+
return usernameRegex.test(username);
|
|
55
|
+
}
|
|
56
|
+
async function questionWithValidation(rl, prompt, validator, optional = false) {
|
|
57
|
+
while (true) {
|
|
58
|
+
const answer = await question(rl, prompt);
|
|
59
|
+
// Allow empty if optional
|
|
60
|
+
if (!answer && optional) {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
// Require value if not optional
|
|
64
|
+
if (!answer && !optional) {
|
|
65
|
+
console.log(chalk.red(' ✗ This field is required'));
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// Validate if validator provided
|
|
69
|
+
if (answer && validator) {
|
|
70
|
+
const result = validator(answer);
|
|
71
|
+
if (!result.valid) {
|
|
72
|
+
console.log(chalk.red(` ✗ ${result.error}`));
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return answer;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
38
79
|
function printHeader() {
|
|
39
80
|
console.log();
|
|
40
81
|
console.log(chalk.cyan('┌────────────────────────────────────────┐'));
|
|
@@ -195,26 +236,15 @@ export async function initCommand() {
|
|
|
195
236
|
rl.close();
|
|
196
237
|
process.exit(1);
|
|
197
238
|
}
|
|
198
|
-
const name = await
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
const location = await question(rl, chalk.cyan('? ') + 'Where are you located? ');
|
|
209
|
-
if (!location) {
|
|
210
|
-
console.log(chalk.red('✗') + ' Location is required');
|
|
211
|
-
process.exit(1);
|
|
212
|
-
}
|
|
213
|
-
const email = await question(rl, chalk.cyan('? ') + 'What\'s your email? ');
|
|
214
|
-
if (!email) {
|
|
215
|
-
console.log(chalk.red('✗') + ' Email is required');
|
|
216
|
-
process.exit(1);
|
|
217
|
-
}
|
|
239
|
+
const name = await questionWithValidation(rl, chalk.cyan('? ') + 'What\'s your name? ');
|
|
240
|
+
const title = await questionWithValidation(rl, chalk.cyan('? ') + 'What\'s your title/role? ');
|
|
241
|
+
const location = await questionWithValidation(rl, chalk.cyan('? ') + 'Where are you located? ');
|
|
242
|
+
const email = await questionWithValidation(rl, chalk.cyan('? ') + 'What\'s your email? ', (value) => {
|
|
243
|
+
if (!isValidEmail(value)) {
|
|
244
|
+
return { valid: false, error: 'Invalid email format (example: you@example.com)' };
|
|
245
|
+
}
|
|
246
|
+
return { valid: true };
|
|
247
|
+
});
|
|
218
248
|
// Theme selection
|
|
219
249
|
console.log();
|
|
220
250
|
console.log(chalk.cyan('? ') + 'Which theme would you like?');
|
|
@@ -236,10 +266,30 @@ export async function initCommand() {
|
|
|
236
266
|
console.log(chalk.yellow(' Invalid choice, using SRCL'));
|
|
237
267
|
}
|
|
238
268
|
console.log(chalk.dim('\n Optional fields (press Enter to skip)'));
|
|
239
|
-
const github = await
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
269
|
+
const github = await questionWithValidation(rl, chalk.cyan('? ') + 'GitHub username? ' + chalk.dim('(optional) '), (value) => {
|
|
270
|
+
if (!isValidUsername(value)) {
|
|
271
|
+
return { valid: false, error: 'Invalid username format (letters, numbers, hyphens, underscores only)' };
|
|
272
|
+
}
|
|
273
|
+
return { valid: true };
|
|
274
|
+
}, true);
|
|
275
|
+
const linkedin = await questionWithValidation(rl, chalk.cyan('? ') + 'LinkedIn username? ' + chalk.dim('(optional) '), (value) => {
|
|
276
|
+
if (!isValidUsername(value)) {
|
|
277
|
+
return { valid: false, error: 'Invalid username format (letters, numbers, hyphens, underscores only)' };
|
|
278
|
+
}
|
|
279
|
+
return { valid: true };
|
|
280
|
+
}, true);
|
|
281
|
+
const twitter = await questionWithValidation(rl, chalk.cyan('? ') + 'Twitter username? ' + chalk.dim('(optional) '), (value) => {
|
|
282
|
+
if (!isValidUsername(value)) {
|
|
283
|
+
return { valid: false, error: 'Invalid username format (letters, numbers, hyphens, underscores only)' };
|
|
284
|
+
}
|
|
285
|
+
return { valid: true };
|
|
286
|
+
}, true);
|
|
287
|
+
const website = await questionWithValidation(rl, chalk.cyan('? ') + 'Website URL? ' + chalk.dim('(optional) '), (value) => {
|
|
288
|
+
if (!isValidUrl(value)) {
|
|
289
|
+
return { valid: false, error: 'Invalid URL format (must start with http:// or https://)' };
|
|
290
|
+
}
|
|
291
|
+
return { valid: true };
|
|
292
|
+
}, true);
|
|
243
293
|
rl.close();
|
|
244
294
|
const input = {
|
|
245
295
|
name,
|
|
@@ -267,12 +317,12 @@ export async function initCommand() {
|
|
|
267
317
|
console.log(chalk.dim(' └── ') + chalk.cyan('site/') + chalk.dim(' (generated after render)'));
|
|
268
318
|
console.log();
|
|
269
319
|
console.log(chalk.bold('Next steps:'));
|
|
270
|
-
console.log(chalk.dim(' 1.') + ' Edit ' + chalk.cyan(`${folderName}/portfolio.yaml`) + ' and add your
|
|
320
|
+
console.log(chalk.dim(' 1.') + ' Edit ' + chalk.cyan(`${folderName}/portfolio.yaml`) + ' and add your content');
|
|
271
321
|
console.log(chalk.dim(' 2.') + ' Add images to ' + chalk.cyan(`${folderName}/images/`));
|
|
272
|
-
console.log(chalk.dim(' 3.') + ' Run: ' + chalk.cyan(`cd ${folderName} && devfolio-page
|
|
273
|
-
console.log(chalk.dim(' 4.') + ' Open: ' + chalk.cyan(
|
|
322
|
+
console.log(chalk.dim(' 3.') + ' Run: ' + chalk.cyan(`cd ${folderName} && devfolio-page dev`));
|
|
323
|
+
console.log(chalk.dim(' 4.') + ' Open: ' + chalk.cyan(`http://localhost:3000`) + ' in your browser');
|
|
274
324
|
console.log();
|
|
275
|
-
console.log('Tip: Check ' + chalk.cyan('https://devfolio.page/
|
|
325
|
+
console.log('Tip: Check ' + chalk.cyan('https://devfolio.page/docs') + ' for full documentation');
|
|
276
326
|
console.log();
|
|
277
327
|
}
|
|
278
328
|
catch (err) {
|
|
@@ -96,8 +96,8 @@ const contentSectionSchema = z.discriminatedUnion('type', [
|
|
|
96
96
|
const simpleProjectSchema = z.object({
|
|
97
97
|
name: z.string().min(1, 'Project name is required'),
|
|
98
98
|
url: urlString.optional(),
|
|
99
|
-
description: z.string().
|
|
100
|
-
tags: z.array(z.string()).
|
|
99
|
+
description: z.string().optional(),
|
|
100
|
+
tags: z.array(z.string()).optional(),
|
|
101
101
|
featured: z.boolean().optional(),
|
|
102
102
|
});
|
|
103
103
|
// Rich project schema (for case studies)
|
|
@@ -109,10 +109,10 @@ const richProjectSchema = z.object({
|
|
|
109
109
|
thumbnail: urlOrPath.optional(),
|
|
110
110
|
hero: urlOrPath.optional(),
|
|
111
111
|
meta: z.object({
|
|
112
|
-
year: z.union([z.string(), z.number()]),
|
|
113
|
-
role: z.string().
|
|
112
|
+
year: z.union([z.string(), z.number()]).optional(),
|
|
113
|
+
role: z.string().optional(),
|
|
114
114
|
timeline: z.string().optional(),
|
|
115
|
-
tech: z.array(z.string()).
|
|
115
|
+
tech: z.array(z.string()).optional(),
|
|
116
116
|
links: z
|
|
117
117
|
.object({
|
|
118
118
|
github: urlString.optional(),
|
|
@@ -121,30 +121,30 @@ const richProjectSchema = z.object({
|
|
|
121
121
|
case_study: urlString.optional(),
|
|
122
122
|
})
|
|
123
123
|
.optional(),
|
|
124
|
-
}),
|
|
125
|
-
sections: z.array(contentSectionSchema).
|
|
124
|
+
}).optional(),
|
|
125
|
+
sections: z.array(contentSectionSchema).optional(),
|
|
126
126
|
});
|
|
127
127
|
// =============================================================================
|
|
128
128
|
// Experience & Education Schemas
|
|
129
129
|
// =============================================================================
|
|
130
130
|
const experienceSchema = z.object({
|
|
131
131
|
company: z.string().min(1, 'Company name is required'),
|
|
132
|
-
role: z.string().
|
|
132
|
+
role: z.string().optional(),
|
|
133
133
|
date: z.object({
|
|
134
|
-
start: dateFormat,
|
|
135
|
-
end: dateOrPresent,
|
|
136
|
-
}),
|
|
134
|
+
start: dateFormat.optional(),
|
|
135
|
+
end: dateOrPresent.optional(),
|
|
136
|
+
}).optional(),
|
|
137
137
|
location: z.string().optional(),
|
|
138
138
|
description: z.string().optional(),
|
|
139
|
-
highlights: z.array(z.string()).
|
|
139
|
+
highlights: z.array(z.string()).optional(),
|
|
140
140
|
});
|
|
141
141
|
const educationSchema = z.object({
|
|
142
142
|
institution: z.string().min(1, 'Institution name is required'),
|
|
143
|
-
degree: z.string().
|
|
143
|
+
degree: z.string().optional(),
|
|
144
144
|
date: z.object({
|
|
145
|
-
start: dateFormat,
|
|
146
|
-
end: dateFormat,
|
|
147
|
-
}),
|
|
145
|
+
start: dateFormat.optional(),
|
|
146
|
+
end: dateFormat.optional(),
|
|
147
|
+
}).optional(),
|
|
148
148
|
location: z.string().optional(),
|
|
149
149
|
description: z.string().optional(),
|
|
150
150
|
highlights: z.array(z.string()).optional(),
|
|
@@ -172,9 +172,9 @@ const richSkillsSchema = z.object({
|
|
|
172
172
|
// =============================================================================
|
|
173
173
|
const writingSchema = z.object({
|
|
174
174
|
title: z.string().min(1, 'Article title is required'),
|
|
175
|
-
url: urlString,
|
|
176
|
-
date: dateFormat,
|
|
177
|
-
description: z.string().optional(),
|
|
175
|
+
url: urlString.optional(),
|
|
176
|
+
date: dateFormat.optional(),
|
|
177
|
+
description: z.string().optional(),
|
|
178
178
|
excerpt: z.string().optional(),
|
|
179
179
|
cover: urlOrPath.optional(),
|
|
180
180
|
publication: z.string().optional(),
|
|
@@ -186,23 +186,23 @@ const writingSchema = z.object({
|
|
|
186
186
|
// =============================================================================
|
|
187
187
|
const experimentSchema = z.object({
|
|
188
188
|
title: z.string().min(1, 'Experiment title is required'),
|
|
189
|
-
description: z.string().
|
|
189
|
+
description: z.string().optional(),
|
|
190
190
|
image: urlOrPath.optional(),
|
|
191
191
|
github: urlString.optional(),
|
|
192
192
|
demo: urlString.optional(),
|
|
193
|
-
tags: z.array(z.string()).
|
|
193
|
+
tags: z.array(z.string()).optional(),
|
|
194
194
|
});
|
|
195
195
|
const testimonialSchema = z.object({
|
|
196
196
|
quote: z.string().min(1, 'Quote is required'),
|
|
197
|
-
author: z.string().
|
|
197
|
+
author: z.string().optional(),
|
|
198
198
|
company: z.string().optional(),
|
|
199
199
|
role: z.string().optional(),
|
|
200
200
|
image: urlOrPath.optional(),
|
|
201
201
|
});
|
|
202
202
|
const timelineItemSchema = z.object({
|
|
203
|
-
year: z.union([z.string(), z.number()]),
|
|
203
|
+
year: z.union([z.string(), z.number()]).optional(),
|
|
204
204
|
title: z.string().min(1, 'Timeline item title is required'),
|
|
205
|
-
description: z.string().
|
|
205
|
+
description: z.string().optional(),
|
|
206
206
|
image: urlOrPath.optional(),
|
|
207
207
|
});
|
|
208
208
|
// =============================================================================
|
|
@@ -227,28 +227,28 @@ export const portfolioSchema = z.object({
|
|
|
227
227
|
// Core metadata
|
|
228
228
|
meta: z.object({
|
|
229
229
|
name: z.string().min(1, 'Name is required'),
|
|
230
|
-
title: z.string().
|
|
230
|
+
title: z.string().optional(),
|
|
231
231
|
tagline: z.string().optional(),
|
|
232
|
-
location: z.string().
|
|
232
|
+
location: z.string().optional(),
|
|
233
233
|
timezone: z.string().optional(),
|
|
234
234
|
avatar: urlOrPath.optional(),
|
|
235
235
|
hero_image: urlOrPath.optional(),
|
|
236
236
|
}),
|
|
237
237
|
// Contact information
|
|
238
238
|
contact: z.object({
|
|
239
|
-
email: z.string().email('Must be a valid email address'),
|
|
239
|
+
email: z.string().email('Must be a valid email address').optional(),
|
|
240
240
|
website: urlString.optional(),
|
|
241
241
|
github: z.string().optional(),
|
|
242
242
|
linkedin: z.string().optional(),
|
|
243
243
|
twitter: z.string().optional(),
|
|
244
|
-
}),
|
|
245
|
-
// Bio
|
|
246
|
-
bio: z.string().
|
|
244
|
+
}).optional(),
|
|
245
|
+
// Bio
|
|
246
|
+
bio: z.string().optional(),
|
|
247
247
|
// Extended about section (optional)
|
|
248
248
|
about: z
|
|
249
249
|
.object({
|
|
250
|
-
short: z.string().
|
|
251
|
-
long: z.string().
|
|
250
|
+
short: z.string().optional(),
|
|
251
|
+
long: z.string().optional(),
|
|
252
252
|
})
|
|
253
253
|
.optional(),
|
|
254
254
|
// Sections (backwards compatible structure)
|
|
@@ -258,7 +258,7 @@ export const portfolioSchema = z.object({
|
|
|
258
258
|
skills: simpleSkillsSchema.optional(),
|
|
259
259
|
writing: z.array(writingSchema).optional(),
|
|
260
260
|
education: z.array(educationSchema).optional(),
|
|
261
|
-
}),
|
|
261
|
+
}).optional(),
|
|
262
262
|
// Rich projects (new structure for case studies)
|
|
263
263
|
projects: z.array(richProjectSchema).optional(),
|
|
264
264
|
// New content types
|
|
@@ -645,7 +645,8 @@ async function generateSinglePageSiteInMemory(portfolio, themePath, theme, files
|
|
|
645
645
|
enableHotkeys: settings.enable_hotkeys !== false,
|
|
646
646
|
animate: settings.animate || 'subtle',
|
|
647
647
|
hasExperience: (portfolio.sections.experience?.length ?? 0) > 0,
|
|
648
|
-
hasProjects: (portfolio.sections.projects?.length ?? 0) > 0,
|
|
648
|
+
hasProjects: ((portfolio.projects?.length ?? 0) > 0) || ((portfolio.sections.projects?.length ?? 0) > 0),
|
|
649
|
+
hasExperiments: (portfolio.experiments?.length ?? 0) > 0,
|
|
649
650
|
hasSkills: portfolio.sections.skills && Object.keys(portfolio.sections.skills).length > 0,
|
|
650
651
|
hasWriting: (portfolio.sections.writing?.length ?? 0) > 0,
|
|
651
652
|
hasEducation: (portfolio.sections.education?.length ?? 0) > 0,
|
package/package.json
CHANGED