devfolio-page 0.1.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 (115) hide show
  1. package/README.md +219 -0
  2. package/dist/cli/commands/init.js +282 -0
  3. package/dist/cli/commands/render.js +105 -0
  4. package/dist/cli/commands/themes.js +40 -0
  5. package/dist/cli/commands/validate.js +86 -0
  6. package/dist/cli/helpers/validate.js +99 -0
  7. package/dist/cli/index.js +51 -0
  8. package/dist/cli/postinstall.js +20 -0
  9. package/dist/cli/schemas/portfolio.schema.js +299 -0
  10. package/dist/generator/builder.js +551 -0
  11. package/dist/generator/markdown.js +57 -0
  12. package/dist/generator/themes/dark-academia/partials/education.html +15 -0
  13. package/dist/generator/themes/dark-academia/partials/experience.html +23 -0
  14. package/dist/generator/themes/dark-academia/partials/hero.html +15 -0
  15. package/dist/generator/themes/dark-academia/partials/projects.html +17 -0
  16. package/dist/generator/themes/dark-academia/partials/skills.html +11 -0
  17. package/dist/generator/themes/dark-academia/partials/writing.html +15 -0
  18. package/dist/generator/themes/dark-academia/script.js +91 -0
  19. package/dist/generator/themes/dark-academia/styles.css +913 -0
  20. package/dist/generator/themes/dark-academia/template.html +46 -0
  21. package/dist/generator/themes/dark-academia/templates/experiments-index.html +80 -0
  22. package/dist/generator/themes/dark-academia/templates/homepage.html +125 -0
  23. package/dist/generator/themes/dark-academia/templates/project.html +101 -0
  24. package/dist/generator/themes/dark-academia/templates/projects-index.html +80 -0
  25. package/dist/generator/themes/dark-academia/templates/writing-index.html +75 -0
  26. package/dist/generator/themes/modern/partials/education.html +14 -0
  27. package/dist/generator/themes/modern/partials/experience.html +21 -0
  28. package/dist/generator/themes/modern/partials/hero.html +15 -0
  29. package/dist/generator/themes/modern/partials/projects.html +17 -0
  30. package/dist/generator/themes/modern/partials/skills.html +11 -0
  31. package/dist/generator/themes/modern/partials/writing.html +14 -0
  32. package/dist/generator/themes/modern/script.js +136 -0
  33. package/dist/generator/themes/modern/styles.css +835 -0
  34. package/dist/generator/themes/modern/template.html +59 -0
  35. package/dist/generator/themes/modern/templates/experiments-index.html +78 -0
  36. package/dist/generator/themes/modern/templates/homepage.html +125 -0
  37. package/dist/generator/themes/modern/templates/project.html +98 -0
  38. package/dist/generator/themes/modern/templates/projects-index.html +79 -0
  39. package/dist/generator/themes/modern/templates/writing-index.html +73 -0
  40. package/dist/generator/themes/srcl/partials/education.html +27 -0
  41. package/dist/generator/themes/srcl/partials/experience.html +25 -0
  42. package/dist/generator/themes/srcl/partials/hero.html +22 -0
  43. package/dist/generator/themes/srcl/partials/projects.html +24 -0
  44. package/dist/generator/themes/srcl/partials/sections/code.html +8 -0
  45. package/dist/generator/themes/srcl/partials/sections/demo.html +8 -0
  46. package/dist/generator/themes/srcl/partials/sections/gallery.html +12 -0
  47. package/dist/generator/themes/srcl/partials/sections/image.html +6 -0
  48. package/dist/generator/themes/srcl/partials/sections/interactive.html +8 -0
  49. package/dist/generator/themes/srcl/partials/sections/metrics.html +10 -0
  50. package/dist/generator/themes/srcl/partials/sections/outcomes.html +5 -0
  51. package/dist/generator/themes/srcl/partials/sections/overview.html +5 -0
  52. package/dist/generator/themes/srcl/partials/sections/process.html +5 -0
  53. package/dist/generator/themes/srcl/partials/skills.html +21 -0
  54. package/dist/generator/themes/srcl/partials/writing.html +14 -0
  55. package/dist/generator/themes/srcl/script.js +354 -0
  56. package/dist/generator/themes/srcl/styles.css +1260 -0
  57. package/dist/generator/themes/srcl/template.html +46 -0
  58. package/dist/generator/themes/srcl/templates/experiments-index.html +66 -0
  59. package/dist/generator/themes/srcl/templates/homepage.html +136 -0
  60. package/dist/generator/themes/srcl/templates/project.html +96 -0
  61. package/dist/generator/themes/srcl/templates/projects-index.html +70 -0
  62. package/dist/generator/themes/srcl/templates/writing-index.html +61 -0
  63. package/dist/types/portfolio.js +4 -0
  64. package/package.json +58 -0
  65. package/src/generator/themes/dark-academia/partials/education.html +15 -0
  66. package/src/generator/themes/dark-academia/partials/experience.html +23 -0
  67. package/src/generator/themes/dark-academia/partials/hero.html +15 -0
  68. package/src/generator/themes/dark-academia/partials/projects.html +17 -0
  69. package/src/generator/themes/dark-academia/partials/skills.html +11 -0
  70. package/src/generator/themes/dark-academia/partials/writing.html +15 -0
  71. package/src/generator/themes/dark-academia/script.js +91 -0
  72. package/src/generator/themes/dark-academia/styles.css +913 -0
  73. package/src/generator/themes/dark-academia/template.html +46 -0
  74. package/src/generator/themes/dark-academia/templates/experiments-index.html +80 -0
  75. package/src/generator/themes/dark-academia/templates/homepage.html +125 -0
  76. package/src/generator/themes/dark-academia/templates/project.html +101 -0
  77. package/src/generator/themes/dark-academia/templates/projects-index.html +80 -0
  78. package/src/generator/themes/dark-academia/templates/writing-index.html +75 -0
  79. package/src/generator/themes/modern/partials/education.html +14 -0
  80. package/src/generator/themes/modern/partials/experience.html +21 -0
  81. package/src/generator/themes/modern/partials/hero.html +15 -0
  82. package/src/generator/themes/modern/partials/projects.html +17 -0
  83. package/src/generator/themes/modern/partials/skills.html +11 -0
  84. package/src/generator/themes/modern/partials/writing.html +14 -0
  85. package/src/generator/themes/modern/script.js +136 -0
  86. package/src/generator/themes/modern/styles.css +835 -0
  87. package/src/generator/themes/modern/template.html +59 -0
  88. package/src/generator/themes/modern/templates/experiments-index.html +78 -0
  89. package/src/generator/themes/modern/templates/homepage.html +125 -0
  90. package/src/generator/themes/modern/templates/project.html +98 -0
  91. package/src/generator/themes/modern/templates/projects-index.html +79 -0
  92. package/src/generator/themes/modern/templates/writing-index.html +73 -0
  93. package/src/generator/themes/srcl/partials/education.html +27 -0
  94. package/src/generator/themes/srcl/partials/experience.html +25 -0
  95. package/src/generator/themes/srcl/partials/hero.html +22 -0
  96. package/src/generator/themes/srcl/partials/projects.html +24 -0
  97. package/src/generator/themes/srcl/partials/sections/code.html +8 -0
  98. package/src/generator/themes/srcl/partials/sections/demo.html +8 -0
  99. package/src/generator/themes/srcl/partials/sections/gallery.html +12 -0
  100. package/src/generator/themes/srcl/partials/sections/image.html +6 -0
  101. package/src/generator/themes/srcl/partials/sections/interactive.html +8 -0
  102. package/src/generator/themes/srcl/partials/sections/metrics.html +10 -0
  103. package/src/generator/themes/srcl/partials/sections/outcomes.html +5 -0
  104. package/src/generator/themes/srcl/partials/sections/overview.html +5 -0
  105. package/src/generator/themes/srcl/partials/sections/process.html +5 -0
  106. package/src/generator/themes/srcl/partials/skills.html +21 -0
  107. package/src/generator/themes/srcl/partials/writing.html +14 -0
  108. package/src/generator/themes/srcl/script.js +354 -0
  109. package/src/generator/themes/srcl/styles.css +1260 -0
  110. package/src/generator/themes/srcl/template.html +46 -0
  111. package/src/generator/themes/srcl/templates/experiments-index.html +66 -0
  112. package/src/generator/themes/srcl/templates/homepage.html +136 -0
  113. package/src/generator/themes/srcl/templates/project.html +96 -0
  114. package/src/generator/themes/srcl/templates/projects-index.html +70 -0
  115. package/src/generator/themes/srcl/templates/writing-index.html +61 -0
