create-foliokit 0.0.0-stage → 1.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/LICENSE +21 -0
- package/README.md +377 -2
- package/bin/index.js +6 -0
- package/package.json +50 -4
- package/src/cli.js +111 -0
- package/src/generator.js +133 -0
- package/src/prompts.js +44 -0
- package/src/utils/errors.js +7 -0
- package/src/utils/paths.js +12 -0
- package/src/utils/progress.js +5 -0
- package/src/utils/project-name.js +30 -0
- package/templates/default/.gitignore +20 -0
- package/templates/default/data.ts +227 -0
- package/templates/default/index.html +23 -0
- package/templates/default/package-lock.json +1280 -0
- package/templates/default/package.json +28 -0
- package/templates/default/public/avatar-placeholder.svg +10 -0
- package/templates/default/public/footer-heart.png +0 -0
- package/templates/default/public/love-always-wins.png +0 -0
- package/templates/default/public/project-placeholder.svg +10 -0
- package/templates/default/public/robots.txt +2 -0
- package/templates/default/public/stars-foreground-landscape.svg +202 -0
- package/templates/default/public/stars-foreground-portrait.svg +202 -0
- package/templates/default/public/stars-landscape.svg +322 -0
- package/templates/default/public/stars-portrait.svg +322 -0
- package/templates/default/scripts/generate-seo.mjs +34 -0
- package/templates/default/src/App.tsx +552 -0
- package/templates/default/src/BrandIcon.tsx +36 -0
- package/templates/default/src/ProjectMagazineGallery.css +90 -0
- package/templates/default/src/ProjectMagazineGallery.tsx +210 -0
- package/templates/default/src/main.tsx +10 -0
- package/templates/default/src/styles.css +536 -0
- package/templates/default/src/utils.ts +2 -0
- package/templates/default/src/vite-env.d.ts +1 -0
- package/templates/default/tests/portfolio.test.js +96 -0
- package/templates/default/tsconfig.app.json +21 -0
- package/templates/default/tsconfig.json +7 -0
- package/templates/default/tsconfig.node.json +16 -0
- package/templates/default/vite.config.ts +38 -0
package/src/generator.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { copyFile, lstat, mkdir, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { extname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
3
|
+
import { getDefaultTemplatePath } from './utils/paths.js'
|
|
4
|
+
import { validateProjectName } from './utils/project-name.js'
|
|
5
|
+
import { createProgress } from './utils/progress.js'
|
|
6
|
+
|
|
7
|
+
const excludedDirectories = new Set([
|
|
8
|
+
'.angular', '.cache', '.git', '.netlify', '.next', '.parcel-cache', '.svelte-kit',
|
|
9
|
+
'.turbo', '.vercel', '.vite', 'build', 'coverage', 'dist', 'node_modules', 'tmp', 'temp',
|
|
10
|
+
])
|
|
11
|
+
const excludedExtensions = new Set([
|
|
12
|
+
'.bak', '.log', '.orig', '.pem', '.p12', '.pfx', '.key', '.secret', '.token',
|
|
13
|
+
'.swp', '.swo', '.tsbuildinfo',
|
|
14
|
+
])
|
|
15
|
+
const excludedFiles = new Set([
|
|
16
|
+
'.ds_store', '.envrc', '.npmignore', '.npmrc', '.netrc', '.pypirc', 'credentials', 'credentials.json',
|
|
17
|
+
'id_ed25519', 'id_rsa', 'secrets.json', 'thumbs.db',
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
function isWithin(parent, child) {
|
|
21
|
+
const path = relative(parent, child)
|
|
22
|
+
return path === '' || (!path.startsWith(`..${sep}`) && path !== '..' && !isAbsolute(path))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function shouldExclude(name, isDirectory) {
|
|
26
|
+
const lower = name.toLowerCase()
|
|
27
|
+
if (isDirectory && excludedDirectories.has(lower)) return true
|
|
28
|
+
if (excludedFiles.has(lower) || excludedExtensions.has(extname(lower))) return true
|
|
29
|
+
if (lower === '.env' || (lower.startsWith('.env.') && lower !== '.env.example')) return true
|
|
30
|
+
if (/\.(?:tmp|temp|swp|swo|orig|bak|log)$/.test(lower)) return true
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveProjectDestination(projectName, workingDirectory = process.cwd()) {
|
|
35
|
+
const validation = validateProjectName(projectName)
|
|
36
|
+
if (validation !== true) throw new Error(`Invalid project name: ${validation}`)
|
|
37
|
+
const base = resolve(workingDirectory)
|
|
38
|
+
const destination = resolve(base, projectName)
|
|
39
|
+
if (!isWithin(base, destination) || destination === base) {
|
|
40
|
+
throw new Error('The project destination must be a new child of the working directory.')
|
|
41
|
+
}
|
|
42
|
+
return destination
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function copyTree(source, destination) {
|
|
46
|
+
await mkdir(destination, { recursive: true })
|
|
47
|
+
for (const entry of await readdir(source, { withFileTypes: true })) {
|
|
48
|
+
if (shouldExclude(entry.name, entry.isDirectory())) continue
|
|
49
|
+
const from = join(source, entry.name)
|
|
50
|
+
const to = join(destination, entry.name)
|
|
51
|
+
if (entry.isSymbolicLink()) throw new Error(`Template contains an unsupported symbolic link: ${from}`)
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
await copyTree(from, to)
|
|
54
|
+
} else if (entry.isFile()) {
|
|
55
|
+
await copyFile(from, to)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function updatePackageNames(destination, projectName) {
|
|
61
|
+
const packagePath = join(destination, 'package.json')
|
|
62
|
+
const packageData = JSON.parse(await readFile(packagePath, 'utf8'))
|
|
63
|
+
packageData.name = projectName
|
|
64
|
+
await writeFile(packagePath, `${JSON.stringify(packageData, null, 2)}\n`)
|
|
65
|
+
|
|
66
|
+
const lockPath = join(destination, 'package-lock.json')
|
|
67
|
+
try {
|
|
68
|
+
const lock = JSON.parse(await readFile(lockPath, 'utf8'))
|
|
69
|
+
lock.name = projectName
|
|
70
|
+
if (lock.packages?.['']) lock.packages[''].name = projectName
|
|
71
|
+
await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}\n`)
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error.code !== 'ENOENT') throw error
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getDefaultTemplateDirectory() {
|
|
78
|
+
return getDefaultTemplatePath()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function generatePortfolio(options, context = {}) {
|
|
82
|
+
const projectName = options?.projectName
|
|
83
|
+
const workingDirectory = resolve(context.workingDirectory ?? process.cwd())
|
|
84
|
+
const templatePath = resolve(context.templateDirectory ?? getDefaultTemplateDirectory())
|
|
85
|
+
|
|
86
|
+
const [workingStats, templateStats] = await Promise.all([lstat(workingDirectory), lstat(templatePath)])
|
|
87
|
+
if (!workingStats.isDirectory()) throw new Error(`Working directory is not a directory: ${workingDirectory}`)
|
|
88
|
+
if (!templateStats.isDirectory()) throw new Error(`Portfolio template directory was not found: ${templatePath}`)
|
|
89
|
+
const [realDestinationParent, realTemplate] = await Promise.all([realpath(workingDirectory), realpath(templatePath)])
|
|
90
|
+
const destination = resolveProjectDestination(projectName, realDestinationParent)
|
|
91
|
+
if (isWithin(realTemplate, destination) || isWithin(destination, realTemplate)) {
|
|
92
|
+
throw new Error('The project destination cannot overlap the bundled template directory.')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const creating = createProgress('Creating project...').start()
|
|
96
|
+
try {
|
|
97
|
+
await mkdir(destination)
|
|
98
|
+
} catch (error) {
|
|
99
|
+
creating.fail('Could not create project directory')
|
|
100
|
+
if (error.code === 'EEXIST' && options.overwrite === true) {
|
|
101
|
+
const existing = await lstat(destination)
|
|
102
|
+
if (!existing.isDirectory() || existing.isSymbolicLink()) {
|
|
103
|
+
throw new Error(`The destination exists and is not a regular directory: ${destination}`)
|
|
104
|
+
}
|
|
105
|
+
await rm(destination, { recursive: true })
|
|
106
|
+
await mkdir(destination)
|
|
107
|
+
} else if (error.code === 'EEXIST') {
|
|
108
|
+
const conflict = new Error(`The destination already exists: ${destination}`)
|
|
109
|
+
conflict.code = 'EEXIST'
|
|
110
|
+
conflict.projectPath = destination
|
|
111
|
+
throw conflict
|
|
112
|
+
} else {
|
|
113
|
+
throw error
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
creating.succeed('Project directory created')
|
|
117
|
+
|
|
118
|
+
const progress = createProgress('Copying portfolio template...').start()
|
|
119
|
+
let configuring
|
|
120
|
+
try {
|
|
121
|
+
await copyTree(templatePath, destination)
|
|
122
|
+
progress.succeed('Copying portfolio template')
|
|
123
|
+
configuring = createProgress('Configuring package.json...').start()
|
|
124
|
+
await updatePackageNames(destination, projectName)
|
|
125
|
+
configuring.succeed('Configuring package.json')
|
|
126
|
+
return destination
|
|
127
|
+
} catch (error) {
|
|
128
|
+
progress.fail('Could not create portfolio')
|
|
129
|
+
configuring?.fail('Could not configure project')
|
|
130
|
+
await rm(destination, { recursive: true, force: true })
|
|
131
|
+
throw error
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/prompts.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import inquirer from 'inquirer'
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_PROJECT_NAME,
|
|
4
|
+
normalizeProjectName,
|
|
5
|
+
validateProjectName,
|
|
6
|
+
} from './utils/project-name.js'
|
|
7
|
+
|
|
8
|
+
export function createProjectQuestions() {
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
type: 'input',
|
|
12
|
+
name: 'projectName',
|
|
13
|
+
message: 'What should your project be called?',
|
|
14
|
+
default: DEFAULT_PROJECT_NAME,
|
|
15
|
+
filter: normalizeProjectName,
|
|
16
|
+
validate: validateProjectName,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
type: 'confirm',
|
|
20
|
+
name: 'installDependencies',
|
|
21
|
+
message: 'Install dependencies?',
|
|
22
|
+
default: true,
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function promptForProjectOptions(prompt = (questions) => inquirer.prompt(questions)) {
|
|
28
|
+
try {
|
|
29
|
+
const answers = await prompt(createProjectQuestions())
|
|
30
|
+
if (!answers) return null
|
|
31
|
+
|
|
32
|
+
const projectName = normalizeProjectName(answers.projectName)
|
|
33
|
+
const validation = validateProjectName(projectName)
|
|
34
|
+
if (validation !== true) throw new Error(validation)
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
projectName,
|
|
38
|
+
installDependencies: Boolean(answers.installDependencies),
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error instanceof Error && error.name === 'ExitPromptError') return null
|
|
42
|
+
throw error
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { dirname, resolve } from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
|
|
4
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
|
5
|
+
|
|
6
|
+
export function getPackagePath(...segments) {
|
|
7
|
+
return resolve(packageRoot, ...segments)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getDefaultTemplatePath() {
|
|
11
|
+
return getPackagePath('templates', 'default')
|
|
12
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const DEFAULT_PROJECT_NAME = 'my-portfolio'
|
|
2
|
+
|
|
3
|
+
const projectNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
4
|
+
const reservedNames = new Set([
|
|
5
|
+
'node_modules',
|
|
6
|
+
'con',
|
|
7
|
+
'prn',
|
|
8
|
+
'aux',
|
|
9
|
+
'nul',
|
|
10
|
+
...Array.from({ length: 9 }, (_, index) => `com${index + 1}`),
|
|
11
|
+
...Array.from({ length: 9 }, (_, index) => `lpt${index + 1}`),
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
export function normalizeProjectName(value) {
|
|
15
|
+
if (typeof value !== 'string') return DEFAULT_PROJECT_NAME
|
|
16
|
+
return value.trim() || DEFAULT_PROJECT_NAME
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function validateProjectName(value) {
|
|
20
|
+
const name = typeof value === 'string' ? value.trim() : ''
|
|
21
|
+
|
|
22
|
+
if (!name) return 'Enter a project name.'
|
|
23
|
+
if (name.length > 214) return 'Project names must be 214 characters or fewer.'
|
|
24
|
+
if (!projectNamePattern.test(name)) {
|
|
25
|
+
return 'Use lowercase letters and numbers separated by single hyphens.'
|
|
26
|
+
}
|
|
27
|
+
if (reservedNames.has(name)) return 'That name is reserved by the operating system or npm.'
|
|
28
|
+
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* FolioKit Portfolio Configuration
|
|
3
|
+
*
|
|
4
|
+
* Customize your portfolio by editing the information in this file.
|
|
5
|
+
* Replace the sample content with your own details.
|
|
6
|
+
*
|
|
7
|
+
* You generally do not need to edit the React components or other source
|
|
8
|
+
* files to update your personal information, projects, skills, or links.
|
|
9
|
+
*
|
|
10
|
+
* Icon Reference Links:
|
|
11
|
+
* - Technical Skills: https://skillicons.dev
|
|
12
|
+
* - Social & Brand Icons: https://simpleicons.org
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface Skill {
|
|
16
|
+
name: string
|
|
17
|
+
icon: string
|
|
18
|
+
category?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Project {
|
|
22
|
+
name: string
|
|
23
|
+
category?: string
|
|
24
|
+
description: string
|
|
25
|
+
image?: string
|
|
26
|
+
imageAlt?: string
|
|
27
|
+
technologies: Skill[]
|
|
28
|
+
liveUrl?: string
|
|
29
|
+
githubUrl?: string
|
|
30
|
+
featured?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ContentItem {
|
|
34
|
+
title: string
|
|
35
|
+
organization?: string
|
|
36
|
+
date?: string
|
|
37
|
+
description?: string
|
|
38
|
+
image?: string
|
|
39
|
+
links?: { label: string; url: string }[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface Testimonial {
|
|
43
|
+
name: string
|
|
44
|
+
role: string
|
|
45
|
+
company?: string
|
|
46
|
+
message: string
|
|
47
|
+
image?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SocialIcon {
|
|
51
|
+
label: string
|
|
52
|
+
icon: string
|
|
53
|
+
url?: string
|
|
54
|
+
color?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface PortfolioConfig {
|
|
58
|
+
profile: {
|
|
59
|
+
name: string
|
|
60
|
+
headline: string
|
|
61
|
+
introduction: string
|
|
62
|
+
about: string
|
|
63
|
+
location?: string
|
|
64
|
+
timeZone?: string
|
|
65
|
+
timeZoneLabel?: string
|
|
66
|
+
profileImage?: string
|
|
67
|
+
resumeUrl?: string
|
|
68
|
+
quote?: string
|
|
69
|
+
}
|
|
70
|
+
seo: {
|
|
71
|
+
title: string
|
|
72
|
+
description: string
|
|
73
|
+
siteUrl?: string
|
|
74
|
+
image?: string
|
|
75
|
+
}
|
|
76
|
+
socialIcons: SocialIcon[]
|
|
77
|
+
skills: Skill[]
|
|
78
|
+
projects: Project[]
|
|
79
|
+
experience: ContentItem[]
|
|
80
|
+
education: ContentItem[]
|
|
81
|
+
testimonials: Testimonial[]
|
|
82
|
+
contact: {
|
|
83
|
+
email?: string
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const portfolioConfig: PortfolioConfig = {
|
|
88
|
+
// Personal Information
|
|
89
|
+
profile: {
|
|
90
|
+
name: 'Your Name',
|
|
91
|
+
headline: 'Full Stack Developer',
|
|
92
|
+
introduction: "Hey, there! I'm a",
|
|
93
|
+
about: `Hey, there! I'm a developer who loves building products that people actually use. Write a couple of sentences here about who you are, what you do, and what you're currently working on.
|
|
94
|
+
|
|
95
|
+
Use this space to talk about your experience, the kinds of problems you enjoy solving, and the technologies you reach for most often.
|
|
96
|
+
|
|
97
|
+
Wrap up with what you're excited about right now — side projects, open source, or whatever you're learning next`,
|
|
98
|
+
location: 'Delhi, India',
|
|
99
|
+
timeZone: 'Asia/Kolkata', // IANA time zone for the live clock
|
|
100
|
+
timeZoneLabel: 'IST',
|
|
101
|
+
profileImage: '/avatar-placeholder.svg', // Path relative to public/ directory
|
|
102
|
+
quote: 'Build with curiosity. Share what you learn.',
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
// SEO & Metadata
|
|
106
|
+
seo: {
|
|
107
|
+
title: '{{name}} — {{headline}}',
|
|
108
|
+
description: '{{name}} is a {{headline}} building useful, thoughtful web experiences.',
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
// Social Links
|
|
112
|
+
// Add, edit, remove, or reorder your social media profiles.
|
|
113
|
+
// Find brand icons at: https://simpleicons.org (built-in icons: 'mail', 'file-text')
|
|
114
|
+
socialIcons: [
|
|
115
|
+
{ label: 'GitHub', icon: 'github', url: 'https://github.com/your-username', color: '#f2f0e9' },
|
|
116
|
+
{ label: 'LinkedIn', icon: 'linkedin', url: 'https://linkedin.com/in/your-username', color: '#0a66c2' },
|
|
117
|
+
{ label: 'Instagram', icon: 'instagram', url: 'https://instagram.com/your-username', color: '#e4405f' },
|
|
118
|
+
{ label: 'X', icon: 'x', url: 'https://x.com/your-username', color: '#f2f0e9' },
|
|
119
|
+
],
|
|
120
|
+
|
|
121
|
+
// Skills
|
|
122
|
+
// Add, edit, remove, or reorder skills.
|
|
123
|
+
// Find skill icon identifiers at: https://skillicons.dev
|
|
124
|
+
skills: [
|
|
125
|
+
{ name: 'HTML', icon: 'html', category: 'Frontend' },
|
|
126
|
+
{ name: 'CSS', icon: 'css', category: 'Frontend' },
|
|
127
|
+
{ name: 'JavaScript', icon: 'js', category: 'Frontend' },
|
|
128
|
+
{ name: 'React', icon: 'react', category: 'Frontend' },
|
|
129
|
+
{ name: 'TypeScript', icon: 'ts', category: 'Frontend' },
|
|
130
|
+
{ name: 'Tailwind CSS', icon: 'tailwind', category: 'Frontend' },
|
|
131
|
+
{ name: 'Vite', icon: 'vite', category: 'Tools' },
|
|
132
|
+
{ name: 'Redux', icon: 'redux', category: 'Frontend' },
|
|
133
|
+
{ name: 'Markdown', icon: 'md', category: 'Tools' },
|
|
134
|
+
{ name: 'Node.js', icon: 'nodejs', category: 'Backend' },
|
|
135
|
+
{ name: 'Express.js', icon: 'express', category: 'Backend' },
|
|
136
|
+
{ name: 'MongoDB', icon: 'mongodb', category: 'Backend' },
|
|
137
|
+
{ name: 'MySQL', icon: 'mysql', category: 'Backend' },
|
|
138
|
+
{ name: 'PostgreSQL', icon: 'postgres', category: 'Backend' },
|
|
139
|
+
{ name: 'C', icon: 'c', category: 'Languages' },
|
|
140
|
+
{ name: 'Python', icon: 'python', category: 'Languages' },
|
|
141
|
+
{ name: 'Java', icon: 'java', category: 'Languages' },
|
|
142
|
+
{ name: 'Postman', icon: 'postman', category: 'Tools' },
|
|
143
|
+
{ name: 'VS Code', icon: 'vscode', category: 'Tools' },
|
|
144
|
+
{ name: 'Git', icon: 'git', category: 'Tools' },
|
|
145
|
+
{ name: 'GitHub', icon: 'github', category: 'Tools' },
|
|
146
|
+
{ name: 'npm', icon: 'npm', category: 'Tools' },
|
|
147
|
+
{ name: 'GitHub Actions', icon: 'githubactions', category: 'Tools' },
|
|
148
|
+
],
|
|
149
|
+
|
|
150
|
+
// Projects
|
|
151
|
+
// Add, edit, remove, or reorder projects. Set featured: true to highlight in the 3D gallery.
|
|
152
|
+
// Technology icon identifiers also come from: https://skillicons.dev
|
|
153
|
+
projects: [
|
|
154
|
+
{
|
|
155
|
+
name: 'Project One',
|
|
156
|
+
category: 'PROJECT CATEGORY',
|
|
157
|
+
description: 'Add a short description of this project, the problem it solves, and what you built.',
|
|
158
|
+
image: '/project-placeholder.svg',
|
|
159
|
+
imageAlt: 'Project image placeholder, 1200 by 675 pixels',
|
|
160
|
+
technologies: [{ name: 'React', icon: 'react' }, { name: 'TypeScript', icon: 'ts' }],
|
|
161
|
+
featured: true, // Highlights project in the 3D magazine gallery
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'Project Two',
|
|
165
|
+
category: 'PROJECT CATEGORY',
|
|
166
|
+
description: 'Describe another project here. Replace the title, image, and technology stack in data.ts.',
|
|
167
|
+
image: '/project-placeholder.svg',
|
|
168
|
+
imageAlt: 'Project image placeholder, 1200 by 675 pixels',
|
|
169
|
+
technologies: [{ name: 'Node.js', icon: 'nodejs' }, { name: 'Express.js', icon: 'express' }, { name: 'MongoDB', icon: 'mongodb' }],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'Project Three',
|
|
173
|
+
category: 'PROJECT CATEGORY',
|
|
174
|
+
description: 'Use this space to explain what you made and the tools or technologies you used.',
|
|
175
|
+
image: '/project-placeholder.svg',
|
|
176
|
+
imageAlt: 'Project image placeholder, 1200 by 675 pixels',
|
|
177
|
+
technologies: [{ name: 'Python', icon: 'python' }, { name: 'C', icon: 'c' }, { name: 'Java', icon: 'java' }],
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
|
|
181
|
+
// Work Experience
|
|
182
|
+
// Add, edit, remove, or reorder work history. Leave empty ([]) to hide this section.
|
|
183
|
+
experience: [
|
|
184
|
+
{
|
|
185
|
+
title: 'Your Role',
|
|
186
|
+
organization: 'Company Name',
|
|
187
|
+
date: 'Month YYYY – Present',
|
|
188
|
+
description: 'Replace this sample with your role, company, and dates.',
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
|
|
192
|
+
// Education
|
|
193
|
+
// Add, edit, remove, or reorder education. Leave empty ([]) to hide this section.
|
|
194
|
+
education: [
|
|
195
|
+
{
|
|
196
|
+
title: 'Degree or Program',
|
|
197
|
+
organization: 'Your University',
|
|
198
|
+
date: 'YYYY – YYYY',
|
|
199
|
+
description: 'Add your major, score, or another education detail here.',
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
|
|
203
|
+
// Testimonials
|
|
204
|
+
// Add, edit, remove, or reorder testimonials. Leave empty ([]) to hide this section.
|
|
205
|
+
testimonials: [
|
|
206
|
+
{
|
|
207
|
+
name: 'Person One',
|
|
208
|
+
role: 'Role or designation',
|
|
209
|
+
message: 'Replace this placeholder with a genuine testimonial before publishing.',
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'Person Two',
|
|
213
|
+
role: 'Role or designation',
|
|
214
|
+
message: 'Replace this placeholder with a genuine testimonial before publishing.',
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'Person Three',
|
|
218
|
+
role: 'Role or designation',
|
|
219
|
+
message: 'Replace this placeholder with a genuine testimonial before publishing.',
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
|
|
223
|
+
// Contact
|
|
224
|
+
contact: {
|
|
225
|
+
email: 'hello@example.com',
|
|
226
|
+
},
|
|
227
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="theme-color" content="#000000" />
|
|
7
|
+
<meta name="description" content="A developer portfolio." />
|
|
8
|
+
<meta property="og:type" content="website" />
|
|
9
|
+
<meta property="og:title" content="Developer Portfolio" />
|
|
10
|
+
<meta property="og:description" content="A developer portfolio." />
|
|
11
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
12
|
+
<meta name="twitter:title" content="Developer Portfolio" />
|
|
13
|
+
<meta name="twitter:description" content="A developer portfolio." />
|
|
14
|
+
<title>Developer Portfolio</title>
|
|
15
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
16
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
17
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<div id="root"></div>
|
|
21
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|