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.
- package/README.md +219 -0
- package/dist/cli/commands/init.js +282 -0
- package/dist/cli/commands/render.js +105 -0
- package/dist/cli/commands/themes.js +40 -0
- package/dist/cli/commands/validate.js +86 -0
- package/dist/cli/helpers/validate.js +99 -0
- package/dist/cli/index.js +51 -0
- package/dist/cli/postinstall.js +20 -0
- package/dist/cli/schemas/portfolio.schema.js +299 -0
- package/dist/generator/builder.js +551 -0
- package/dist/generator/markdown.js +57 -0
- package/dist/generator/themes/dark-academia/partials/education.html +15 -0
- package/dist/generator/themes/dark-academia/partials/experience.html +23 -0
- package/dist/generator/themes/dark-academia/partials/hero.html +15 -0
- package/dist/generator/themes/dark-academia/partials/projects.html +17 -0
- package/dist/generator/themes/dark-academia/partials/skills.html +11 -0
- package/dist/generator/themes/dark-academia/partials/writing.html +15 -0
- package/dist/generator/themes/dark-academia/script.js +91 -0
- package/dist/generator/themes/dark-academia/styles.css +913 -0
- package/dist/generator/themes/dark-academia/template.html +46 -0
- package/dist/generator/themes/dark-academia/templates/experiments-index.html +80 -0
- package/dist/generator/themes/dark-academia/templates/homepage.html +125 -0
- package/dist/generator/themes/dark-academia/templates/project.html +101 -0
- package/dist/generator/themes/dark-academia/templates/projects-index.html +80 -0
- package/dist/generator/themes/dark-academia/templates/writing-index.html +75 -0
- package/dist/generator/themes/modern/partials/education.html +14 -0
- package/dist/generator/themes/modern/partials/experience.html +21 -0
- package/dist/generator/themes/modern/partials/hero.html +15 -0
- package/dist/generator/themes/modern/partials/projects.html +17 -0
- package/dist/generator/themes/modern/partials/skills.html +11 -0
- package/dist/generator/themes/modern/partials/writing.html +14 -0
- package/dist/generator/themes/modern/script.js +136 -0
- package/dist/generator/themes/modern/styles.css +835 -0
- package/dist/generator/themes/modern/template.html +59 -0
- package/dist/generator/themes/modern/templates/experiments-index.html +78 -0
- package/dist/generator/themes/modern/templates/homepage.html +125 -0
- package/dist/generator/themes/modern/templates/project.html +98 -0
- package/dist/generator/themes/modern/templates/projects-index.html +79 -0
- package/dist/generator/themes/modern/templates/writing-index.html +73 -0
- package/dist/generator/themes/srcl/partials/education.html +27 -0
- package/dist/generator/themes/srcl/partials/experience.html +25 -0
- package/dist/generator/themes/srcl/partials/hero.html +22 -0
- package/dist/generator/themes/srcl/partials/projects.html +24 -0
- package/dist/generator/themes/srcl/partials/sections/code.html +8 -0
- package/dist/generator/themes/srcl/partials/sections/demo.html +8 -0
- package/dist/generator/themes/srcl/partials/sections/gallery.html +12 -0
- package/dist/generator/themes/srcl/partials/sections/image.html +6 -0
- package/dist/generator/themes/srcl/partials/sections/interactive.html +8 -0
- package/dist/generator/themes/srcl/partials/sections/metrics.html +10 -0
- package/dist/generator/themes/srcl/partials/sections/outcomes.html +5 -0
- package/dist/generator/themes/srcl/partials/sections/overview.html +5 -0
- package/dist/generator/themes/srcl/partials/sections/process.html +5 -0
- package/dist/generator/themes/srcl/partials/skills.html +21 -0
- package/dist/generator/themes/srcl/partials/writing.html +14 -0
- package/dist/generator/themes/srcl/script.js +354 -0
- package/dist/generator/themes/srcl/styles.css +1260 -0
- package/dist/generator/themes/srcl/template.html +46 -0
- package/dist/generator/themes/srcl/templates/experiments-index.html +66 -0
- package/dist/generator/themes/srcl/templates/homepage.html +136 -0
- package/dist/generator/themes/srcl/templates/project.html +96 -0
- package/dist/generator/themes/srcl/templates/projects-index.html +70 -0
- package/dist/generator/themes/srcl/templates/writing-index.html +61 -0
- package/dist/types/portfolio.js +4 -0
- package/package.json +58 -0
- package/src/generator/themes/dark-academia/partials/education.html +15 -0
- package/src/generator/themes/dark-academia/partials/experience.html +23 -0
- package/src/generator/themes/dark-academia/partials/hero.html +15 -0
- package/src/generator/themes/dark-academia/partials/projects.html +17 -0
- package/src/generator/themes/dark-academia/partials/skills.html +11 -0
- package/src/generator/themes/dark-academia/partials/writing.html +15 -0
- package/src/generator/themes/dark-academia/script.js +91 -0
- package/src/generator/themes/dark-academia/styles.css +913 -0
- package/src/generator/themes/dark-academia/template.html +46 -0
- package/src/generator/themes/dark-academia/templates/experiments-index.html +80 -0
- package/src/generator/themes/dark-academia/templates/homepage.html +125 -0
- package/src/generator/themes/dark-academia/templates/project.html +101 -0
- package/src/generator/themes/dark-academia/templates/projects-index.html +80 -0
- package/src/generator/themes/dark-academia/templates/writing-index.html +75 -0
- package/src/generator/themes/modern/partials/education.html +14 -0
- package/src/generator/themes/modern/partials/experience.html +21 -0
- package/src/generator/themes/modern/partials/hero.html +15 -0
- package/src/generator/themes/modern/partials/projects.html +17 -0
- package/src/generator/themes/modern/partials/skills.html +11 -0
- package/src/generator/themes/modern/partials/writing.html +14 -0
- package/src/generator/themes/modern/script.js +136 -0
- package/src/generator/themes/modern/styles.css +835 -0
- package/src/generator/themes/modern/template.html +59 -0
- package/src/generator/themes/modern/templates/experiments-index.html +78 -0
- package/src/generator/themes/modern/templates/homepage.html +125 -0
- package/src/generator/themes/modern/templates/project.html +98 -0
- package/src/generator/themes/modern/templates/projects-index.html +79 -0
- package/src/generator/themes/modern/templates/writing-index.html +73 -0
- package/src/generator/themes/srcl/partials/education.html +27 -0
- package/src/generator/themes/srcl/partials/experience.html +25 -0
- package/src/generator/themes/srcl/partials/hero.html +22 -0
- package/src/generator/themes/srcl/partials/projects.html +24 -0
- package/src/generator/themes/srcl/partials/sections/code.html +8 -0
- package/src/generator/themes/srcl/partials/sections/demo.html +8 -0
- package/src/generator/themes/srcl/partials/sections/gallery.html +12 -0
- package/src/generator/themes/srcl/partials/sections/image.html +6 -0
- package/src/generator/themes/srcl/partials/sections/interactive.html +8 -0
- package/src/generator/themes/srcl/partials/sections/metrics.html +10 -0
- package/src/generator/themes/srcl/partials/sections/outcomes.html +5 -0
- package/src/generator/themes/srcl/partials/sections/overview.html +5 -0
- package/src/generator/themes/srcl/partials/sections/process.html +5 -0
- package/src/generator/themes/srcl/partials/skills.html +21 -0
- package/src/generator/themes/srcl/partials/writing.html +14 -0
- package/src/generator/themes/srcl/script.js +354 -0
- package/src/generator/themes/srcl/styles.css +1260 -0
- package/src/generator/themes/srcl/template.html +46 -0
- package/src/generator/themes/srcl/templates/experiments-index.html +66 -0
- package/src/generator/themes/srcl/templates/homepage.html +136 -0
- package/src/generator/themes/srcl/templates/project.html +96 -0
- package/src/generator/themes/srcl/templates/projects-index.html +70 -0
- package/src/generator/themes/srcl/templates/writing-index.html +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# devfolio.page
|
|
2
|
+
|
|
3
|
+
> Your portfolio as code. Version control it like software.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g devfolio-page
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Create a new portfolio folder
|
|
15
|
+
devfolio-page init
|
|
16
|
+
|
|
17
|
+
# Edit the portfolio config
|
|
18
|
+
cd my-portfolio
|
|
19
|
+
nano portfolio.yaml
|
|
20
|
+
|
|
21
|
+
# Generate the static site
|
|
22
|
+
devfolio-page render
|
|
23
|
+
|
|
24
|
+
# Open in browser
|
|
25
|
+
open site/index.html
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Folder Structure
|
|
29
|
+
|
|
30
|
+
After running `devfolio-page init`, you get:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
my-portfolio/
|
|
34
|
+
├── portfolio.yaml # Your config
|
|
35
|
+
├── images/ # Add images here
|
|
36
|
+
└── site/ # Generated after render
|
|
37
|
+
├── index.html
|
|
38
|
+
└── assets/
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
### `devfolio-page init`
|
|
44
|
+
|
|
45
|
+
Create a new portfolio folder with config and images directory.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
devfolio-page init
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### `devfolio-page render`
|
|
52
|
+
|
|
53
|
+
Generate a static website. Run from inside your portfolio folder.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
devfolio-page render # Uses portfolio.yaml → site/
|
|
57
|
+
devfolio-page render --theme modern # Use a different theme
|
|
58
|
+
devfolio-page render --output ./dist # Custom output directory
|
|
59
|
+
devfolio-page render other.yaml # Use a different YAML file
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Options:**
|
|
63
|
+
|
|
64
|
+
- `-t, --theme <theme>` - Theme to use (srcl, modern, dark-academia)
|
|
65
|
+
- `-o, --output <dir>` - Output directory (default: ./site)
|
|
66
|
+
|
|
67
|
+
### `devfolio-page validate <file>`
|
|
68
|
+
|
|
69
|
+
Validate your portfolio YAML file.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
devfolio-page validate portfolio.yaml
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## YAML Schema
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
meta:
|
|
79
|
+
name: Your Name
|
|
80
|
+
title: Your Title
|
|
81
|
+
location: Your Location
|
|
82
|
+
timezone: America/Los_Angeles # optional
|
|
83
|
+
|
|
84
|
+
contact:
|
|
85
|
+
email: you@example.com
|
|
86
|
+
github: username # optional
|
|
87
|
+
linkedin: username # optional
|
|
88
|
+
twitter: username # optional
|
|
89
|
+
website: https://... # optional
|
|
90
|
+
|
|
91
|
+
bio: |
|
|
92
|
+
A few sentences about yourself.
|
|
93
|
+
Can be multiple lines.
|
|
94
|
+
|
|
95
|
+
sections:
|
|
96
|
+
experience:
|
|
97
|
+
- company: Company Name
|
|
98
|
+
role: Your Role
|
|
99
|
+
date:
|
|
100
|
+
start: 2024-01
|
|
101
|
+
end: present
|
|
102
|
+
location: City, State # optional
|
|
103
|
+
highlights:
|
|
104
|
+
- Achievement 1
|
|
105
|
+
- Achievement 2
|
|
106
|
+
|
|
107
|
+
projects:
|
|
108
|
+
- name: Project Name
|
|
109
|
+
url: https://github.com/... # optional
|
|
110
|
+
description: What the project does
|
|
111
|
+
tags: [React, TypeScript]
|
|
112
|
+
featured: true # optional
|
|
113
|
+
|
|
114
|
+
skills:
|
|
115
|
+
Frontend: [React, TypeScript, Next.js]
|
|
116
|
+
Backend: [Node.js, Python, Go]
|
|
117
|
+
Tools: [Git, Docker, AWS]
|
|
118
|
+
|
|
119
|
+
writing:
|
|
120
|
+
- title: Article Title
|
|
121
|
+
url: https://...
|
|
122
|
+
date: 2024-12
|
|
123
|
+
description: Brief description # optional
|
|
124
|
+
|
|
125
|
+
education:
|
|
126
|
+
- institution: University Name
|
|
127
|
+
degree: B.S. Computer Science
|
|
128
|
+
date:
|
|
129
|
+
start: 2016-09
|
|
130
|
+
end: 2020-05
|
|
131
|
+
location: City, State # optional
|
|
132
|
+
highlights: # optional
|
|
133
|
+
- Dean's List
|
|
134
|
+
- Relevant coursework
|
|
135
|
+
|
|
136
|
+
theme: srcl # srcl, modern, dark-academia
|
|
137
|
+
|
|
138
|
+
settings:
|
|
139
|
+
show_grid: false # show character grid overlay
|
|
140
|
+
enable_hotkeys: true # enable keyboard shortcuts
|
|
141
|
+
color_scheme: dark # dark or light
|
|
142
|
+
animate: subtle # none, subtle, or full
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Themes
|
|
146
|
+
|
|
147
|
+
### SRCL (Default)
|
|
148
|
+
|
|
149
|
+
Terminal-inspired aesthetic from [sacred.computer](https://sacred.computer).
|
|
150
|
+
|
|
151
|
+
- Monospace typography
|
|
152
|
+
- Character-based spacing
|
|
153
|
+
- Keyboard navigation (Ctrl+T, Ctrl+G)
|
|
154
|
+
- Dark/light mode toggle
|
|
155
|
+
|
|
156
|
+
### Modern
|
|
157
|
+
|
|
158
|
+
Clean, contemporary design.
|
|
159
|
+
|
|
160
|
+
- Sans-serif fonts
|
|
161
|
+
- Card-based layout
|
|
162
|
+
- Smooth animations
|
|
163
|
+
|
|
164
|
+
### Dark Academia
|
|
165
|
+
|
|
166
|
+
Classical, scholarly design.
|
|
167
|
+
|
|
168
|
+
- Serif typography
|
|
169
|
+
- Warm color palette
|
|
170
|
+
- Book-like layout
|
|
171
|
+
|
|
172
|
+
## Keyboard Shortcuts
|
|
173
|
+
|
|
174
|
+
When `enable_hotkeys: true`:
|
|
175
|
+
|
|
176
|
+
| Key | Action |
|
|
177
|
+
|-----|--------|
|
|
178
|
+
| `Ctrl+T` | Toggle dark/light theme |
|
|
179
|
+
| `Ctrl+G` | Toggle grid overlay |
|
|
180
|
+
| `1-9` | Jump to section |
|
|
181
|
+
| `Esc` | Close accordions |
|
|
182
|
+
|
|
183
|
+
## Philosophy
|
|
184
|
+
|
|
185
|
+
Inspired by [RenderCV](https://github.com/sinaatalay/rendercv). Your portfolio should be:
|
|
186
|
+
|
|
187
|
+
- **Version controlled** - Track changes with Git
|
|
188
|
+
- **Content-first** - Separate content from presentation
|
|
189
|
+
- **Portable** - Deploy anywhere (Vercel, Netlify, GitHub Pages)
|
|
190
|
+
- **Fast** - Static HTML, no JavaScript required
|
|
191
|
+
- **Accessible** - Semantic HTML, keyboard navigation
|
|
192
|
+
|
|
193
|
+
## Development
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Install dependencies
|
|
197
|
+
npm install
|
|
198
|
+
|
|
199
|
+
# Run in dev mode
|
|
200
|
+
npm run dev
|
|
201
|
+
|
|
202
|
+
# Build
|
|
203
|
+
npm run build
|
|
204
|
+
|
|
205
|
+
# Link globally for testing
|
|
206
|
+
npm link
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Examples
|
|
210
|
+
|
|
211
|
+
See the `examples/` directory:
|
|
212
|
+
|
|
213
|
+
- `minimal.yaml` - Bare minimum required fields
|
|
214
|
+
- `full.yaml` - All sections and options
|
|
215
|
+
- `engineer.yaml` - Realistic software engineer example
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
Built with [SRCL](https://sacred.computer).
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import * as readline from 'readline';
|
|
3
|
+
import { writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
const THEMES = [
|
|
6
|
+
{
|
|
7
|
+
id: 'srcl',
|
|
8
|
+
name: 'SRCL',
|
|
9
|
+
description: 'Terminal aesthetic with monospace fonts',
|
|
10
|
+
example: 'https://devfolio.page/portfolios/engineer-srcl/',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'modern',
|
|
14
|
+
name: 'Modern',
|
|
15
|
+
description: 'Clean, contemporary design',
|
|
16
|
+
example: 'https://devfolio.page/portfolios/designer-modern/',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'dark-academia',
|
|
20
|
+
name: 'Dark Academia',
|
|
21
|
+
description: 'Scholarly, vintage aesthetic',
|
|
22
|
+
example: 'https://devfolio.page/portfolios/researcher-academia/',
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
function createInterface() {
|
|
26
|
+
return readline.createInterface({
|
|
27
|
+
input: process.stdin,
|
|
28
|
+
output: process.stdout,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function question(rl, prompt) {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
rl.question(prompt, (answer) => {
|
|
34
|
+
resolve(answer.trim());
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function printHeader() {
|
|
39
|
+
console.log();
|
|
40
|
+
console.log(chalk.cyan('┌────────────────────────────────────────┐'));
|
|
41
|
+
console.log(chalk.cyan('│') + ' Welcome to ' + chalk.bold('devfolio.page') + '! ' + chalk.cyan('│'));
|
|
42
|
+
console.log(chalk.cyan('│') + ' Let\'s create your portfolio ' + chalk.cyan('│'));
|
|
43
|
+
console.log(chalk.cyan('└────────────────────────────────────────┘'));
|
|
44
|
+
console.log();
|
|
45
|
+
console.log('This will create a portfolio folder with your YAML config.');
|
|
46
|
+
console.log('You can add projects, experiments, and writing later.');
|
|
47
|
+
console.log();
|
|
48
|
+
}
|
|
49
|
+
function generateYaml(input) {
|
|
50
|
+
return `# Portfolio configuration for devfolio.page
|
|
51
|
+
# Run: devfolio render (from this folder)
|
|
52
|
+
|
|
53
|
+
meta:
|
|
54
|
+
name: ${input.name}
|
|
55
|
+
title: ${input.title}
|
|
56
|
+
location: ${input.location}
|
|
57
|
+
# avatar: /images/avatar.jpg
|
|
58
|
+
# hero_image: /images/hero.jpg
|
|
59
|
+
|
|
60
|
+
contact:
|
|
61
|
+
email: ${input.email}
|
|
62
|
+
${input.website ? `website: ${input.website}` : '# website: https://yoursite.com'}
|
|
63
|
+
${input.github ? `github: ${input.github}` : '# github: yourusername'}
|
|
64
|
+
${input.linkedin ? `linkedin: ${input.linkedin}` : '# linkedin: yourusername'}
|
|
65
|
+
${input.twitter ? `twitter: ${input.twitter}` : '# twitter: yourusername'}
|
|
66
|
+
|
|
67
|
+
bio: |
|
|
68
|
+
Write a brief introduction about yourself here.
|
|
69
|
+
What do you do? What are you passionate about?
|
|
70
|
+
|
|
71
|
+
about:
|
|
72
|
+
short: |
|
|
73
|
+
A brief description about yourself. This shows on the homepage.
|
|
74
|
+
|
|
75
|
+
long: |
|
|
76
|
+
### Background
|
|
77
|
+
|
|
78
|
+
Tell your story here. Use markdown formatting.
|
|
79
|
+
|
|
80
|
+
### What I Do
|
|
81
|
+
|
|
82
|
+
Describe your work and expertise.
|
|
83
|
+
|
|
84
|
+
### Interests
|
|
85
|
+
|
|
86
|
+
Share what drives you.
|
|
87
|
+
|
|
88
|
+
# Add your projects here
|
|
89
|
+
# Each project can have multiple sections: overview, images, code, demos, etc.
|
|
90
|
+
projects:
|
|
91
|
+
- id: example-project # URL slug
|
|
92
|
+
title: Example Project
|
|
93
|
+
subtitle: A brief description
|
|
94
|
+
featured: true # Show on homepage
|
|
95
|
+
|
|
96
|
+
thumbnail: /images/projects/example/thumbnail.png
|
|
97
|
+
# hero: /images/projects/example/hero.png
|
|
98
|
+
|
|
99
|
+
meta:
|
|
100
|
+
year: 2024
|
|
101
|
+
role: Solo Developer
|
|
102
|
+
timeline: 2 weeks
|
|
103
|
+
tech: [React, TypeScript]
|
|
104
|
+
links:
|
|
105
|
+
github: https://github.com/yourusername/project
|
|
106
|
+
# demo: https://project-demo.com
|
|
107
|
+
|
|
108
|
+
sections:
|
|
109
|
+
- type: overview
|
|
110
|
+
content: |
|
|
111
|
+
## The Problem
|
|
112
|
+
|
|
113
|
+
Describe the problem you solved.
|
|
114
|
+
|
|
115
|
+
## The Solution
|
|
116
|
+
|
|
117
|
+
Explain your approach.
|
|
118
|
+
|
|
119
|
+
# Add more sections: image, gallery, code, metrics, etc.
|
|
120
|
+
# See documentation for all section types
|
|
121
|
+
|
|
122
|
+
# Add side projects and experiments
|
|
123
|
+
experiments:
|
|
124
|
+
- title: Cool Experiment
|
|
125
|
+
description: A fun side project
|
|
126
|
+
# image: /images/experiments/cool.png
|
|
127
|
+
github: https://github.com/yourusername/experiment
|
|
128
|
+
tags: [javascript, fun]
|
|
129
|
+
|
|
130
|
+
sections:
|
|
131
|
+
# Add your work experience
|
|
132
|
+
experience:
|
|
133
|
+
- company: Company Name
|
|
134
|
+
role: Your Role
|
|
135
|
+
date:
|
|
136
|
+
start: 2024-01
|
|
137
|
+
end: present
|
|
138
|
+
location: City, Country
|
|
139
|
+
highlights:
|
|
140
|
+
- Achievement 1
|
|
141
|
+
- Achievement 2
|
|
142
|
+
|
|
143
|
+
# Add your skills by category
|
|
144
|
+
skills:
|
|
145
|
+
Languages: [TypeScript, Python]
|
|
146
|
+
Tools: [Git, Docker]
|
|
147
|
+
|
|
148
|
+
# Add articles or blog posts
|
|
149
|
+
writing:
|
|
150
|
+
- title: Blog Post Title
|
|
151
|
+
url: https://yourblog.com/post
|
|
152
|
+
date: 2024-12
|
|
153
|
+
excerpt: A brief excerpt of the post
|
|
154
|
+
# cover: /images/writing/post.png
|
|
155
|
+
tags: [tech, tutorial]
|
|
156
|
+
featured: true
|
|
157
|
+
|
|
158
|
+
# Add your education (optional)
|
|
159
|
+
# education:
|
|
160
|
+
# - institution: University Name
|
|
161
|
+
# degree: B.S. Computer Science
|
|
162
|
+
# date:
|
|
163
|
+
# start: 2016-09
|
|
164
|
+
# end: 2020-05
|
|
165
|
+
|
|
166
|
+
# Choose your theme
|
|
167
|
+
theme: ${input.theme} # or: srcl, modern, dark-academia
|
|
168
|
+
|
|
169
|
+
# Configure layout
|
|
170
|
+
layout:
|
|
171
|
+
homepage_style: hero # or: grid, minimal
|
|
172
|
+
project_layout: case-study # or: grid, list
|
|
173
|
+
show_experiments: true
|
|
174
|
+
show_timeline: false
|
|
175
|
+
|
|
176
|
+
settings:
|
|
177
|
+
show_grid: false
|
|
178
|
+
enable_hotkeys: true
|
|
179
|
+
color_scheme: dark
|
|
180
|
+
animate: subtle
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
export async function initCommand() {
|
|
184
|
+
printHeader();
|
|
185
|
+
const rl = createInterface();
|
|
186
|
+
try {
|
|
187
|
+
// Ask for folder name first
|
|
188
|
+
let folderName = await question(rl, chalk.cyan('? ') + 'Portfolio folder name? ' + chalk.dim('(portfolio) '));
|
|
189
|
+
if (!folderName) {
|
|
190
|
+
folderName = 'portfolio';
|
|
191
|
+
}
|
|
192
|
+
if (existsSync(folderName)) {
|
|
193
|
+
console.log(chalk.red('✗') + ` ${folderName}/ already exists`);
|
|
194
|
+
console.log(chalk.dim(' Choose a different name or delete the existing folder'));
|
|
195
|
+
rl.close();
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
const name = await question(rl, chalk.cyan('? ') + 'What\'s your name? ');
|
|
199
|
+
if (!name) {
|
|
200
|
+
console.log(chalk.red('✗') + ' Name is required');
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
const title = await question(rl, chalk.cyan('? ') + 'What\'s your title/role? ');
|
|
204
|
+
if (!title) {
|
|
205
|
+
console.log(chalk.red('✗') + ' Title is required');
|
|
206
|
+
process.exit(1);
|
|
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
|
+
}
|
|
218
|
+
// Theme selection
|
|
219
|
+
console.log();
|
|
220
|
+
console.log(chalk.cyan('? ') + 'Which theme would you like?');
|
|
221
|
+
console.log();
|
|
222
|
+
THEMES.forEach((theme, index) => {
|
|
223
|
+
console.log(` ${chalk.cyan(`[${index + 1}]`)} ${chalk.bold(theme.name)} - ${theme.description}`);
|
|
224
|
+
console.log(` ${chalk.dim('See example:')} ${chalk.dim.underline(theme.example)}`);
|
|
225
|
+
});
|
|
226
|
+
console.log();
|
|
227
|
+
let themeChoice = await question(rl, chalk.cyan('? ') + `Enter 1-${THEMES.length} ` + chalk.dim('(1) '));
|
|
228
|
+
if (!themeChoice)
|
|
229
|
+
themeChoice = '1';
|
|
230
|
+
const themeIndex = parseInt(themeChoice, 10) - 1;
|
|
231
|
+
let theme = THEMES[0].id;
|
|
232
|
+
if (themeIndex >= 0 && themeIndex < THEMES.length) {
|
|
233
|
+
theme = THEMES[themeIndex].id;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
console.log(chalk.yellow(' Invalid choice, using SRCL'));
|
|
237
|
+
}
|
|
238
|
+
console.log(chalk.dim('\n Optional fields (press Enter to skip)'));
|
|
239
|
+
const github = await question(rl, chalk.cyan('? ') + 'GitHub username? ' + chalk.dim('(optional) '));
|
|
240
|
+
const linkedin = await question(rl, chalk.cyan('? ') + 'LinkedIn username? ' + chalk.dim('(optional) '));
|
|
241
|
+
const twitter = await question(rl, chalk.cyan('? ') + 'Twitter username? ' + chalk.dim('(optional) '));
|
|
242
|
+
const website = await question(rl, chalk.cyan('? ') + 'Website URL? ' + chalk.dim('(optional) '));
|
|
243
|
+
rl.close();
|
|
244
|
+
const input = {
|
|
245
|
+
name,
|
|
246
|
+
title,
|
|
247
|
+
location,
|
|
248
|
+
email,
|
|
249
|
+
github: github || undefined,
|
|
250
|
+
linkedin: linkedin || undefined,
|
|
251
|
+
twitter: twitter || undefined,
|
|
252
|
+
website: website || undefined,
|
|
253
|
+
theme,
|
|
254
|
+
};
|
|
255
|
+
// Create folder structure
|
|
256
|
+
mkdirSync(folderName, { recursive: true });
|
|
257
|
+
mkdirSync(path.join(folderName, 'images'), { recursive: true });
|
|
258
|
+
const yamlPath = path.join(folderName, 'portfolio.yaml');
|
|
259
|
+
const yaml = generateYaml(input);
|
|
260
|
+
writeFileSync(yamlPath, yaml);
|
|
261
|
+
console.log();
|
|
262
|
+
console.log(chalk.green('✓') + ' Created ' + chalk.bold(folderName + '/'));
|
|
263
|
+
console.log();
|
|
264
|
+
console.log(chalk.dim(' Structure:'));
|
|
265
|
+
console.log(chalk.dim(' ├── ') + chalk.cyan('portfolio.yaml') + chalk.dim(' (your config)'));
|
|
266
|
+
console.log(chalk.dim(' ├── ') + chalk.cyan('images/') + chalk.dim(' (add images here)'));
|
|
267
|
+
console.log(chalk.dim(' └── ') + chalk.cyan('site/') + chalk.dim(' (generated after render)'));
|
|
268
|
+
console.log();
|
|
269
|
+
console.log(chalk.bold('Next steps:'));
|
|
270
|
+
console.log(chalk.dim(' 1.') + ' Edit ' + chalk.cyan(`${folderName}/portfolio.yaml`) + ' and add your projects');
|
|
271
|
+
console.log(chalk.dim(' 2.') + ' Add images to ' + chalk.cyan(`${folderName}/images/`));
|
|
272
|
+
console.log(chalk.dim(' 3.') + ' Run: ' + chalk.cyan(`cd ${folderName} && devfolio render`));
|
|
273
|
+
console.log(chalk.dim(' 4.') + ' Open: ' + chalk.cyan(`${folderName}/site/index.html`) + ' to view');
|
|
274
|
+
console.log();
|
|
275
|
+
console.log('Tip: Check ' + chalk.cyan('https://devfolio.page/portfolios/') + ' for examples');
|
|
276
|
+
console.log();
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
rl.close();
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { validatePortfolio, ValidationError, FileError, ParseError, } from '../helpers/validate.js';
|
|
5
|
+
import { buildStaticSite } from '../../generator/builder.js';
|
|
6
|
+
const TOTAL_STEPS = 4;
|
|
7
|
+
function logStep(step, message) {
|
|
8
|
+
console.log(chalk.cyan(`[${step}/${TOTAL_STEPS}]`) + ' ' + message);
|
|
9
|
+
}
|
|
10
|
+
function findAvailableOutputDir(baseDir) {
|
|
11
|
+
const resolved = path.resolve(baseDir);
|
|
12
|
+
// If it doesn't exist, use it as-is
|
|
13
|
+
if (!existsSync(resolved)) {
|
|
14
|
+
return resolved;
|
|
15
|
+
}
|
|
16
|
+
// Find next available numbered directory
|
|
17
|
+
let counter = 1;
|
|
18
|
+
while (existsSync(`${resolved}${counter}`)) {
|
|
19
|
+
counter++;
|
|
20
|
+
}
|
|
21
|
+
return `${resolved}${counter}`;
|
|
22
|
+
}
|
|
23
|
+
export async function renderCommand(file, options) {
|
|
24
|
+
console.log();
|
|
25
|
+
try {
|
|
26
|
+
// Step 1: Validate
|
|
27
|
+
logStep(1, 'Validating portfolio...');
|
|
28
|
+
const portfolio = validatePortfolio(file);
|
|
29
|
+
console.log(chalk.green(' ✓') + ' Valid');
|
|
30
|
+
// Step 2: Load theme
|
|
31
|
+
const theme = options.theme || portfolio.theme || 'srcl';
|
|
32
|
+
logStep(2, `Loading theme: ${theme}`);
|
|
33
|
+
console.log(chalk.green(' ✓') + ' Theme loaded');
|
|
34
|
+
// Step 3: Generate HTML
|
|
35
|
+
const outputDir = findAvailableOutputDir(options.output);
|
|
36
|
+
logStep(3, 'Generating HTML...');
|
|
37
|
+
const result = await buildStaticSite(portfolio, {
|
|
38
|
+
outputDir,
|
|
39
|
+
theme,
|
|
40
|
+
});
|
|
41
|
+
console.log(chalk.green(' ✓') + ` Generated ${result.files.length} files`);
|
|
42
|
+
// Step 4: Done
|
|
43
|
+
logStep(4, 'Copying assets...');
|
|
44
|
+
console.log(chalk.green(' ✓') + ' Assets copied');
|
|
45
|
+
// Success output
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(chalk.green('✓') + ' Portfolio rendered successfully!');
|
|
48
|
+
console.log();
|
|
49
|
+
console.log(' Output: ' + chalk.cyan(outputDir));
|
|
50
|
+
console.log(' Theme: ' + chalk.cyan(theme));
|
|
51
|
+
console.log();
|
|
52
|
+
console.log(chalk.bold('Files created:'));
|
|
53
|
+
result.files.forEach((f) => {
|
|
54
|
+
console.log(chalk.dim(' •') + ` ${f}`);
|
|
55
|
+
});
|
|
56
|
+
console.log();
|
|
57
|
+
console.log(chalk.bold('Next steps:'));
|
|
58
|
+
console.log(' • Open ' + chalk.cyan(`${outputDir}/index.html`) + ' in your browser');
|
|
59
|
+
console.log(' • Or run ' + chalk.cyan(`npx serve ${outputDir}`) + ' for a local server');
|
|
60
|
+
console.log(' • Deploy the ' + chalk.cyan('site/') + ' folder to Vercel, Netlify, or GitHub Pages');
|
|
61
|
+
console.log();
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.log();
|
|
65
|
+
if (err instanceof ValidationError) {
|
|
66
|
+
console.log(chalk.red('✗') + ' Validation failed\n');
|
|
67
|
+
for (const error of err.errors) {
|
|
68
|
+
console.log(` ${chalk.red('•')} ${chalk.yellow(error.path)} - ${error.message}`);
|
|
69
|
+
}
|
|
70
|
+
if (err.errors[0]?.hint) {
|
|
71
|
+
console.log();
|
|
72
|
+
console.log(chalk.dim(' Expected format:'));
|
|
73
|
+
err.errors[0].hint.split('\n').forEach((line) => {
|
|
74
|
+
console.log(chalk.dim(` ${line}`));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
console.log();
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
if (err instanceof FileError) {
|
|
81
|
+
console.log(chalk.red('✗') + ` ${err.message}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
if (err instanceof ParseError) {
|
|
85
|
+
console.log(chalk.red('✗') + ' YAML syntax error');
|
|
86
|
+
if (err.line !== undefined) {
|
|
87
|
+
console.log(` Line ${err.line + 1}: ${err.message}`);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.log(` ${err.message}`);
|
|
91
|
+
}
|
|
92
|
+
console.log();
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
// Build errors
|
|
96
|
+
if (err instanceof Error) {
|
|
97
|
+
console.log(chalk.red('✗') + ' Build failed');
|
|
98
|
+
console.log();
|
|
99
|
+
console.log(` ${err.message}`);
|
|
100
|
+
console.log();
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
const THEMES = [
|
|
3
|
+
{
|
|
4
|
+
id: 'srcl',
|
|
5
|
+
name: 'SRCL',
|
|
6
|
+
description: 'Terminal aesthetic with monospace fonts',
|
|
7
|
+
example: 'https://devfolio.page/portfolios/engineer-srcl/',
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
id: 'modern',
|
|
11
|
+
name: 'Modern',
|
|
12
|
+
description: 'Clean, contemporary design',
|
|
13
|
+
example: 'https://devfolio.page/portfolios/designer-modern/',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'dark-academia',
|
|
17
|
+
name: 'Dark Academia',
|
|
18
|
+
description: 'Scholarly, vintage aesthetic',
|
|
19
|
+
example: 'https://devfolio.page/portfolios/researcher-academia/',
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
export function themesCommand() {
|
|
23
|
+
console.log();
|
|
24
|
+
console.log(chalk.bold('Available Themes'));
|
|
25
|
+
console.log();
|
|
26
|
+
THEMES.forEach((theme) => {
|
|
27
|
+
console.log(` ${chalk.cyan(theme.id)}`);
|
|
28
|
+
console.log(` ${theme.description}`);
|
|
29
|
+
console.log(` ${chalk.dim('Example:')} ${chalk.dim.underline(theme.example)}`);
|
|
30
|
+
console.log();
|
|
31
|
+
});
|
|
32
|
+
console.log(chalk.dim('Usage:'));
|
|
33
|
+
console.log(` devfolio render ${chalk.cyan('--theme srcl')}`);
|
|
34
|
+
console.log(` devfolio render ${chalk.cyan('--theme modern')}`);
|
|
35
|
+
console.log(` devfolio render ${chalk.cyan('--theme dark-academia')}`);
|
|
36
|
+
console.log();
|
|
37
|
+
console.log(chalk.dim('Or set the theme in your portfolio.yaml:'));
|
|
38
|
+
console.log(chalk.dim(' theme: srcl'));
|
|
39
|
+
console.log();
|
|
40
|
+
}
|