@@ -0,0 +1,86 @@
1
+ import chalk from 'chalk';
2
+ import { validatePortfolio, getPortfolioSummary, ValidationError, FileError, ParseError, } from '../helpers/validate.js';
3
+ export async function validateCommand(file) {
4
+ console.log(chalk.dim(`Validating ${file}...\n`));
5
+ try {
6
+ const portfolio = validatePortfolio(file);
7
+ const summary = getPortfolioSummary(portfolio);
8
+ console.log(chalk.green('✓') + ` ${chalk.bold(file)} is valid\n`);
9
+ // Show summary
10
+ console.log(` ${chalk.dim('Meta:')} ${summary.name}, ${summary.title}`);
11
+ // Build sections summary
12
+ const sections = [];
13
+ if (summary.experienceCount > 0) {
14
+ sections.push(`${summary.experienceCount} experience`);
15
+ }
16
+ if (summary.projectCount > 0) {
17
+ sections.push(`${summary.projectCount} project${summary.projectCount > 1 ? 's' : ''}`);
18
+ }
19
+ if (summary.skillCategoryCount > 0) {
20
+ sections.push(`${summary.skillCategoryCount} skill categor${summary.skillCategoryCount > 1 ? 'ies' : 'y'}`);
21
+ }
22
+ if (summary.writingCount > 0) {
23
+ sections.push(`${summary.writingCount} article${summary.writingCount > 1 ? 's' : ''}`);
24
+ }
25
+ if (summary.educationCount > 0) {
26
+ sections.push(`${summary.educationCount} education`);
27
+ }
28
+ if (sections.length > 0) {
29
+ console.log(` ${chalk.dim('Sections:')} ${sections.join(', ')}`);
30
+ }
31
+ else {
32
+ console.log(` ${chalk.dim('Sections:')} ${chalk.yellow('none (add some content!)')}`);
33
+ }
34
+ console.log(` ${chalk.dim('Theme:')} ${summary.theme}`);
35
+ console.log();
36
+ }
37
+ catch (err) {
38
+ if (err instanceof ValidationError) {
39
+ console.log(chalk.red('✗') + ' Validation failed\n');
40
+ // Group errors by whether they have hints
41
+ const errorsWithHints = [];
42
+ const errorsWithoutHints = [];
43
+ for (const error of err.errors) {
44
+ if (error.hint) {
45
+ errorsWithHints.push(error);
46
+ }
47
+ else {
48
+ errorsWithoutHints.push(error);
49
+ }
50
+ }
51
+ // Show all errors
52
+ for (const error of err.errors) {
53
+ console.log(` ${chalk.red('•')} ${chalk.yellow(error.path)} - ${error.message}`);
54
+ }
55
+ // Show hints for first error that has one
56
+ const firstHint = errorsWithHints[0];
57
+ if (firstHint?.hint) {
58
+ console.log();
59
+ console.log(chalk.dim(' Expected format:'));
60
+ const hintLines = firstHint.hint.split('\n');
61
+ for (const line of hintLines) {
62
+ console.log(chalk.dim(` ${line}`));
63
+ }
64
+ }
65
+ console.log();
66
+ process.exit(1);
67
+ }
68
+ if (err instanceof FileError) {
69
+ console.log(chalk.red('✗') + ` ${err.message}`);
70
+ process.exit(1);
71
+ }
72
+ if (err instanceof ParseError) {
73
+ console.log(chalk.red('✗') + ' YAML syntax error\n');
74
+ if (err.line !== undefined) {
75
+ console.log(` ${chalk.dim('Line')} ${err.line + 1}: ${err.message}`);
76
+ }
77
+ else {
78
+ console.log(` ${err.message}`);
79
+ }
80
+ console.log();
81
+ process.exit(1);
82
+ }
83
+ // Unknown error
84
+ throw err;
85
+ }
86
+ }
@@ -0,0 +1,99 @@
1
+ import { readFileSync } from 'fs';
2
+ import yaml from 'js-yaml';
3
+ import { portfolioSchema } from '../schemas/portfolio.schema.js';
4
+ export class ValidationError extends Error {
5
+ errors;
6
+ constructor(message, errors) {
7
+ super(message);
8
+ this.errors = errors;
9
+ this.name = 'ValidationError';
10
+ }
11
+ }
12
+ export class FileError extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = 'FileError';
16
+ }
17
+ }
18
+ export class ParseError extends Error {
19
+ line;
20
+ constructor(message, line) {
21
+ super(message);
22
+ this.line = line;
23
+ this.name = 'ParseError';
24
+ }
25
+ }
26
+ function getHintForError(path, message) {
27
+ if (path.includes('date') && message.includes('YYYY-MM')) {
28
+ return `date:
29
+ start: 2024-01
30
+ end: present`;
31
+ }
32
+ if (path.includes('email')) {
33
+ return 'email: you@example.com';
34
+ }
35
+ if (path.includes('url') || message.includes('URL')) {
36
+ return 'url: https://example.com';
37
+ }
38
+ if (path.includes('theme')) {
39
+ return "theme: srcl # Options: srcl, modern, minimal, dark-academia";
40
+ }
41
+ if (path.includes('highlights') && message.includes('at least')) {
42
+ return `highlights:
43
+ - Your key achievement or responsibility`;
44
+ }
45
+ if (path.includes('tags') && message.includes('at least')) {
46
+ return 'tags: [TypeScript, React]';
47
+ }
48
+ return undefined;
49
+ }
50
+ export function validatePortfolio(yamlPath) {
51
+ // Read file
52
+ let content;
53
+ try {
54
+ content = readFileSync(yamlPath, 'utf-8');
55
+ }
56
+ catch (err) {
57
+ if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
58
+ throw new FileError(`File not found: ${yamlPath}`);
59
+ }
60
+ throw new FileError(`Cannot read file: ${yamlPath}`);
61
+ }
62
+ // Parse YAML
63
+ let data;
64
+ try {
65
+ data = yaml.load(content);
66
+ }
67
+ catch (err) {
68
+ if (err instanceof yaml.YAMLException) {
69
+ throw new ParseError(`YAML syntax error: ${err.reason}`, err.mark?.line);
70
+ }
71
+ throw new ParseError('Failed to parse YAML');
72
+ }
73
+ // Validate against schema
74
+ const result = portfolioSchema.safeParse(data);
75
+ if (!result.success) {
76
+ const errors = result.error.issues.map((issue) => {
77
+ const path = issue.path.join('.');
78
+ const message = issue.message;
79
+ const hint = getHintForError(path, message);
80
+ return { path, message, hint };
81
+ });
82
+ throw new ValidationError('Portfolio validation failed', errors);
83
+ }
84
+ return result.data;
85
+ }
86
+ export function getPortfolioSummary(portfolio) {
87
+ return {
88
+ name: portfolio.meta.name,
89
+ title: portfolio.meta.title,
90
+ experienceCount: portfolio.sections.experience?.length ?? 0,
91
+ projectCount: portfolio.sections.projects?.length ?? 0,
92
+ skillCategoryCount: portfolio.sections.skills
93
+ ? Object.keys(portfolio.sections.skills).length
94
+ : 0,
95
+ writingCount: portfolio.sections.writing?.length ?? 0,
96
+ educationCount: portfolio.sections.education?.length ?? 0,
97
+ theme: portfolio.theme ?? 'srcl',
98
+ };
99
+ }
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import { initCommand } from './commands/init.js';
5
+ import { validateCommand } from './commands/validate.js';
6
+ import { renderCommand } from './commands/render.js';
7
+ import { themesCommand } from './commands/themes.js';
8
+ const program = new Command();
9
+ program
10
+ .name('devfolio')
11
+ .description('Your portfolio as code. Version control it like software.')
12
+ .version('0.1.0');
13
+ program
14
+ .command('init')
15
+ .description('Create a new portfolio.yaml with interactive prompts')
16
+ .action(initCommand);
17
+ program
18
+ .command('validate <file>')
19
+ .description('Validate a portfolio YAML file')
20
+ .action(validateCommand);
21
+ program
22
+ .command('render [file]')
23
+ .description('Generate static site from portfolio YAML')
24
+ .option('-t, --theme <theme>', 'Theme to use (defaults to YAML theme or srcl)')
25
+ .option('-o, --output <dir>', 'Output directory', './site')
26
+ .action((file, options) => renderCommand(file || 'portfolio.yaml', options));
27
+ program
28
+ .command('themes')
29
+ .description('List available themes with examples')
30
+ .action(themesCommand);
31
+ // Show welcome message if no arguments provided
32
+ if (process.argv.length === 2) {
33
+ console.log();
34
+ console.log(chalk.cyan('┌─────────────────────────────────────────────────────────┐'));
35
+ console.log(chalk.cyan('│') + ' ' + chalk.bold('devfolio.page') + ' - Your portfolio as code ' + chalk.cyan('│'));
36
+ console.log(chalk.cyan('└─────────────────────────────────────────────────────────┘'));
37
+ console.log();
38
+ console.log(chalk.bold('Commands:'));
39
+ console.log();
40
+ console.log(' ' + chalk.cyan('devfolio init') + ' Create a new portfolio folder');
41
+ console.log(' ' + chalk.cyan('devfolio render') + ' Generate site (uses portfolio.yaml)');
42
+ console.log(' ' + chalk.cyan('devfolio themes') + ' List available themes');
43
+ console.log(' ' + chalk.cyan('devfolio validate') + ' <file> Validate a portfolio YAML file');
44
+ console.log(' ' + chalk.cyan('devfolio --help') + ' Show all options');
45
+ console.log();
46
+ console.log(chalk.dim('Get started:'));
47
+ console.log(chalk.dim(' $ ') + 'devfolio init');
48
+ console.log();
49
+ process.exit(0);
50
+ }
51
+ program.parse();
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import chalk from 'chalk';
3
+ // Use stderr so output isn't suppressed during global installs
4
+ const log = (msg = '') => process.stderr.write(msg + '\n');
5
+ log();
6
+ log(chalk.cyan('┌─────────────────────────────────────────────────────────┐'));
7
+ log(chalk.cyan('│') + ' ' + chalk.bold('devfolio.page') + ' installed successfully! ' + chalk.cyan('│'));
8
+ log(chalk.cyan('│') + ' Your portfolio as code. ' + chalk.cyan('│'));
9
+ log(chalk.cyan('└─────────────────────────────────────────────────────────┘'));
10
+ log();
11
+ log(chalk.bold('Available commands:'));
12
+ log();
13
+ log(' ' + chalk.cyan('devfolio init') + ' Create a new portfolio folder');
14
+ log(' ' + chalk.cyan('devfolio render') + ' Generate static site from YAML');
15
+ log(' ' + chalk.cyan('devfolio validate') + ' <file> Validate a portfolio YAML file');
16
+ log(' ' + chalk.cyan('devfolio --help') + ' Show all options');
17
+ log();
18
+ log(chalk.dim('Get started:'));
19
+ log(chalk.dim(' $ ') + 'devfolio init');
20
+ log();
@@ -0,0 +1,299 @@
1
+ import { z } from 'zod';
2
+ // =============================================================================
3
+ // Common Validators
4
+ // =============================================================================
5
+ // Custom date format validator (YYYY-MM or YYYY-MM-DD)
6
+ const dateFormat = z
7
+ .string()
8
+ .regex(/^\d{4}-\d{2}(-\d{2})?$/, 'Date must be in YYYY-MM or YYYY-MM-DD format');
9
+ // Date that can also be "present"
10
+ const dateOrPresent = z.union([dateFormat, z.literal('present')]);
11
+ // URL validation with auto-fix for missing protocol
12
+ const urlString = z
13
+ .string()
14
+ .transform((val) => {
15
+ // Auto-add https:// if no protocol specified
16
+ if (val && !val.match(/^https?:\/\//)) {
17
+ return `https://${val}`;
18
+ }
19
+ return val;
20
+ })
21
+ .pipe(z.string().url('Must be a valid URL'));
22
+ // Optional URL that's less strict (for image paths, etc.)
23
+ const urlOrPath = z.string().min(1, 'Path or URL is required');
24
+ // =============================================================================
25
+ // Content Section Schemas (for rich project pages)
26
+ // =============================================================================
27
+ const overviewSectionSchema = z.object({
28
+ type: z.literal('overview'),
29
+ content: z.string().min(1, 'Overview content is required'),
30
+ });
31
+ const imageSectionSchema = z.object({
32
+ type: z.literal('image'),
33
+ src: urlOrPath,
34
+ caption: z.string().optional(),
35
+ alt: z.string().optional(),
36
+ });
37
+ const gallerySectionSchema = z.object({
38
+ type: z.literal('gallery'),
39
+ images: z
40
+ .array(z.object({
41
+ src: urlOrPath,
42
+ caption: z.string().optional(),
43
+ alt: z.string().optional(),
44
+ }))
45
+ .min(1, 'Gallery must have at least one image'),
46
+ });
47
+ const demoSectionSchema = z.object({
48
+ type: z.literal('demo'),
49
+ title: z.string().optional(),
50
+ embed: z.string().min(1, 'Demo embed code is required'),
51
+ });
52
+ const codeSectionSchema = z.object({
53
+ type: z.literal('code'),
54
+ title: z.string().optional(),
55
+ language: z.string().min(1, 'Code language is required'),
56
+ code: z.string().min(1, 'Code content is required'),
57
+ });
58
+ const metricsSectionSchema = z.object({
59
+ type: z.literal('metrics'),
60
+ data: z
61
+ .array(z.object({
62
+ label: z.string().min(1, 'Metric label is required'),
63
+ value: z.string().min(1, 'Metric value is required'),
64
+ }))
65
+ .min(1, 'At least one metric is required'),
66
+ });
67
+ const outcomesSectionSchema = z.object({
68
+ type: z.literal('outcomes'),
69
+ content: z.string().min(1, 'Outcomes content is required'),
70
+ });
71
+ const processSectionSchema = z.object({
72
+ type: z.literal('process'),
73
+ content: z.string().min(1, 'Process content is required'),
74
+ });
75
+ const interactiveSectionSchema = z.object({
76
+ type: z.literal('interactive'),
77
+ title: z.string().optional(),
78
+ embed_type: z.enum(['iframe', 'embed']),
79
+ url: urlString,
80
+ });
81
+ const contentSectionSchema = z.discriminatedUnion('type', [
82
+ overviewSectionSchema,
83
+ imageSectionSchema,
84
+ gallerySectionSchema,
85
+ demoSectionSchema,
86
+ codeSectionSchema,
87
+ metricsSectionSchema,
88
+ outcomesSectionSchema,
89
+ processSectionSchema,
90
+ interactiveSectionSchema,
91
+ ]);
92
+ // =============================================================================
93
+ // Project Schemas
94
+ // =============================================================================
95
+ // Simple project schema (backwards compatible)
96
+ const simpleProjectSchema = z.object({
97
+ name: z.string().min(1, 'Project name is required'),
98
+ url: urlString.optional(),
99
+ description: z.string().min(1, 'Project description is required'),
100
+ tags: z.array(z.string()).min(1, 'At least one tag is required'),
101
+ featured: z.boolean().optional(),
102
+ });
103
+ // Rich project schema (for case studies)
104
+ const richProjectSchema = z.object({
105
+ id: z.string().min(1, 'Project ID (URL slug) is required'),
106
+ title: z.string().min(1, 'Project title is required'),
107
+ subtitle: z.string().optional(),
108
+ featured: z.boolean().optional(),
109
+ thumbnail: urlOrPath,
110
+ hero: urlOrPath.optional(),
111
+ meta: z.object({
112
+ year: z.union([z.string(), z.number()]),
113
+ role: z.string().min(1, 'Role is required'),
114
+ timeline: z.string().optional(),
115
+ tech: z.array(z.string()).min(1, 'At least one technology is required'),
116
+ links: z
117
+ .object({
118
+ github: urlString.optional(),
119
+ demo: urlString.optional(),
120
+ live: urlString.optional(),
121
+ case_study: urlString.optional(),
122
+ })
123
+ .optional(),
124
+ }),
125
+ sections: z.array(contentSectionSchema).min(1, 'At least one section is required'),
126
+ });
127
+ // =============================================================================
128
+ // Experience & Education Schemas
129
+ // =============================================================================
130
+ const experienceSchema = z.object({
131
+ company: z.string().min(1, 'Company name is required'),
132
+ role: z.string().min(1, 'Role is required'),
133
+ date: z.object({
134
+ start: dateFormat,
135
+ end: dateOrPresent,
136
+ }),
137
+ location: z.string().optional(),
138
+ description: z.string().optional(),
139
+ highlights: z.array(z.string()).min(1, 'At least one highlight is required'),
140
+ });
141
+ const educationSchema = z.object({
142
+ institution: z.string().min(1, 'Institution name is required'),
143
+ degree: z.string().min(1, 'Degree is required'),
144
+ date: z.object({
145
+ start: dateFormat,
146
+ end: dateFormat,
147
+ }),
148
+ location: z.string().optional(),
149
+ description: z.string().optional(),
150
+ highlights: z.array(z.string()).optional(),
151
+ });
152
+ // =============================================================================
153
+ // Skills Schemas
154
+ // =============================================================================
155
+ // Simple skills schema (backwards compatible) - dynamic categories
156
+ const simpleSkillsSchema = z.record(z.string(), z.array(z.string()).min(1, 'Each skill category needs at least one skill'));
157
+ // Rich skills schema with details
158
+ const richSkillsSchema = z.object({
159
+ categories: z.array(z.object({
160
+ name: z.string().min(1, 'Category name is required'),
161
+ level: z.enum(['beginner', 'proficient', 'expert']).optional(),
162
+ items: z.array(z.object({
163
+ name: z.string().min(1, 'Skill name is required'),
164
+ years: z.number().optional(),
165
+ icon: z.string().optional(),
166
+ note: z.string().optional(),
167
+ })),
168
+ })),
169
+ });
170
+ // =============================================================================
171
+ // Writing Schema
172
+ // =============================================================================
173
+ const writingSchema = z.object({
174
+ title: z.string().min(1, 'Article title is required'),
175
+ url: urlString,
176
+ date: dateFormat,
177
+ description: z.string().optional(), // Backwards compatible
178
+ excerpt: z.string().optional(),
179
+ cover: urlOrPath.optional(),
180
+ publication: z.string().optional(),
181
+ tags: z.array(z.string()).optional(),
182
+ featured: z.boolean().optional(),
183
+ });
184
+ // =============================================================================
185
+ // New Content Types
186
+ // =============================================================================
187
+ const experimentSchema = z.object({
188
+ title: z.string().min(1, 'Experiment title is required'),
189
+ description: z.string().min(1, 'Experiment description is required'),
190
+ image: urlOrPath.optional(),
191
+ github: urlString.optional(),
192
+ demo: urlString.optional(),
193
+ tags: z.array(z.string()).min(1, 'At least one tag is required'),
194
+ });
195
+ const testimonialSchema = z.object({
196
+ quote: z.string().min(1, 'Quote is required'),
197
+ author: z.string().min(1, 'Author name is required'),
198
+ company: z.string().optional(),
199
+ role: z.string().optional(),
200
+ image: urlOrPath.optional(),
201
+ });
202
+ const timelineItemSchema = z.object({
203
+ year: z.union([z.string(), z.number()]),
204
+ title: z.string().min(1, 'Timeline item title is required'),
205
+ description: z.string().min(1, 'Timeline item description is required'),
206
+ image: urlOrPath.optional(),
207
+ });
208
+ // =============================================================================
209
+ // Settings & Layout Schemas
210
+ // =============================================================================
211
+ const settingsSchema = z.object({
212
+ show_grid: z.boolean().optional(),
213
+ enable_hotkeys: z.boolean().optional(),
214
+ color_scheme: z.enum(['dark', 'light']).optional(),
215
+ animate: z.enum(['none', 'subtle', 'full']).optional(),
216
+ });
217
+ const layoutSchema = z.object({
218
+ homepage_style: z.enum(['hero', 'grid', 'minimal']).optional(),
219
+ project_layout: z.enum(['case-study', 'grid', 'list']).optional(),
220
+ show_experiments: z.boolean().optional(),
221
+ show_timeline: z.boolean().optional(),
222
+ });
223
+ // =============================================================================
224
+ // Main Portfolio Schema
225
+ // =============================================================================
226
+ export const portfolioSchema = z.object({
227
+ // Core metadata
228
+ meta: z.object({
229
+ name: z.string().min(1, 'Name is required'),
230
+ title: z.string().min(1, 'Professional title is required'),
231
+ tagline: z.string().optional(),
232
+ location: z.string().min(1, 'Location is required'),
233
+ timezone: z.string().optional(),
234
+ avatar: urlOrPath.optional(),
235
+ hero_image: urlOrPath.optional(),
236
+ }),
237
+ // Contact information
238
+ contact: z.object({
239
+ email: z.string().email('Must be a valid email address'),
240
+ website: urlString.optional(),
241
+ github: z.string().optional(),
242
+ linkedin: z.string().optional(),
243
+ twitter: z.string().optional(),
244
+ }),
245
+ // Bio (required for backwards compatibility)
246
+ bio: z.string().min(10, 'Bio should be at least 10 characters'),
247
+ // Extended about section (optional)
248
+ about: z
249
+ .object({
250
+ short: z.string().min(1, 'Short bio is required'),
251
+ long: z.string().min(1, 'Long bio is required'),
252
+ })
253
+ .optional(),
254
+ // Sections (backwards compatible structure)
255
+ sections: z.object({
256
+ experience: z.array(experienceSchema).optional(),
257
+ projects: z.array(simpleProjectSchema).optional(),
258
+ skills: simpleSkillsSchema.optional(),
259
+ writing: z.array(writingSchema).optional(),
260
+ education: z.array(educationSchema).optional(),
261
+ }),
262
+ // Rich projects (new structure for case studies)
263
+ projects: z.array(richProjectSchema).optional(),
264
+ // New content types
265
+ experiments: z.array(experimentSchema).optional(),
266
+ testimonials: z.array(testimonialSchema).optional(),
267
+ timeline: z.array(timelineItemSchema).optional(),
268
+ // Theme selection
269
+ theme: z.enum(['srcl', 'modern', 'minimal', 'dark-academia']).optional(),
270
+ // Layout configuration
271
+ layout: layoutSchema.optional(),
272
+ // Settings
273
+ settings: settingsSchema.optional(),
274
+ });
275
+ // =============================================================================
276
+ // Helper Functions
277
+ // =============================================================================
278
+ // Format Zod errors nicely
279
+ export function formatValidationErrors(error) {
280
+ return error.issues.map((issue) => {
281
+ const path = issue.path.join('.');
282
+ return `${path}: ${issue.message}`;
283
+ });
284
+ }
285
+ // Get a user-friendly name for a section type
286
+ export function getSectionTypeName(type) {
287
+ const names = {
288
+ overview: 'Overview',
289
+ image: 'Image',
290
+ gallery: 'Gallery',
291
+ demo: 'Demo',
292
+ code: 'Code Block',
293
+ metrics: 'Metrics',
294
+ outcomes: 'Outcomes',
295
+ process: 'Process',
296
+ interactive: 'Interactive Embed',
297
+ };
298
+ return names[type] || type;
299
+ }