Upload
Some checks failed
Main Branch / Run Prettier check (push) Has been cancelled
Main Branch / Run TypeScript check (push) Has been cancelled
Main Branch / Run Astro check (push) Has been cancelled
Main Branch / Run Percy check (push) Has been cancelled
Main Branch / Create release (push) Has been cancelled
Main Branch / Deploy to Netlify (push) Has been cancelled
Main Branch / Run Lighthouse check (push) Has been cancelled
13
.editorconfig
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
max_line_length = 120
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
1
.gitbook.yaml
Normal file
|
|
@ -0,0 +1 @@
|
|||
root: ./docs/
|
||||
43
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'to refine'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Version:**
|
||||
What version of the project are you using?
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'to refine'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
23
.github/renovate.json
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:js-app",
|
||||
":rebaseStalePrs",
|
||||
":automergePr",
|
||||
":automergeRequireAllStatusChecks",
|
||||
":separateMultipleMajorReleases",
|
||||
":semanticCommits",
|
||||
"schedule:weekly"
|
||||
],
|
||||
"labels": ["dependencies"],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackagePatterns": ["*"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"groupName": "all non-major dependencies",
|
||||
"groupSlug": "all-minor-patch",
|
||||
"automerge": true,
|
||||
"platformAutomerge": true
|
||||
}
|
||||
]
|
||||
}
|
||||
19
.github/scripts/update-changelog.js
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
const fs = require('fs');
|
||||
|
||||
module.exports = (version, prNumber) => {
|
||||
const changelog = fs.readFileSync('CHANGELOG.md', 'utf8');
|
||||
const changelogLines = changelog.split('\n');
|
||||
const lastChangeIndex = changelogLines.findIndex((line) => line.startsWith('## ['));
|
||||
|
||||
const textToAppend = `
|
||||
## [${version}] - ${new Date().toISOString().split('T')[0]}
|
||||
|
||||
### Dependencies
|
||||
|
||||
- chore(deps): update dependencies ([details](https://github.com/KonradSzwarc/devscard/pull/${prNumber}))
|
||||
`.trim();
|
||||
|
||||
changelogLines.splice(lastChangeIndex, 0, textToAppend + '\n');
|
||||
|
||||
fs.writeFileSync('CHANGELOG.md', changelogLines.join('\n'));
|
||||
};
|
||||
16
.github/workflows/create-issue-branch.yml
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
name: Create Issue Branch
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [assigned]
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
create_issue_branch_job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create Issue Branch
|
||||
uses: robvanderleek/create-issue-branch@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
40
.github/workflows/dependency-update-changelog.yml
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
name: Modify changelog for dependency updates
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.event.label.name == 'dependencies' && github.repository == 'KonradSzwarc/devscard'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
- name: Get project information
|
||||
id: projectinfo
|
||||
uses: gregoranders/nodejs-project-info@v0.0.20
|
||||
- name: Get next possible versions
|
||||
id: semvers
|
||||
uses: WyriHaximus/github-action-next-semvers@v1
|
||||
with:
|
||||
version: ${{ steps.projectinfo.outputs.version }}
|
||||
- name: Update package.json version
|
||||
uses: reedyuk/npm-version@1.2.1
|
||||
with:
|
||||
version: ${{ steps.semvers.outputs.patch }}
|
||||
- name: Update CHANGELOG.md
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const script = require('.github/scripts/update-changelog.js');
|
||||
const version = '${{ steps.semvers.outputs.patch }}';
|
||||
const prNumber = '${{ github.event.pull_request.number }}';
|
||||
|
||||
script(version, prNumber);
|
||||
- name: Commit changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
136
.github/workflows/main-branch.yml
vendored
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
name: Main Branch
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
prettier:
|
||||
name: Run Prettier check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run Prettier
|
||||
run: npm run prettier:check
|
||||
|
||||
typescript:
|
||||
name: Run TypeScript check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run TypeScript types check
|
||||
run: npm run ts:check
|
||||
|
||||
astro:
|
||||
name: Run Astro check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run Astro check
|
||||
run: npm run astro:check
|
||||
|
||||
deploy:
|
||||
name: Deploy to Netlify
|
||||
needs: [prettier, typescript, astro]
|
||||
if: github.repository == 'KonradSzwarc/devscard'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
deploy-url: ${{ steps.netlify.outputs.deploy-url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run Astro build command
|
||||
run: npm run build
|
||||
- name: Deploy to Netlify
|
||||
id: netlify
|
||||
uses: nwtgck/actions-netlify@v2
|
||||
with:
|
||||
publish-dir: dist
|
||||
production-branch: main
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
production-deploy: true
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
timeout-minutes: 1
|
||||
|
||||
percy:
|
||||
name: Run Percy check
|
||||
if: github.repository == 'KonradSzwarc/devscard'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run Astro build command
|
||||
run: npm run build
|
||||
env:
|
||||
PUBLIC_APP_ENV: snapshot
|
||||
- name: Percy check
|
||||
env:
|
||||
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
|
||||
run: npx percy snapshot dist/
|
||||
|
||||
lighthouse:
|
||||
name: Run Lighthouse check
|
||||
if: github.repository == 'KonradSzwarc/devscard'
|
||||
needs: deploy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Lighthouse check
|
||||
uses: foo-software/lighthouse-check-action@master
|
||||
with:
|
||||
urls: ${{ needs.deploy.outputs.deploy-url }}
|
||||
device: all
|
||||
|
||||
release:
|
||||
name: Create release
|
||||
if: github.repository == 'KonradSzwarc/devscard'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Get project information
|
||||
id: projectinfo
|
||||
uses: gregoranders/nodejs-project-info@v0.0.20
|
||||
- name: Get changelog entries
|
||||
id: changelog_reader
|
||||
uses: mindsers/changelog-reader-action@v2
|
||||
with:
|
||||
version: ${{ steps.projectinfo.outputs.version }}
|
||||
- name: Create a new tag and release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag: ${{ steps.changelog_reader.outputs.version }}
|
||||
name: Release ${{ steps.changelog_reader.outputs.version }}
|
||||
body: ${{ steps.changelog_reader.outputs.changes }}
|
||||
allowUpdates: true
|
||||
188
.github/workflows/pull-request.yml
vendored
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
name: Pull Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, edited, synchronize]
|
||||
|
||||
jobs:
|
||||
lint-title:
|
||||
name: Validate PR title
|
||||
if: github.repository == 'KonradSzwarc/devscard'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
requireScope: false
|
||||
|
||||
version:
|
||||
name: Check package.json version
|
||||
if: github.repository == 'KonradSzwarc/devscard'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout PR branch
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Get project information
|
||||
id: projectinfo-current
|
||||
uses: gregoranders/nodejs-project-info@v0.0.20
|
||||
- name: Checkout main branch
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
- name: Get project information
|
||||
id: projectinfo-main
|
||||
uses: gregoranders/nodejs-project-info@v0.0.20
|
||||
- name: Get next possible versions
|
||||
id: semvers
|
||||
uses: WyriHaximus/github-action-next-semvers@v1
|
||||
with:
|
||||
version: ${{ steps.projectinfo-main.outputs.version }}
|
||||
- name: Assert correct version bump
|
||||
uses: nick-fields/assert-action@v1
|
||||
with:
|
||||
expected: ${{ steps.projectinfo-current.outputs.version }}
|
||||
actual: 'Possible version bumps: ${{ steps.semvers.outputs.patch }}, ${{ steps.semvers.outputs.minor }}, ${{ steps.semvers.outputs.major }}'
|
||||
comparison: contains
|
||||
|
||||
changelog:
|
||||
name: Check changelog
|
||||
if: github.repository == 'KonradSzwarc/devscard'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
- name: Get project information
|
||||
id: projectinfo
|
||||
uses: gregoranders/nodejs-project-info@v0.0.20
|
||||
- name: Enforce changelog update
|
||||
uses: dangoslen/changelog-enforcer@v3
|
||||
with:
|
||||
expectedLatestVersion: ${{ steps.projectinfo.outputs.version }}
|
||||
- name: Get changelog entries
|
||||
id: changelog_reader
|
||||
uses: mindsers/changelog-reader-action@v2
|
||||
with:
|
||||
version: ${{ steps.projectinfo.outputs.version }}
|
||||
- name: Assert correct changelog version
|
||||
uses: nick-fields/assert-action@v1
|
||||
with:
|
||||
expected: ${{ steps.projectinfo.outputs.version }}
|
||||
actual: ${{ steps.changelog_reader.outputs.version }}
|
||||
|
||||
prettier:
|
||||
name: Run Prettier check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run Prettier
|
||||
run: npm run prettier:check
|
||||
|
||||
typescript:
|
||||
name: Run TypeScript check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run TypeScript types check
|
||||
run: npm run ts:check
|
||||
|
||||
astro:
|
||||
name: Run Astro check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run Astro check
|
||||
run: npm run astro:check
|
||||
|
||||
percy:
|
||||
name: Run Percy check
|
||||
if: github.repository == 'KonradSzwarc/devscard'
|
||||
needs: [prettier, typescript, astro, lint-title, version, changelog]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run Astro build command
|
||||
run: npm run build
|
||||
env:
|
||||
PUBLIC_APP_ENV: snapshot
|
||||
- name: Percy check
|
||||
env:
|
||||
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
|
||||
run: npx percy snapshot dist/
|
||||
|
||||
preview:
|
||||
name: Create deploy preview
|
||||
needs: [prettier, typescript, astro, lint-title, version, changelog]
|
||||
if: github.repository == 'KonradSzwarc/devscard'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
deploy-url: ${{ steps.netlify.outputs.deploy-url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run Astro build command
|
||||
run: npm run build
|
||||
- name: Deploy to Netlify
|
||||
id: netlify
|
||||
uses: nwtgck/actions-netlify@v2
|
||||
with:
|
||||
publish-dir: dist
|
||||
production-branch: main
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
alias: deploy-preview-${{ github.event.number }}
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
timeout-minutes: 1
|
||||
|
||||
lighthouse:
|
||||
name: Run Lighthouse check
|
||||
if: github.repository == 'KonradSzwarc/devscard'
|
||||
needs: preview
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Lighthouse check
|
||||
uses: foo-software/lighthouse-check-action@master
|
||||
with:
|
||||
urls: ${{ needs.preview.outputs.deploy-url }}
|
||||
gitAuthor: ${{ github.actor }}
|
||||
gitBranch: ${{ github.ref }}
|
||||
gitHubAccessToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
device: all
|
||||
prCommentEnabled: true
|
||||
prCommentSaveOld: false
|
||||
13
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Dependencies
|
||||
node_modules
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Build output
|
||||
dist
|
||||
stats.html
|
||||
|
||||
# Favicon generation
|
||||
public/favicons
|
||||
*.generated.astro
|
||||
2
.npmrc
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
save-exact = true
|
||||
engine-strict = true
|
||||
18
.percy.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"version": 2,
|
||||
"snapshot": {
|
||||
"widths": [375, 1280],
|
||||
"minHeight": 1024,
|
||||
"enable-javascript": true
|
||||
},
|
||||
"static": {
|
||||
"include": ["/index.html"]
|
||||
},
|
||||
"discovery": {
|
||||
"allowedHostnames": ["api.iconify.design"],
|
||||
"networkIdleTimeout": 750
|
||||
},
|
||||
"upload": {
|
||||
"files": "**/*.{png,jpg,jpeg,svg,webp}"
|
||||
}
|
||||
}
|
||||
10
.prettierrc.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Specify only the rules that cannot be defined within the .editorconfig file as Prettier inherits config from there.
|
||||
# See: https://prettier.io/docs/en/configuration.html#editorconfig
|
||||
|
||||
singleQuote: true
|
||||
endOfLine: 'auto'
|
||||
|
||||
pluginSearchDirs: false
|
||||
plugins:
|
||||
- 'prettier-plugin-astro'
|
||||
- 'prettier-plugin-tailwindcss'
|
||||
11
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"EditorConfig.EditorConfig",
|
||||
"astro-build.astro-vscode",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"esbenp.prettier-vscode",
|
||||
"foxundermoon.shell-format",
|
||||
"mgmcdermott.vscode-language-babel",
|
||||
"redhat.vscode-yaml"
|
||||
]
|
||||
}
|
||||
14
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.rulers": [120],
|
||||
"emmet.includeLanguages": { "javascript": "javascriptreact", "astro": "javascriptreact" },
|
||||
"files.eol": "\n",
|
||||
"prettier.documentSelectors": ["**/*.astro"],
|
||||
"tailwindCSS.classAttributes": ["class", "className", "class:list"],
|
||||
"tailwindCSS.experimental.classRegex": [["/\\* tw \\*/ ([^;]*);", "'([^']*)'"]],
|
||||
"tailwindCSS.includeLanguages": { "javascript": "javascriptreact", "astro": "javascriptreact" },
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"[ignore]": { "editor.defaultFormatter": "foxundermoon.shell-format" }
|
||||
}
|
||||
109
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.3.0] - 2023-08-25
|
||||
|
||||
### Breaking
|
||||
|
||||
- Bump astro-compress version to prevent failing install
|
||||
|
||||
## [0.2.1] - 2023-07-10
|
||||
|
||||
### Dependencies
|
||||
|
||||
- chore(deps): update dependencies ([details](https://github.com/KonradSzwarc/devscard/pull/226))
|
||||
|
||||
## [0.2.0] - 2023-07-02
|
||||
|
||||
### Breaking
|
||||
|
||||
- Require Node.js 18.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- chore(deps): update dependencies ([details](https://github.com/KonradSzwarc/devscard/pull/225))
|
||||
|
||||
## [0.1.6] - 2023-05-08
|
||||
|
||||
### Dependencies
|
||||
|
||||
- chore(deps): update dependencies ([details](https://github.com/KonradSzwarc/devscard/pull/220))
|
||||
|
||||
## [0.1.5] - 2023-05-01
|
||||
|
||||
### Dependencies
|
||||
|
||||
- chore(deps): update dependencies ([details](https://github.com/KonradSzwarc/devscard/pull/219))
|
||||
|
||||
## [0.1.4] - 2023-04-05
|
||||
|
||||
### Workflow
|
||||
|
||||
- ci: use pull request URL instead of its API endpoint when generating changelog for dependency updates.
|
||||
|
||||
## [0.1.3] - 2023-03-27
|
||||
|
||||
### Dependencies
|
||||
|
||||
- chore(deps): update dependencies ([details](https://github.com/KonradSzwarc/devscard/pull/214))
|
||||
|
||||
## [0.1.2] - 2023-03-24
|
||||
|
||||
### Dependencies
|
||||
|
||||
- chore(deps): update dependencies ([details](https://github.com/KonradSzwarc/devscard/pull/212))
|
||||
|
||||
## [0.1.1] - 2023-03-20
|
||||
|
||||
### Dependencies
|
||||
|
||||
- chore(deps): update dependencies ([details](https://github.com/KonradSzwarc/devscard/pull/213))
|
||||
|
||||
## [0.1.0] - 2023-03-13
|
||||
|
||||
### Features
|
||||
|
||||
- feat: improved target attribute in labelled values ([details](https://github.com/KonradSzwarc/devscard/pull/210))
|
||||
|
||||
## [0.0.6] - 2023-03-06
|
||||
|
||||
### Dependencies
|
||||
|
||||
- chore(deps): update dependencies ([details](https://github.com/KonradSzwarc/devscard/pull/209))
|
||||
|
||||
## [0.0.5] - 2023-02-27
|
||||
|
||||
### Dependencies
|
||||
|
||||
- chore(deps): update dependencies ([details](https://github.com/KonradSzwarc/devscard/pull/207))
|
||||
|
||||
## [0.0.4] - 2023-02-22
|
||||
|
||||
### Workflow
|
||||
|
||||
- ci: automatically update project version and changelog for Renovate's PRs
|
||||
|
||||
## [0.0.3] - 2023-02-21
|
||||
|
||||
### Dependencies
|
||||
|
||||
- chore(deps): update dependencies ([details](https://github.com/KonradSzwarc/devscard/pull/202))
|
||||
|
||||
## [0.0.2] - 2023-02-16
|
||||
|
||||
### Dependencies
|
||||
|
||||
- chore(deps): update dependencies ([details](https://github.com/KonradSzwarc/devscard/pull/201))
|
||||
|
||||
## [0.0.1] - 2023-02-08
|
||||
|
||||
### Workflow
|
||||
|
||||
- ci: add `package.json` version and changelog check to PR workflow.
|
||||
- ci: setup release workflow on the `main` branch.
|
||||
|
||||
### Docs
|
||||
|
||||
- docs: create changelog file.
|
||||
- docs: add [contributing page](https://devscard.gitbook.io/docs/project-development/contributing).
|
||||
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Konrad Szwarc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
34
README.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# DevsCard
|
||||
|
||||
A fully customizable template to create your online (and paper) resume without writing a single line of code.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This project will remain available but <ins>**won't receive updates**</ins>.
|
||||
>
|
||||
> If you are searching for a great framework to build your resume, **[check out Zenith](https://github.com/KonradSzwarc/zenith)**. It's a new iteration of this project utilizing recent Astro version capabilities that will provide you with much more features and customizability.
|
||||
|
||||
## Features
|
||||
|
||||
- **✍️ Intellisense** — provide your data in TypeScript files, getting autocompletion and description of each property right in your IDE.
|
||||
- **📱 Responsiveness** — the resume is created and automatically tested to look good both on mobile and desktop devices.
|
||||
- **🌠 Assets optimization** — all images in your CV are minimized and resized automatically at build time.
|
||||
- **⚡️ Performance** — get 100 for all Lighthouse metrics, ensuring a great experience for visitors and a high score for search engines.
|
||||
- **📄 PDF generation** — generate an accompanying PDF version of your CV with one command.
|
||||
- **🔶 Built-in icon sets** — choose from over 100 000 [Iconify](https://iconify.design/) icons to represent your skills.
|
||||
- **🌍 I18n** — customize your resume's locale, date formatting, and used translations.
|
||||
- **🔎 SEO friendly** — the entire website is designed with SEO in mind. All SEO-related config properties are required and well-described.
|
||||
- **🏭 Favicons generation** — invoke one command to generate all favicons and full app manifest for your website.
|
||||
- **🛠 Data helpers** — use built-in helpers to define your skills/socials once and reuse them across the configuration.
|
||||
- **🔀 Data transformers** — utilize type-safe data transformers to create multiple variants of your resume without duplicating your data.
|
||||
|
||||
## Documentation
|
||||
|
||||
To learn how to set up your resume, go to:
|
||||
|
||||
[https://devscard.gitbook.io/docs](https://devscard.gitbook.io/docs/setup-guide)
|
||||
|
||||
## Example
|
||||
|
||||
To see an example CV, visit the link below:
|
||||
|
||||
[https://devscard.netlify.app](https://devscard.netlify.app/)
|
||||
18
astro.config.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import image from '@astrojs/image';
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
import compress from 'astro-compress';
|
||||
import { defineConfig } from 'astro/config';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [tailwind(), image(), compress()],
|
||||
vite: {
|
||||
plugins: [visualizer()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'date-fns/locale': 'date-fns/locale/index.js',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
BIN
docs/.gitbook/assets/clone-repository.png
Normal file
|
After Width: | Height: | Size: 944 KiB |
BIN
docs/.gitbook/assets/device-dimensions.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
docs/.gitbook/assets/device-mode.png
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
docs/.gitbook/assets/fork-repository.png
Normal file
|
After Width: | Height: | Size: 653 KiB |
BIN
docs/.gitbook/assets/netlify-build-settings.png
Normal file
|
After Width: | Height: | Size: 384 KiB |
BIN
docs/.gitbook/assets/netlify-connect-github.png
Normal file
|
After Width: | Height: | Size: 391 KiB |
BIN
docs/.gitbook/assets/netlify-deployed-site.png
Normal file
|
After Width: | Height: | Size: 746 KiB |
BIN
docs/.gitbook/assets/netlify-import-project.png
Normal file
|
After Width: | Height: | Size: 496 KiB |
BIN
docs/.gitbook/assets/netlify-pick-repository.png
Normal file
|
After Width: | Height: | Size: 596 KiB |
17
docs/README.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Introduction
|
||||
|
||||
DevsCard is a fully customizable template to create your online resume without writing a single line of code. After forking the repository, you only need to update the initial data with your own and deploy the project.
|
||||
|
||||
Apart from the website template, DevsCard offers you plenty of additional features that will make it easier to take care of your online and offline presence, including:
|
||||
|
||||
- **✍️ Intellisense** — provide your data in TypeScript files, getting autocompletion and description of each property right in your IDE.
|
||||
- **📱 Responsiveness** — the resume is created and automatically tested to look great on mobile and desktop devices.
|
||||
- **🌠 Assets optimization** — all images in your CV are minimized and resized automatically at build time.
|
||||
- **⚡️ Performance** — get 100 for all Lighthouse metrics, ensuring a great experience for visitors and a high score for search engines.
|
||||
- **📄 PDF generation** — generate an accompanying PDF version of your CV with one command.
|
||||
- **🔶 Built-in icon sets** — choose from over 100 000 [Iconify](https://iconify.design/) icons to represent your skills.
|
||||
- **🌍 I18n** — customize your resume's locale, date formatting, and used translations.
|
||||
- **🔎 SEO friendly** — the entire website is designed with SEO in mind. All SEO-related config properties are required and well-described.
|
||||
- **🏭 Favicons generation** — invoke one command to generate all favicons and full app manifest for your website.
|
||||
- **🛠 Data helpers** — use built-in helpers to define your skills/socials once and reuse them across the configuration.
|
||||
- **🔀 Data transformers** — utilize type-safe data transformers to create multiple variants of your resume without duplicating your data.
|
||||
15
docs/SUMMARY.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Table of contents
|
||||
|
||||
- [Introduction](README.md)
|
||||
- [Setup guide](setup-guide.md)
|
||||
- [PDF generation](pdf-generation.md)
|
||||
- [Data transformation](data-transformation.md)
|
||||
|
||||
## Project development
|
||||
|
||||
- [Contributing](contributing.md)
|
||||
|
||||
## External links
|
||||
|
||||
- [Example resume](https://devscard.netlify.app)
|
||||
- [GitHub repository](https://github.com/KonradSzwarc/devscard)
|
||||
68
docs/contributing.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# Contributing
|
||||
|
||||
## PR workflow
|
||||
|
||||
### Maintainers
|
||||
|
||||
1. Create a pull request with a title matching the conventional commits convention.
|
||||
2. Wait for all required checks to pass.
|
||||
3. If you got a failing Percy check, it's okay as long as your PR was supposed to affect UI. Go to Percy's dashboard and review your visual changes.
|
||||
4. Assign PR for one (or a few) of the maintainers.
|
||||
5. Your PR is ready to merge when you have at least one approval and no unresolved threads.
|
||||
6. Update the date in the changelog to be the current one.
|
||||
7. Wait for checks to pass again and merge PR.
|
||||
|
||||
### Outside contributors
|
||||
|
||||
In progress...
|
||||
|
||||
## Versioning and changelog
|
||||
|
||||
As this project is used in a fork-based way, each merge to the `main` branch should contain the following:
|
||||
|
||||
- a version bump in `package.json`,
|
||||
- a new entry in the `CHANGELOG.md`.
|
||||
|
||||
For the `CHANGELOG.md`, each update should look like follows:
|
||||
|
||||
```md
|
||||
## [<version>] - <date>
|
||||
|
||||
**Related issue:** <issue-link>
|
||||
|
||||
### <affected>
|
||||
|
||||
<description>
|
||||
```
|
||||
|
||||
### Semantic versioning (\<version>)
|
||||
|
||||
Helps users to determine the changes made between their fork version and the recent version.
|
||||
|
||||
- Patch change (0.0.x) — bug fixes, refactors, docs, and dependency updates.
|
||||
- Minor change (0.x.0) — features.
|
||||
- Major change (x.0.0) — not used until we release a stable project version (1.0.0).
|
||||
|
||||
### Change date (\<date>)
|
||||
|
||||
Date when changes were merged written in YYYY-MM-DD format.
|
||||
|
||||
### Related issue (\<issue-link>)
|
||||
|
||||
If the changes are related to a particular issue, provide its URL.
|
||||
|
||||
### Affected components (\<affected>)
|
||||
|
||||
Determines what parts of the project were affected by changes.
|
||||
|
||||
- Docs — documentation updates.
|
||||
- Dependencies — dependency updates.
|
||||
- Workflow — changes in the development workflow.
|
||||
- Web — changes related to the web version of the resume.
|
||||
- Pdf — changes related to the pdf version of the resume.
|
||||
- Schema — some optional schema properties were added.
|
||||
- Schema (breaking) — some required schema properties were added, some properties were renamed/removed, data structure changed.
|
||||
|
||||
### Description
|
||||
|
||||
A short description of the changes you made. If your changes require some actions on project forks, remember to describe them.
|
||||
48
docs/data-transformation.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Data transformation
|
||||
|
||||
In some cases you may want to modify data between web and pdf versions of your resume or even create multiple variants of them. To address those needs while still providing single place for configuration we introduced a concept of data transformers.
|
||||
|
||||
When visiting `index.astro` (web resume source) or `pdf.astro` (pdf resume source) you can see that we use the `cv()` function to get data for a particular resume variant. By default `cv()` returns the entire configuration you specified within the `src/data` folder. However, you can modify this behavior by passing some data transformers into it.
|
||||
|
||||
For example:
|
||||
|
||||
```javascript
|
||||
import cv, { hideProject, hideSkillSet, renameSkillSet } from '@/data';
|
||||
|
||||
cv(
|
||||
// [skills] Skill set with name "I want to learn" won't be displayed
|
||||
hideSkillSet('I want to learn'),
|
||||
|
||||
// [skills] Skill set named "I speak" will be renamed to "Languages"
|
||||
renameSkillSet('I speak', 'Languages'),
|
||||
|
||||
// [portfolio] "Disco Ninjas" project won't be visible
|
||||
hideProject('Disco Ninjas')
|
||||
);
|
||||
```
|
||||
|
||||
## Data transformers
|
||||
|
||||
### General
|
||||
|
||||
`hideSection(sectionKey)` — hides section with a specified key.
|
||||
|
||||
### Skills
|
||||
|
||||
`hideSkillSet(skillSetTitle)` — hides skill set with a specified title.
|
||||
|
||||
`renameSkillSet(from, to)` — changes name of a specified skill set (`from`) to a different one (`to`).
|
||||
|
||||
`hideSkills(skillSetTitle, skillNames[])` — finds the skill set by its title and hides all its skills matched by their name.
|
||||
|
||||
### Portfolio
|
||||
|
||||
`hideProject(projectName)` — hides project with a specified name.
|
||||
|
||||
### Experience
|
||||
|
||||
`hideJob(role, company?)` — hides job where you had a specified role. If you had same role in multiple companies, you can provide a precise company as the second parameter.
|
||||
|
||||
### Education
|
||||
|
||||
`hideDiploma(title, institution?)` — hides education record by its title. If you have multiple diplomas with the same title, you can provide a an institution name as the second parameter to narrow the filter.
|
||||
42
docs/pdf-generation.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# PDF generation
|
||||
|
||||
You can use DevsCard to generate a PDF version of your resume.
|
||||
To do it, invoke the `npm run generate-pdf` command from the project's root and look to `public/cv.pdf` for the result.
|
||||
|
||||
## Customizing the data
|
||||
|
||||
PDF generation script takes data from the same files your web resume does. However, it applies a few modifications:
|
||||
|
||||
- For `main-section.data.ts` and projects in `portfolio-section.data.ts` you can define a separate property called `pdfDetails`. If specified, it will be used instead of the `details` to render label-value text paris.
|
||||
- Value of the `links` property is ignored. If you want some URLs to be present in PDF, add them as `pdfDetails`.
|
||||
- There are no testimonials and favorites sections.
|
||||
|
||||
You can also provide your own modifications using [data transformers](./data-transformation.md) in the `pdf.astro` file.
|
||||
|
||||
## Footer
|
||||
|
||||
Sometimes you need to put some text at the bottom of each PDF file (e.g. a data processing clause).
|
||||
|
||||
With DevsCard you can achieve it by providing the `pdf.footer` property in the `src/data/config.ts` file.
|
||||
|
||||
## Local testing
|
||||
|
||||
It might be tedious to run the generate command each time you want to see your changes.
|
||||
|
||||
Luckily, a few steps can give you a way to get a live preview of your changes.
|
||||
|
||||
1\. Invoke `npm run dev` to start local development server.
|
||||
|
||||
2\. Go to `http://localhost:3000/pdf`.
|
||||
|
||||
3\. Open developer tools and turn on the Device Mode by clicking the phone icon in the top left corner.
|
||||
|
||||
<figure><img src=".gitbook/assets/device-mode.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
4\. Set device dimensions to 794x1122. Optionally you can save it as the A4 dimensions for further use.
|
||||
|
||||
<figure><img src=".gitbook/assets/device-dimensions.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
5\. Edit any part of your data of `pdf.astro` to see changes live.
|
||||
|
||||
6\. When the result looks satisfying, invoke `npm run generate-pdf` to generate a new PDF file.
|
||||
70
docs/setup-guide.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Setup guide
|
||||
|
||||
## 1. Run the project locally
|
||||
|
||||
1\. Create a fork of the [project repository](https://github.com/KonradSzwarc/devscard).
|
||||
|
||||
<figure><img src=".gitbook/assets/fork-repository.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
2\. Go to the forked repository and clone it to your local machine.
|
||||
|
||||
<figure><img src=".gitbook/assets/clone-repository.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
3\. Open the cloned project in your IDE of choice and run `npm install`.
|
||||
|
||||
4\. Invoke `npm run dev` in the project's root directory and go to `http://localhost:3000`. You should see a resume website filled with initial data.
|
||||
|
||||
## 2. Provide your data
|
||||
|
||||
#### Basics
|
||||
|
||||
To fill the CV with your data, go to the `src/data` directory. There you should focus on three places:
|
||||
|
||||
- `config.ts` — use it to provide metadata of your website and set up its locales.
|
||||
- `sections` — contains files with data for each section of the resume.
|
||||
- `helpers` — helper functions you can utilize to reduce the amount of repetitiveness when setting up your socials and skills.
|
||||
- `links.ts` — functions that ensure you always use the same icon and name when providing links to external websites. We provide the most popular socials out-of-the-box, so there is a chance you won't edit anything in this file.
|
||||
- `skills.ts` — one place where you define your skills to reuse them in multiple sections. You can remove the skills used in the example template and replace them with your own.
|
||||
|
||||
#### **Tips**
|
||||
|
||||
- You can hover over each configuration property to get its description.
|
||||
- Some property descriptions start with `[WEB]` or `[PDF]`. It means those properties are used only in the web/pdf version of the resume.
|
||||
- Although you can provide URLs for images, we highly recommend putting all images in the `src/assets` directory and importing them using the `import` statement. This way, images will be auto-optimized, so you won't have to worry about their dimensions.
|
||||
- To know the aspect ratio of an image, hover over the `image` property.
|
||||
|
||||
## 3. Generate PDF (optional)
|
||||
|
||||
Within the main section, you will find an `action` property. It allows you to provide a pdf resume to download. If you don't have one, feel free to use our CV generator by invoking `npm run generate-pdf`. Generated resume will be placed in `public/cv.pdf` and use the same data as the web one. You can learn more about PDF generation [here](./pdf-generation.md).
|
||||
|
||||
## 4. Deploy to Netlify
|
||||
|
||||
{% hint style="info" %}
|
||||
As the resume is entirely static, you can deploy it to any hosting provider. In this guide, we use Netlify as it's free and easy to set up.
|
||||
{% endhint %}
|
||||
|
||||
1\. Create a commit for your CV updates and push it to GitHub.
|
||||
|
||||
2\. Create a [Netlify](https://www.netlify.com/) account.
|
||||
|
||||
3\. Go to the "Sites" tab and choose "Import from Git".
|
||||
|
||||
<figure><img src=".gitbook/assets/netlify-import-project.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
4\. Connect Netlify with your GitHub account.
|
||||
|
||||
<figure><img src=".gitbook/assets/netlify-connect-github.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
5\. Pick a repository with your forked project
|
||||
|
||||
<figure><img src=".gitbook/assets/netlify-pick-repository.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
6\. On the last step, go with the default settings suggested by Netlify and click "Deploy site".
|
||||
|
||||
<figure><img src=".gitbook/assets/netlify-build-settings.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
7\. After around one minute, your resume will be live 🎉
|
||||
|
||||
<figure><img src=".gitbook/assets/netlify-deployed-site.png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
From now on, each push to `main` branch will cause redeploy of the Netlify website. You may want to go to the "Site settings" tab to update your site name or even [set up your domain](https://youtu.be/bY7Tkh9Vz8I).
|
||||
16769
package-lock.json
generated
Normal file
71
package.json
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
"name": "devscard",
|
||||
"description": "Template for creating a comprehensive virtual CV for developers.",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
"npm": ">=9",
|
||||
"yarn": "please-use-npm",
|
||||
"pnpm": "please-use-npm"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "npm run generate-favicons",
|
||||
"dev": "astro dev",
|
||||
"prebuild": "npm run generate-favicons",
|
||||
"__prebuild": "move-file ./src/pages/pdf.astro ./src/pages/_pdf.astro && npm run generate-favicons",
|
||||
"build": "npm run generate-favicons && astro build",
|
||||
"postbuild": "move-file ./src/pages/_pdf.astro ./src/pages/pdf.astro",
|
||||
"preview": "astro preview",
|
||||
"generate-pdf": "ts-node scripts/generate-pdf.ts",
|
||||
"generate-favicons": "ts-node scripts/generate-favicons.ts",
|
||||
"prettier:check": "prettier --check . --ignore-path .gitignore",
|
||||
"prettier:write": "prettier --write . --ignore-path .gitignore",
|
||||
"astro:check": "astro check",
|
||||
"ts:check": "tsc --jsx preserve --skipLibCheck",
|
||||
"check": "concurrently npm:*:check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "1.4.4",
|
||||
"iconify-icon": "1.0.8",
|
||||
"nanoid": "4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/image": "0.17.2",
|
||||
"@astrojs/react": "2.2.1",
|
||||
"@astrojs/tailwind": "4.0.0",
|
||||
"@percy/cli": "1.26.2",
|
||||
"@types/marked": "5.0.0",
|
||||
"astro": "2.8.0",
|
||||
"astro-compress": "2.0.14",
|
||||
"concurrently": "8.2.0",
|
||||
"date-fns": "2.30.0",
|
||||
"favicons": "7.1.3",
|
||||
"iconify-icon-names": "1.1.0",
|
||||
"immer": "10.0.2",
|
||||
"locales-ts": "1.0.0",
|
||||
"marked": "5.1.1",
|
||||
"move-file-cli": "3.0.0",
|
||||
"photoswipe": "5.3.8",
|
||||
"postcss": "8.4.25",
|
||||
"prettier": "2.8.8",
|
||||
"prettier-plugin-astro": "0.10.0",
|
||||
"prettier-plugin-tailwindcss": "0.3.0",
|
||||
"puppeteer": "20.8.0",
|
||||
"puppeteer-report": "3.1.0",
|
||||
"rollup-plugin-visualizer": "5.9.2",
|
||||
"tailwindcss": "3.3.2",
|
||||
"ts-node": "10.9.1",
|
||||
"type-fest": "3.13.0",
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/KonradSzwarc/devscard.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/KonradSzwarc/devscard/issues"
|
||||
},
|
||||
"homepage": "https://github.com/KonradSzwarc/devscard#readme",
|
||||
"packageManager": "pnpm@9.7.0+sha512.dc09430156b427f5ecfc79888899e1c39d2d690f004be70e05230b72cb173d96839587545d09429b55ac3c429c801b4dc3c0e002f653830a420fa2dd4e3cf9cf"
|
||||
}
|
||||
6
postcss.config.cjs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
BIN
public/fonts/Inter-roman.var.woff2
Normal file
65
scripts/generate-favicons.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { favicons, config as faviconsConfig, FaviconFile, FaviconImage } from 'favicons';
|
||||
import config from '../src/data/config';
|
||||
import { mkdir, writeFile, rm } from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
const FAVICONS_DIR = './public/favicons';
|
||||
const ASTRO_FILE_PATH = './src/web/head/favicons.generated.astro';
|
||||
|
||||
const generateFavicons = () =>
|
||||
favicons(`.${config.meta.faviconPath}`, {
|
||||
...faviconsConfig.defaults,
|
||||
path: '/favicons',
|
||||
appName: config.meta.title,
|
||||
appDescription: config.meta.description,
|
||||
appShortName: config.meta.title,
|
||||
lang: config.i18n.locale.code,
|
||||
start_url: '.',
|
||||
icons: {
|
||||
android: ['android-chrome-192x192.png', 'android-chrome-512x512.png'],
|
||||
windows: false,
|
||||
yandex: false,
|
||||
appleStartup: false,
|
||||
appleIcon: ['apple-touch-icon.png'],
|
||||
favicons: ['favicon-16x16.png', 'favicon-32x32.png', 'favicon.ico'],
|
||||
},
|
||||
});
|
||||
|
||||
const clearFaviconsDir = async () => {
|
||||
if (existsSync(FAVICONS_DIR)) {
|
||||
await rm(FAVICONS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
await mkdir(FAVICONS_DIR);
|
||||
};
|
||||
|
||||
const saveFaviconAsset = async (file: FaviconFile | FaviconImage) => {
|
||||
await writeFile(`${FAVICONS_DIR}/${file.name}`, file.contents);
|
||||
|
||||
console.log(`${file.name} has been created successfully`);
|
||||
};
|
||||
|
||||
const generateAstroFile = async (html: string[]) => {
|
||||
const comments = [
|
||||
'<!-- This file is auto-generated. Do not edit it manually. -->\n',
|
||||
'<!-- In order to apply changes to it, adjust configuration object in generate-favicons.ts script and run it -->\n',
|
||||
];
|
||||
|
||||
const formattedHtml = html.map((line) => line.replace('>', '/>')).join('\n');
|
||||
|
||||
await writeFile(ASTRO_FILE_PATH, [...comments, formattedHtml, '\n']);
|
||||
|
||||
console.log(`${ASTRO_FILE_PATH} has been updated successfully`);
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const { images, files, html } = await generateFavicons();
|
||||
|
||||
await clearFaviconsDir();
|
||||
|
||||
await Promise.all([...images, ...files].map(saveFaviconAsset));
|
||||
|
||||
await generateAstroFile(html);
|
||||
};
|
||||
|
||||
main();
|
||||
59
scripts/generate-pdf.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { exec } from 'node:child_process';
|
||||
import * as path from 'node:path';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import { pdfPage } from 'puppeteer-report';
|
||||
|
||||
const waitFor = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const goTo = async (page: puppeteer.Page, url: string) => {
|
||||
await page.goto(url, { waitUntil: 'networkidle0' });
|
||||
};
|
||||
|
||||
type GoToReturn = ReturnType<typeof goTo>;
|
||||
|
||||
interface RetryOptions {
|
||||
promise: () => GoToReturn;
|
||||
retries: number;
|
||||
retryTime: number;
|
||||
}
|
||||
|
||||
const retry = async ({ promise, retries, retryTime }: RetryOptions): GoToReturn => {
|
||||
try {
|
||||
return await promise();
|
||||
} catch (error) {
|
||||
if (retries <= 0) throw error;
|
||||
|
||||
await waitFor(retryTime);
|
||||
|
||||
return await retry({ promise, retries: retries - 1, retryTime });
|
||||
}
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const child = exec('npm run dev');
|
||||
|
||||
const browser = await puppeteer.launch({ headless: 'new' });
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.setViewport({ width: 794, height: 1122, deviceScaleFactor: 2 });
|
||||
|
||||
await retry({
|
||||
promise: () => goTo(page, 'http://localhost:3000/pdf'),
|
||||
retries: 5,
|
||||
retryTime: 1000,
|
||||
});
|
||||
|
||||
await pdfPage(page, {
|
||||
path: path.join(__dirname, '..', 'public', 'cv.pdf'),
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
margin: { top: '10mm', right: '10mm', bottom: '10mm', left: '10mm' },
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
|
||||
child.kill();
|
||||
};
|
||||
|
||||
main();
|
||||
BIN
src/assets/logos/mensa.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/logos/sfsu.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
src/assets/logos/sfusd.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
src/assets/logos/vcu.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/my-image-default.jpeg
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/assets/my-image.jpeg
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
src/assets/portfolio/jaws-certificate.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
src/assets/portfolio/jaws.png
Normal file
|
After Width: | Height: | Size: 9 KiB |
BIN
src/assets/portfolio/mensa-letter.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
src/assets/portfolio/tt-certificate.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
src/assets/portfolio/tt.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
47
src/components/description.astro
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
import { marked } from 'marked';
|
||||
|
||||
export interface Props {
|
||||
content: string;
|
||||
classList: (string | undefined)[];
|
||||
}
|
||||
|
||||
const minIndent = (str: string) => {
|
||||
const match = str.match(/^[\t ]*(?=\S)/gm);
|
||||
|
||||
if (!match) return 0;
|
||||
|
||||
return match.reduce((r, a) => Math.min(r, a.length), Number.POSITIVE_INFINITY);
|
||||
};
|
||||
|
||||
const stripIndent = (str: string) => {
|
||||
const indent = minIndent(str);
|
||||
|
||||
if (indent === 0) return str;
|
||||
|
||||
const regex = new RegExp(`^[ \\t]{${indent}}`, 'gm');
|
||||
|
||||
return str.replace(regex, '');
|
||||
};
|
||||
|
||||
const parseMarkdown = (str: string) =>
|
||||
marked.parse(stripIndent(str), {
|
||||
breaks: true,
|
||||
headerIds: false,
|
||||
mangle: false,
|
||||
});
|
||||
|
||||
const { content, classList } = Astro.props;
|
||||
---
|
||||
|
||||
<div set:html={parseMarkdown(content)} class:list={['description', ...classList]} />
|
||||
|
||||
<style is:global>
|
||||
.description ul {
|
||||
@apply list-disc pl-5;
|
||||
}
|
||||
|
||||
.description a {
|
||||
@apply underline opacity-90;
|
||||
}
|
||||
</style>
|
||||
10
src/components/fonts.astro
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<style is:global>
|
||||
@font-face {
|
||||
font-family: 'Inter var';
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-named-instance: 'Regular';
|
||||
src: url('/fonts/Inter-roman.var.woff2') format('woff2');
|
||||
}
|
||||
</style>
|
||||
25
src/components/photo.astro
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
import type { Photo } from '@/types/shared';
|
||||
import { Image } from '@astrojs/image/components';
|
||||
|
||||
export interface Props {
|
||||
src: Photo;
|
||||
alt: string;
|
||||
class?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const { src, ...props } = Astro.props;
|
||||
|
||||
const isRemoteImage = typeof src === 'string';
|
||||
const loading = import.meta.env.PUBLIC_APP_ENV === 'snapshot' ? 'eager' : 'lazy';
|
||||
---
|
||||
|
||||
{
|
||||
isRemoteImage ? (
|
||||
<img src={src} {...props} />
|
||||
) : (
|
||||
<Image format="webp" fit="cover" src={src} loading={loading} {...props} />
|
||||
)
|
||||
}
|
||||
15
src/data/_internals/create-link-factory.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { LinkButton } from '@/types/shared';
|
||||
import type { Merge } from 'type-fest';
|
||||
|
||||
type LinkWithoutUrl = Omit<LinkButton, 'url'>;
|
||||
type PartialLinkWithUrl = Partial<LinkButton> & { url: string };
|
||||
|
||||
const createLinkFactory =
|
||||
<Link extends LinkWithoutUrl>(defaultData: Readonly<Link>) =>
|
||||
<Override extends PartialLinkWithUrl>(override: Readonly<Override>) =>
|
||||
({
|
||||
...defaultData,
|
||||
...override,
|
||||
} as Readonly<Merge<Link, Override>>);
|
||||
|
||||
export default createLinkFactory;
|
||||
16
src/data/_internals/create-skill-factory.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { LevelledSkill, Skill, SkillLevel } from '@/types/sections/skills-section.types';
|
||||
import type { Merge } from 'type-fest';
|
||||
|
||||
type SkillWithoutDescription = Omit<Skill, 'description'>;
|
||||
|
||||
interface SkillFactory<S extends SkillWithoutDescription> {
|
||||
<T extends Partial<LevelledSkill>>(data: Readonly<T & { level: SkillLevel }>): Readonly<Merge<S, T>>;
|
||||
<T extends Partial<Skill> | undefined = undefined>(data?: Readonly<T & { level?: never }>): T extends undefined
|
||||
? Readonly<S>
|
||||
: Readonly<Merge<S, T>>;
|
||||
}
|
||||
|
||||
const createSkillFactory = <S extends SkillWithoutDescription>(defaultData: Readonly<S>) =>
|
||||
((data: Record<string, unknown>) => ({ ...defaultData, ...data })) as SkillFactory<S>;
|
||||
|
||||
export default createSkillFactory;
|
||||
13
src/data/_internals/get-cv-data.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import type { Data } from '@/types/data';
|
||||
import transformData from './transform-data';
|
||||
import configData from '../config';
|
||||
import sectionsData from '../sections';
|
||||
|
||||
const data = { config: configData, sections: sectionsData } as const satisfies ReadonlyDeep<Data>;
|
||||
|
||||
export type PreciseData = typeof data;
|
||||
|
||||
const getCvData = transformData(data);
|
||||
|
||||
export default getCvData;
|
||||
14
src/data/_internals/transform-data.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import type { Data } from '@/types/data';
|
||||
import { produce } from 'immer';
|
||||
import type { PreciseData } from './get-cv-data';
|
||||
import type { DataTransformer } from './transformers';
|
||||
|
||||
const transformData =
|
||||
(data: PreciseData) =>
|
||||
(...callbacks: DataTransformer[]): Data =>
|
||||
// @ts-ignore -- waiting for https://github.com/sindresorhus/type-fest/pull/540 to be merged
|
||||
produce(data, (draft) => {
|
||||
callbacks.forEach((callback) => callback(draft));
|
||||
});
|
||||
|
||||
export default transformData;
|
||||
92
src/data/_internals/transformers.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import type { Data } from '@/types/data';
|
||||
import type { Draft } from 'immer';
|
||||
import type { PreciseData } from './get-cv-data';
|
||||
|
||||
export type DraftData = Draft<Data>;
|
||||
|
||||
export type DataTransformer = (draft: DraftData) => void;
|
||||
|
||||
type Sections = PreciseData['sections'];
|
||||
|
||||
type SectionKey = keyof Sections;
|
||||
|
||||
type ProjectName = Sections['portfolio']['projects'][number]['name'];
|
||||
|
||||
type JobRole = Sections['experience']['jobs'][number]['role'];
|
||||
|
||||
type JobCompany = Sections['experience']['jobs'][number]['company'];
|
||||
|
||||
type DiplomaTitle = Sections['education']['diplomas'][number]['title'];
|
||||
|
||||
type DiplomaInstitution = Sections['education']['diplomas'][number]['institution'];
|
||||
|
||||
type SkillSets = Sections['skills']['skillSets'];
|
||||
|
||||
type SkillSetTitle = SkillSets[number]['title'];
|
||||
|
||||
type Filter<T extends Readonly<unknown[]>, P> = T extends Readonly<[infer A, ...infer Rest]>
|
||||
? [...(A extends P ? [A] : []), ...Filter<Rest, P>]
|
||||
: [];
|
||||
|
||||
type SkillsBySkillSet<SkillSet extends SkillSetTitle> = Filter<
|
||||
SkillSets,
|
||||
{ title: SkillSet }
|
||||
>[number]['skills'][number]['name'];
|
||||
|
||||
export const hideSection =
|
||||
(section: SectionKey): DataTransformer =>
|
||||
(draft) => {
|
||||
draft.sections[section].config.visible = false;
|
||||
};
|
||||
|
||||
export const hideJob =
|
||||
(role: JobRole, company?: JobCompany): DataTransformer =>
|
||||
(draft) => {
|
||||
draft.sections.experience.jobs = draft.sections.experience.jobs.filter(
|
||||
(job) => job.role !== role && job.company !== company
|
||||
);
|
||||
};
|
||||
|
||||
export const hideDiploma =
|
||||
(title: DiplomaTitle, institution?: DiplomaInstitution): DataTransformer =>
|
||||
(draft) => {
|
||||
draft.sections.education.diplomas = draft.sections.education.diplomas.filter(
|
||||
(diploma) => diploma.title === title && diploma.institution === institution
|
||||
);
|
||||
};
|
||||
|
||||
export const hideProject =
|
||||
(name: ProjectName): DataTransformer =>
|
||||
(draft) => {
|
||||
draft.sections.portfolio.projects = draft.sections.portfolio.projects.filter((project) => project.name !== name);
|
||||
};
|
||||
|
||||
export const hideSkillSet =
|
||||
(name: SkillSetTitle): DataTransformer =>
|
||||
(draft) => {
|
||||
draft.sections.skills.skillSets = draft.sections.skills.skillSets.filter((skillSet) => skillSet.title !== name);
|
||||
};
|
||||
|
||||
export const renameSkillSet =
|
||||
(from: SkillSetTitle, to: string): DataTransformer =>
|
||||
(draft) => {
|
||||
draft.sections.skills.skillSets = draft.sections.skills.skillSets.map((skillSet) =>
|
||||
skillSet.title === from ? { ...skillSet, title: to } : skillSet
|
||||
);
|
||||
};
|
||||
|
||||
export const hideSkills =
|
||||
<SkillSet extends SkillSetTitle>(
|
||||
skillSetTitle: SkillSetTitle,
|
||||
skills: SkillsBySkillSet<SkillSet>[]
|
||||
): DataTransformer =>
|
||||
(draft) => {
|
||||
draft.sections.skills.skillSets = draft.sections.skills.skillSets.map((skillSet) => {
|
||||
if (skillSet.title !== skillSetTitle) return skillSet;
|
||||
|
||||
return {
|
||||
...skillSet,
|
||||
skills: skillSet.skills.filter((skill) => !skills.includes(skill.name as (typeof skills)[number])),
|
||||
};
|
||||
});
|
||||
};
|
||||
25
src/data/config.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import type { Config } from '@/types/data';
|
||||
import { enUS } from 'date-fns/locale';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
|
||||
const config = {
|
||||
i18n: {
|
||||
locale: enUS,
|
||||
dateFormat: 'MMM yyyy',
|
||||
translations: {
|
||||
now: 'Present',
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
title: 'Juyoung Lee - Resume',
|
||||
description: 'Resume about Juyoung Lee',
|
||||
faviconPath: '/src/assets/my-image.jpeg',
|
||||
},
|
||||
pdf: {
|
||||
footer:
|
||||
//'I hereby give consent for my personal data included in my application to be processed for the purposes of the recruitment process.',
|
||||
'',
|
||||
},
|
||||
} as const satisfies ReadonlyDeep<Config>;
|
||||
|
||||
export default config;
|
||||
168
src/data/helpers/links.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import createLinkFactory from '@/data/_internals/create-link-factory';
|
||||
|
||||
/*
|
||||
|
||||
Use this file to define all websites you use as the "links" property.
|
||||
This way, you can ensure one website has the same name and icon, among all resume sections.
|
||||
|
||||
Where links are used:
|
||||
- education-section.data.ts
|
||||
- experience-section.data.ts
|
||||
- main-section.data.ts
|
||||
- portfolio-section.data.ts
|
||||
- skills-section.data.ts
|
||||
- testimonials-section.data.ts
|
||||
|
||||
Usage examples:
|
||||
link({ url: '...' }) — returns base link object with provided url.
|
||||
link({ name: '...', url: '...' }) — returns link object with a custom name.
|
||||
*/
|
||||
|
||||
// GENERAL
|
||||
export const tiktok = createLinkFactory({
|
||||
name: 'TikTok',
|
||||
icon: 'fa6-brands:tiktok',
|
||||
});
|
||||
|
||||
export const facebook = createLinkFactory({
|
||||
name: 'Facebook',
|
||||
icon: 'fa6-brands:facebook-f',
|
||||
});
|
||||
|
||||
export const linkedin = createLinkFactory({
|
||||
name: 'LinkedIn',
|
||||
icon: 'fa6-brands:linkedin-in',
|
||||
});
|
||||
|
||||
export const twitter = createLinkFactory({
|
||||
name: 'Twitter',
|
||||
icon: 'fa6-brands:twitter',
|
||||
});
|
||||
|
||||
export const pinterest = createLinkFactory({
|
||||
name: 'Pinterest',
|
||||
icon: 'fa6-brands:pinterest',
|
||||
});
|
||||
|
||||
// CODE
|
||||
|
||||
export const github = createLinkFactory({
|
||||
name: 'GitHub',
|
||||
icon: 'fa6-brands:github',
|
||||
});
|
||||
|
||||
export const codepen = createLinkFactory({
|
||||
name: 'CodePen',
|
||||
icon: 'fa6-brands:codepen',
|
||||
});
|
||||
|
||||
export const stackblitz = createLinkFactory({
|
||||
name: 'StackBlitz',
|
||||
icon: 'simple-icons:stackblitz',
|
||||
});
|
||||
|
||||
export const codesandbox = createLinkFactory({
|
||||
name: 'CodeSandbox',
|
||||
icon: 'simple-icons:codesandbox',
|
||||
});
|
||||
|
||||
// BLOG
|
||||
|
||||
export const dev = createLinkFactory({
|
||||
name: 'Dev',
|
||||
icon: 'fa6-brands:dev',
|
||||
});
|
||||
|
||||
export const medium = createLinkFactory({
|
||||
name: 'Medium',
|
||||
icon: 'fa6-brands:medium',
|
||||
});
|
||||
|
||||
// FORUM / CHAT
|
||||
|
||||
export const reddit = createLinkFactory({
|
||||
name: 'Reddit',
|
||||
icon: 'fa6-brands:reddit',
|
||||
});
|
||||
|
||||
export const quora = createLinkFactory({
|
||||
name: 'Quora',
|
||||
icon: 'fa6-brands:quora',
|
||||
});
|
||||
|
||||
export const stackoverflow = createLinkFactory({
|
||||
name: 'Stack Overflow',
|
||||
icon: 'fa6-brands:stack-overflow',
|
||||
});
|
||||
|
||||
// DESIGN
|
||||
|
||||
export const instagram = createLinkFactory({
|
||||
name: 'Instagram',
|
||||
icon: 'fa6-brands:instagram',
|
||||
});
|
||||
export const behance = createLinkFactory({
|
||||
name: 'Behance',
|
||||
icon: 'fa6-brands:behance',
|
||||
});
|
||||
|
||||
export const dribbble = createLinkFactory({
|
||||
name: 'Dribbble',
|
||||
icon: 'fa6-brands:dribbble',
|
||||
});
|
||||
|
||||
export const figma = createLinkFactory({
|
||||
name: 'Figma',
|
||||
icon: 'fa6-brands:figma',
|
||||
});
|
||||
|
||||
// MUSIC
|
||||
|
||||
export const spotify = createLinkFactory({
|
||||
name: 'Spotify',
|
||||
icon: 'fa6-brands:spotify',
|
||||
});
|
||||
|
||||
export const soundcloud = createLinkFactory({
|
||||
name: 'SoundCloud',
|
||||
icon: 'fa6-brands:soundcloud',
|
||||
});
|
||||
|
||||
// VIDEO
|
||||
|
||||
export const youtube = createLinkFactory({
|
||||
name: 'YouTube',
|
||||
icon: 'fa6-brands:youtube',
|
||||
});
|
||||
|
||||
export const twitch = createLinkFactory({
|
||||
name: 'Twitch',
|
||||
icon: 'fa6-brands:twitch',
|
||||
});
|
||||
|
||||
export const vimeo = createLinkFactory({
|
||||
name: 'Vimeo',
|
||||
icon: 'fa6-brands:vimeo',
|
||||
});
|
||||
|
||||
// PROJECT TYPE
|
||||
|
||||
export const website = createLinkFactory({
|
||||
name: 'Website',
|
||||
icon: 'fa6-solid:globe',
|
||||
});
|
||||
|
||||
export const demo = createLinkFactory({
|
||||
name: 'App demo',
|
||||
icon: 'fa6-solid:desktop',
|
||||
});
|
||||
|
||||
export const mockups = createLinkFactory({
|
||||
name: 'Mockups',
|
||||
icon: 'fa6-solid:paintbrush',
|
||||
});
|
||||
|
||||
export const repository = createLinkFactory({
|
||||
name: 'Repository',
|
||||
icon: 'fa6-solid:code-branch',
|
||||
});
|
||||
243
src/data/helpers/skills.ts
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import createSkillFactory from '@/data/_internals/create-skill-factory';
|
||||
|
||||
/*
|
||||
|
||||
Place where you can define all your skills.
|
||||
This way, you can ensure one skill has the same name, icon, and URL among all resume sections.
|
||||
|
||||
Where skills are used:
|
||||
- experience-section.data.ts
|
||||
- portfolio-section.data.ts
|
||||
- skills-section.data.ts
|
||||
|
||||
Usage examples:
|
||||
skill() — returns skill object without any customizations.
|
||||
skill({ level: 3 }) — returns a levelled-skill. It can be used only in skills-section.data.ts.
|
||||
skill({ name: '...' }) — returns skill object with a custom name.
|
||||
skill({ description: '...' }) — returns skill with a description displayed when user hovers over it.
|
||||
|
||||
*/
|
||||
|
||||
export const python = createSkillFactory({
|
||||
name: 'Python',
|
||||
icon: 'simple-icons:python',
|
||||
iconColor: '#3776AB',
|
||||
url: '#',
|
||||
});
|
||||
|
||||
export const mysql = createSkillFactory({
|
||||
name: 'MySQL',
|
||||
icon: 'simple-icons:mysql',
|
||||
iconColor: '#4479A1',
|
||||
url: '#',
|
||||
});
|
||||
|
||||
export const coldfusion = createSkillFactory({
|
||||
name: 'ColdFusion',
|
||||
icon: 'simple-icons:c',
|
||||
iconColor: '#77a8f7',
|
||||
url: '#',
|
||||
});
|
||||
|
||||
export const dreamweaver = createSkillFactory({
|
||||
name: 'Dreamweaver',
|
||||
icon: 'simple-icons:adobedreamweaver',
|
||||
iconColor: '#FF61F6',
|
||||
url: '#',
|
||||
});
|
||||
|
||||
export const jquery = createSkillFactory({
|
||||
name: 'jQuery',
|
||||
icon: 'simple-icons:jquery',
|
||||
iconColor: '#0769AD',
|
||||
url: '#',
|
||||
});
|
||||
|
||||
export const bootstrap = createSkillFactory({
|
||||
name: 'Bootstrap',
|
||||
icon: 'simple-icons:bootstrap',
|
||||
iconColor: '#7952B3',
|
||||
url: '#',
|
||||
});
|
||||
|
||||
export const html = createSkillFactory({
|
||||
name: 'HTML',
|
||||
icon: 'simple-icons:html5',
|
||||
iconColor: '#E34F26',
|
||||
url: '#',
|
||||
});
|
||||
|
||||
export const css = createSkillFactory({
|
||||
name: 'CSS',
|
||||
icon: 'simple-icons:css3',
|
||||
iconColor: '#1572B6',
|
||||
url: '#',
|
||||
});
|
||||
|
||||
export const javascript = createSkillFactory({
|
||||
name: 'JavaScript',
|
||||
icon: 'simple-icons:javascript',
|
||||
iconColor: '#dec81b',
|
||||
url: '#',
|
||||
});
|
||||
|
||||
export const java = createSkillFactory({
|
||||
name: 'Java',
|
||||
icon: 'simple-icons:coffeescript',
|
||||
iconColor: '#2F2625',
|
||||
url: '#',
|
||||
});
|
||||
|
||||
export const processing = createSkillFactory({
|
||||
name: 'Processing',
|
||||
icon: 'simple-icons:processingfoundation',
|
||||
iconColor: '#006699',
|
||||
url: '#',
|
||||
});
|
||||
|
||||
export const apolloGraphql = createSkillFactory({
|
||||
name: 'Apollo GraphQL',
|
||||
icon: 'simple-icons:apollographql',
|
||||
iconColor: '#311C87',
|
||||
url: 'https://www.apollographql.com/',
|
||||
});
|
||||
|
||||
export const astro = createSkillFactory({
|
||||
name: 'Astro',
|
||||
icon: 'simple-icons:astro',
|
||||
iconColor: '#FF5D01',
|
||||
url: 'https://astro.build/',
|
||||
});
|
||||
|
||||
export const chakraUi = createSkillFactory({
|
||||
name: 'Chakra UI',
|
||||
icon: 'simple-icons:chakraui',
|
||||
iconColor: '#319795',
|
||||
url: 'https://chakra-ui.com/',
|
||||
});
|
||||
|
||||
export const cypress = createSkillFactory({
|
||||
name: 'Cypress',
|
||||
icon: 'simple-icons:cypress',
|
||||
iconColor: '#17202C',
|
||||
url: 'https://www.cypress.io/',
|
||||
});
|
||||
|
||||
export const eslint = createSkillFactory({
|
||||
name: 'ESLint',
|
||||
icon: 'simple-icons:eslint',
|
||||
iconColor: '#4B32C3',
|
||||
url: 'https://eslint.org/',
|
||||
});
|
||||
|
||||
export const firebase = createSkillFactory({
|
||||
name: 'Firebase',
|
||||
icon: 'simple-icons:firebase',
|
||||
iconColor: '#FFCA28',
|
||||
url: 'https://firebase.google.com/',
|
||||
});
|
||||
|
||||
export const jest = createSkillFactory({
|
||||
name: 'Jest',
|
||||
icon: 'simple-icons:jest',
|
||||
iconColor: '#C21325',
|
||||
url: 'https://jestjs.io/',
|
||||
});
|
||||
|
||||
export const mongoDb = createSkillFactory({
|
||||
name: 'MongoDB',
|
||||
icon: 'simple-icons:mongodb',
|
||||
iconColor: '#47A248',
|
||||
url: 'https://www.mongodb.com/',
|
||||
});
|
||||
|
||||
export const nestJs = createSkillFactory({
|
||||
name: 'NestJS',
|
||||
icon: 'simple-icons:nestjs',
|
||||
iconColor: '#E0234E',
|
||||
url: 'https://nestjs.com/',
|
||||
});
|
||||
|
||||
export const nextJs = createSkillFactory({
|
||||
name: 'Next.js',
|
||||
icon: 'simple-icons:nextdotjs',
|
||||
iconColor: '#000000',
|
||||
url: 'https://nextjs.org/',
|
||||
});
|
||||
|
||||
export const nx = createSkillFactory({
|
||||
name: 'Nx',
|
||||
icon: 'simple-icons:nx',
|
||||
iconColor: '#143055',
|
||||
url: 'https://nx.dev/',
|
||||
});
|
||||
|
||||
export const pnpm = createSkillFactory({
|
||||
name: 'pnpm',
|
||||
icon: 'simple-icons:pnpm',
|
||||
iconColor: '#F69220',
|
||||
url: 'https://pnpm.io/',
|
||||
});
|
||||
|
||||
export const postgreSql = createSkillFactory({
|
||||
name: 'PostgreSQL',
|
||||
icon: 'simple-icons:postgresql',
|
||||
iconColor: '#4169E1',
|
||||
url: 'https://www.postgresql.org/',
|
||||
});
|
||||
|
||||
export const prettier = createSkillFactory({
|
||||
name: 'Prettier',
|
||||
icon: 'simple-icons:prettier',
|
||||
iconColor: '#F7B93E',
|
||||
url: 'https://prettier.io/',
|
||||
});
|
||||
|
||||
export const react = createSkillFactory({
|
||||
name: 'React.js',
|
||||
icon: 'simple-icons:react',
|
||||
iconColor: '#61DAFB',
|
||||
url: 'https://reactjs.org/',
|
||||
});
|
||||
|
||||
export const reactQuery = createSkillFactory({
|
||||
name: 'React Query',
|
||||
icon: 'simple-icons:reactquery',
|
||||
iconColor: '#FF4154',
|
||||
url: 'https://tanstack.com/query',
|
||||
});
|
||||
|
||||
export const sass = createSkillFactory({
|
||||
name: 'SASS',
|
||||
icon: 'simple-icons:sass',
|
||||
iconColor: '#CC6699',
|
||||
url: 'https://sass-lang.com/',
|
||||
});
|
||||
|
||||
export const supabase = createSkillFactory({
|
||||
name: 'Supabase',
|
||||
icon: 'simple-icons:supabase',
|
||||
iconColor: '#3ECF8E',
|
||||
url: 'https://supabase.io/',
|
||||
});
|
||||
|
||||
export const tailwindCss = createSkillFactory({
|
||||
name: 'Tailwind CSS',
|
||||
icon: 'simple-icons:tailwindcss',
|
||||
iconColor: '#06B6D4',
|
||||
url: 'https://tailwindcss.com/',
|
||||
});
|
||||
|
||||
export const typescript = createSkillFactory({
|
||||
name: 'TypeScript',
|
||||
icon: 'simple-icons:typescript',
|
||||
iconColor: '#3178C6',
|
||||
url: 'https://www.typescriptlang.org/',
|
||||
});
|
||||
|
||||
export const vue = createSkillFactory({
|
||||
name: 'Vue.js',
|
||||
icon: 'simple-icons:vuedotjs',
|
||||
iconColor: '#4FC08D',
|
||||
url: 'https://vuejs.org/',
|
||||
});
|
||||
2
src/data/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './_internals/transformers';
|
||||
export { default } from './_internals/get-cv-data';
|
||||
41
src/data/sections/education-section.data.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import type { EducationSection } from '@/types/sections/education-section.types';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import { website } from '../helpers/links';
|
||||
|
||||
const educationSectionData = {
|
||||
config: {
|
||||
title: 'Education',
|
||||
slug: 'education',
|
||||
icon: 'fa6-solid:graduation-cap',
|
||||
visible: true,
|
||||
},
|
||||
diplomas: [
|
||||
{
|
||||
title: 'Master of Science (M.S.) in Engineering, Concentration in Aerospace Engineering',
|
||||
institution: 'Virginia Commonwealth University',
|
||||
image: import('@/assets/logos/vcu.png'),
|
||||
dates: [new Date('2024-02'), null],
|
||||
description: '2024-',
|
||||
links: [website({ url: 'https://egr.vcu.edu' })],
|
||||
},
|
||||
{
|
||||
title: 'Bachelor of Science (B.S.) in Computer Science 🎓',
|
||||
institution: 'San Francisco State University',
|
||||
image: import('@/assets/logos/sfsu.png'),
|
||||
dates: [new Date('2018-02'), new Date('2020-02')],
|
||||
description: 'Graduated: 2020',
|
||||
links: [website({ url: 'https://cs.sfsu.edu' })],
|
||||
},
|
||||
/*
|
||||
{
|
||||
title: 'Information Technology',
|
||||
institution: 'Wrocław University of Science and Technology',
|
||||
image: import('@/assets/logos/wroclaw-university-of-technology.jpg'),
|
||||
dates: [new Date('2011.10'), new Date('2014.07')],
|
||||
description: "Bachelor's degree. Specialization in application development.",
|
||||
links: [website({ url: '#' })],
|
||||
},*/
|
||||
],
|
||||
} as const satisfies ReadonlyDeep<EducationSection>;
|
||||
|
||||
export default educationSectionData;
|
||||
109
src/data/sections/experience-section.data.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import type { ExperienceSection } from '@/types/sections/experience-section.types';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import { facebook, github, tiktok, instagram, linkedin, twitter, website, youtube } from '../helpers/links';
|
||||
import {
|
||||
chakraUi,
|
||||
eslint,
|
||||
firebase,
|
||||
nextJs,
|
||||
nx,
|
||||
pnpm,
|
||||
react,
|
||||
reactQuery,
|
||||
tailwindCss,
|
||||
typescript,
|
||||
vue,
|
||||
mysql,
|
||||
coldfusion,
|
||||
html,
|
||||
css,
|
||||
javascript,
|
||||
dreamweaver,
|
||||
jquery,
|
||||
bootstrap,
|
||||
java,
|
||||
processing,
|
||||
python,
|
||||
} from '../helpers/skills';
|
||||
|
||||
const experienceSectionData = {
|
||||
config: {
|
||||
title: 'Work experience',
|
||||
slug: 'experience',
|
||||
icon: 'fa6-solid:suitcase',
|
||||
visible: true,
|
||||
},
|
||||
jobs: [
|
||||
// {
|
||||
// role: 'Founder',
|
||||
// company: 'Mimory AI',
|
||||
// image: import('@/assets/logos/mimory.jpg'),
|
||||
// dates: [new Date('2025-05-02'), null], //null ], // Use null for 'Present'
|
||||
// description: ``,
|
||||
// tagsList: {
|
||||
// title: '',
|
||||
// tags: [],
|
||||
// },
|
||||
// links: [
|
||||
// tiktok({ url: 'https://tiktok.com/@mimoryai' }),
|
||||
// youtube({ url: 'https://youtube.com/@mimoryai' }),
|
||||
// instagram({ url: 'https://instagram.com/mimoryai' }),
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
role: 'Web Programmer/Systems Analyst',
|
||||
company: 'National Training and Data Center',
|
||||
image: import('@/assets/logos/vcu.png'),
|
||||
dates: [new Date('2021-03-02'), null], //null ], // Use null for 'Present'
|
||||
description: `
|
||||
- Developed and maintained server-side modules for high-volume data systems funded by the Social Security Administration (SSA), implementing reliable data processing logic and improving system scalability across large datasets
|
||||
- Designed scalable backend systems aligned with evolving program requirements, improving long-term maintainability and supporting operational efficiency
|
||||
- Implemented backend security and privacy enhancements in alignment with federal security standards (FISMA), improving data protection and system resilience
|
||||
- Monitored application performance, diagnosing and resolving issues to ensure high system availability and reliable backend operations
|
||||
`,
|
||||
tagsList: {
|
||||
title: '',
|
||||
tags: [mysql(), coldfusion()],
|
||||
},
|
||||
links: [], //[facebook({ url: '#' }), linkedin({ url: '#' })],
|
||||
},
|
||||
{
|
||||
role: 'Web Accessibility Developer',
|
||||
company: 'San Francisco State University',
|
||||
image: import('@/assets/logos/sfsu.png'),
|
||||
dates: [new Date('2018-08-02'), new Date('2020-05-02')],
|
||||
description: `
|
||||
- Fixed HTML, CSS, JavaScript on campus and 3rd party websites and learning platform (iLearn), and maintained campus-wide best practices for meeting web accessibility requirements WCAG 2.0 (AA, AAA)
|
||||
- Converted documents accessible using PDF Accessibility Checker, CommonLook, MS Word, and corrected errors in headings, reading order, images, tables, etc.
|
||||
- Created text and HTML-based content specific to the accessibility best practices
|
||||
- Provided expert technical assistance and group training for campus-wide IT personnel (webmasters, newsletter editors)
|
||||
- Documented defects using manual and automated testing tools, and collaborated with development teams to establish technical specifications
|
||||
- Managed accessible computer stations, and proctored exams for students who need accommodation
|
||||
- Tools used: debugging tools in Firefox/Chrome, Drupal, WordPress, JAWS, WAVE, ARIA, Colour Contrast Analyzer, Link Klipper, Compliance Sheriff (automated testing tool), etc.
|
||||
`,
|
||||
tagsList: {
|
||||
title: '',
|
||||
tags: [html(), css(), javascript()],
|
||||
},
|
||||
links: [], //[website({ url: '#' }), instagram({ url: '#' })],
|
||||
},
|
||||
{
|
||||
role: 'Java Teaching Assistant',
|
||||
company: 'San Francisco Unified School District',
|
||||
image: import('@/assets/logos/sfusd.png'),
|
||||
dates: [new Date('2019-01-02'), new Date('2019-05-02')],
|
||||
description: `
|
||||
- As part of the National Science Foundation-funded program (CS4SF), taught object-oriented Java programming in 9-12th grade classroom, leveraging knowledge acquired from university coursework
|
||||
- Assisted teacher to help 30-40 students debug in-class coding assignments, and engaged students in learning activities
|
||||
- Maintained positive, calm attitude and soft voice, and worked under teacher's direction to establish clean and comfortable classroom 🤗
|
||||
`,
|
||||
tagsList: {
|
||||
title: '',
|
||||
tags: [java(), processing()],
|
||||
},
|
||||
links: [], //[twitter({ url: '#' }), github({ url: '#' })],
|
||||
},
|
||||
],
|
||||
} as const satisfies ReadonlyDeep<ExperienceSection>;
|
||||
|
||||
export default experienceSectionData;
|
||||
138
src/data/sections/favorites-section.data.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import type { FavoritesSection } from '@/types/sections/favorites-section.types';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
|
||||
const favoritesSectionData = {
|
||||
config: {
|
||||
title: 'My favorites',
|
||||
slug: 'favorites',
|
||||
icon: 'fa6-solid:star',
|
||||
visible: true,
|
||||
},
|
||||
books: {
|
||||
title: 'Books I read',
|
||||
data: [
|
||||
{
|
||||
image: import('@/assets/favorites/books/book-1.jpeg'),
|
||||
title: 'The Pragmatic Programmer: From Journeyman to Master',
|
||||
author: 'Andy Hunt, Dave Thomas',
|
||||
url: 'https://www.goodreads.com/book/show/4099.The_Pragmatic_Programmer',
|
||||
},
|
||||
{
|
||||
image: import('@/assets/favorites/books/book-2.jpg'),
|
||||
title: 'Domain-Driven Design: Tackling Complexity in the Heart of Software',
|
||||
author: 'Eric Evans',
|
||||
url: 'https://www.goodreads.com/book/show/179133.Domain_Driven_Design',
|
||||
},
|
||||
{
|
||||
image: import('@/assets/favorites/books/book-3.jpeg'),
|
||||
title: 'Clean Code: A Handbook of Agile Software Craftsmanship',
|
||||
author: 'Robert C. Martin',
|
||||
url: 'https://www.goodreads.com/book/show/3735293-clean-code',
|
||||
},
|
||||
{
|
||||
image: import('@/assets/favorites/books/book-4.jpeg'),
|
||||
title: 'The Clean Coder: A Code of Conduct for Professional Programmers',
|
||||
author: 'Robert C. Martin',
|
||||
url: 'https://www.goodreads.com/book/show/10284614-the-clean-coder',
|
||||
},
|
||||
],
|
||||
},
|
||||
people: {
|
||||
title: 'People I learn from',
|
||||
data: [
|
||||
{
|
||||
image: import('@/assets/favorites/people/person-1.jpg'),
|
||||
name: 'Kent C. Dodds',
|
||||
url: 'https://kentcdodds.com/',
|
||||
},
|
||||
{
|
||||
image: import('@/assets/favorites/people/person-2.jpeg'),
|
||||
name: 'Kent Beck',
|
||||
url: 'https://www.kentbeck.com/',
|
||||
},
|
||||
{
|
||||
image: import('@/assets/favorites/people/person-3.jpeg'),
|
||||
name: 'Eric Evans',
|
||||
url: 'https://www.domainlanguage.com/',
|
||||
},
|
||||
{
|
||||
image: import('@/assets/favorites/people/person-4.jpeg'),
|
||||
name: 'Martin Fowler',
|
||||
url: 'https://martinfowler.com/',
|
||||
},
|
||||
{
|
||||
image: import('@/assets/favorites/people/person-5.jpg'),
|
||||
name: 'Robert C. Martin',
|
||||
url: 'http://cleancoder.com/',
|
||||
},
|
||||
{
|
||||
image: import('@/assets/favorites/people/person-6.jpeg'),
|
||||
name: 'Adam Dymitruk',
|
||||
url: 'https://eventmodeling.org/',
|
||||
},
|
||||
],
|
||||
},
|
||||
videos: {
|
||||
title: 'Videos I watched',
|
||||
data: [
|
||||
{
|
||||
image: import('@/assets/favorites/videos/video-1.jpeg'),
|
||||
title: 'Building Resilient Frontend Architecture • Monica Lent • GOTO 2019',
|
||||
url: 'https://youtu.be/TqfbAXCCVwE',
|
||||
},
|
||||
{
|
||||
image: import('@/assets/favorites/videos/video-2.jpeg'),
|
||||
title: 'Scaling Yourself • Scott Hanselman • GOTO 2012',
|
||||
url: 'https://youtu.be/FS1mnISoG7U',
|
||||
},
|
||||
{
|
||||
image: import('@/assets/favorites/videos/video-3.jpeg'),
|
||||
title: "Why Isn't Functional Programming the Norm? - Richard Feldman",
|
||||
url: 'https://youtu.be/QyJZzq0v7Z4',
|
||||
},
|
||||
],
|
||||
},
|
||||
medias: {
|
||||
title: 'Media I follow',
|
||||
data: [
|
||||
{
|
||||
image: import('@/assets/favorites/media/media-1.jpeg'),
|
||||
title: 'Fireship.io',
|
||||
type: 'YouTube channel',
|
||||
url: 'https://www.youtube.com/c/Fireship',
|
||||
},
|
||||
{
|
||||
image: import('@/assets/favorites/media/media-2.jpeg'),
|
||||
title: 'Healthy Software Developer',
|
||||
type: 'YouTube channel',
|
||||
url: 'https://www.youtube.com/channel/UCfe_znKY1ukrqlGActlFmaQ',
|
||||
},
|
||||
{
|
||||
image: import('@/assets/favorites/media/media-3.png'),
|
||||
title: 'Bytes',
|
||||
type: 'Newsletter',
|
||||
url: 'https://bytes.dev/',
|
||||
},
|
||||
{
|
||||
image: import('@/assets/favorites/media/media-4.png'),
|
||||
title: 'TypeScript Weekly',
|
||||
type: 'Newsletter',
|
||||
url: 'https://typescript-weekly.com/',
|
||||
},
|
||||
{
|
||||
image: import('@/assets/favorites/media/media-5.jpeg'),
|
||||
title: 'Front End Happy Hour',
|
||||
type: 'Podcast',
|
||||
url: 'https://www.frontendhappyhour.com/',
|
||||
},
|
||||
{
|
||||
image: import('@/assets/favorites/media/media-6.webp'),
|
||||
title: '.cult by Honeypot',
|
||||
type: 'Blog',
|
||||
url: 'https://cult.honeypot.io/',
|
||||
},
|
||||
],
|
||||
},
|
||||
} as const satisfies ReadonlyDeep<FavoritesSection>;
|
||||
|
||||
export default favoritesSectionData;
|
||||
23
src/data/sections/index.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { Sections } from '@/types/data';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import educationData from './education-section.data';
|
||||
import experienceData from './experience-section.data';
|
||||
import favoritesData from './favorites-section.data';
|
||||
import mainData from './main-section.data';
|
||||
import membershipData from './membership-section.data';
|
||||
import portfolioData from './portfolio-section.data';
|
||||
import skillsData from './skills-section.data';
|
||||
import testimonialsData from './testimonials-section.data';
|
||||
|
||||
export const sections = {
|
||||
main: mainData,
|
||||
//skills: skillsData,
|
||||
experience: experienceData,
|
||||
portfolio: portfolioData,
|
||||
education: educationData,
|
||||
membership: membershipData,
|
||||
//testimonials: testimonialsData,
|
||||
//favorites: favoritesData,
|
||||
} as const satisfies ReadonlyDeep<Sections>;
|
||||
|
||||
export default sections;
|
||||
44
src/data/sections/main-section.data.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import type { MainSection } from '@/types/sections/main-section.types';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import { facebook, github, linkedin, twitter, website } from '../helpers/links';
|
||||
|
||||
const mainSectionData = {
|
||||
config: {
|
||||
icon: 'fa6-solid:user',
|
||||
title: 'Profile',
|
||||
slug: 'profile',
|
||||
visible: true,
|
||||
},
|
||||
image: import('@/assets/my-image.jpeg'),
|
||||
fullName: 'Jay Lee',
|
||||
role: 'Software Engineer',
|
||||
details: [
|
||||
// { label: 'Phone', value: '605 475 6961', url: 'tel:605 475 6961' },
|
||||
{ label: 'Location', value: 'San Francisco, CA' },
|
||||
{ label: 'Email', value: '✉️', url: 'https://go.juyung.com/email' },
|
||||
// { label: 'Salary range', value: '18 000 - 25 000 PLN' },
|
||||
],
|
||||
pdfDetails: [
|
||||
// { label: 'Phone', value: '605 475 6961' },
|
||||
// { label: 'Email', value: 'mark.freeman.dev@gmail.com' },
|
||||
{ label: 'Website', value: 'juyung.com', url: 'https://juyung.com', fullRow: true },
|
||||
{ label: 'GitHub', value: 'git.juyung.com', url: 'https://git.juyung.com' },
|
||||
{ label: 'LinkedIn', value: 'job.juyung.com', url: 'https://job.juyung.com' },
|
||||
],
|
||||
description: `I’m a software engineer. Previously, I worked as a web accessibility developer and taught Java programming. I have a Bachelor's in Computer Science and am currently pursuing a Master’s in Aerospace Engineering.`,
|
||||
// 'Lorem ipsum dolor sit amet, consectetur **adipiscing elit**. In sodales ac dui at *vestibulum*. In condimentum metus id dui tincidunt, in blandit mi [vehicula](/). Nulla lacinia, erat sit amet elementum vulputate, lectus mauris volutpat mi, vitae accumsan metus elit ut nunc. Vestibulum lacinia enim eget eros fermentum scelerisque. Proin augue leo, posuere ut imperdiet vitae, fermentum eu ipsum. Sed sed neque sagittis, posuere urna nec, commodo leo. Pellentesque posuere justo vitae massa volutpat maximus.',
|
||||
tags: [], //[{ name: 'Open for freelance' }, { name: 'Available for mentoring' }, { name: 'Working on side project' }],
|
||||
// action: {
|
||||
// label: 'Download Resume',
|
||||
// url: '/cv.pdf',
|
||||
// downloadedFileName: 'Resume-Juyoung.pdf',
|
||||
// },
|
||||
links: [
|
||||
/*facebook({ url: '#' }),*/
|
||||
website({ url: 'https://juyung.com' }),
|
||||
github({ url: 'https://git.juyung.com' }),
|
||||
linkedin({ url: 'https://job.juyung.com' }),
|
||||
], // twitter({ url: '#' })],
|
||||
} as const satisfies ReadonlyDeep<MainSection>;
|
||||
|
||||
export default mainSectionData;
|
||||
27
src/data/sections/membership-section.data.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import type { MembershipSection } from '@/types/sections/membership-section.types';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
|
||||
const membershipSectionData = {
|
||||
config: {
|
||||
title: 'Memberships',
|
||||
slug: 'memberships',
|
||||
icon: 'fa6-solid:star',
|
||||
visible: true,
|
||||
screenshots: {
|
||||
title: 'Screenshots',
|
||||
icon: 'fa6-solid:images',
|
||||
},
|
||||
},
|
||||
memberships: [
|
||||
{
|
||||
organization: 'Mensa',
|
||||
image: import('@/assets/logos/mensa.png'),
|
||||
dates: [new Date('2026'), null],
|
||||
description: 'High IQ society',
|
||||
links: [],
|
||||
screenshots: [{ src: import('@/assets/portfolio/mensa-letter.png'), alt: 'Mensa Letter' }],
|
||||
},
|
||||
],
|
||||
} as const satisfies ReadonlyDeep<MembershipSection>;
|
||||
|
||||
export default membershipSectionData;
|
||||
90
src/data/sections/portfolio-section.data.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import type { PortfolioSection } from '@/types/sections/portfolio-section.types';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import { demo, github, mockups, website } from '../helpers/links';
|
||||
import {
|
||||
chakraUi,
|
||||
eslint,
|
||||
firebase,
|
||||
jest,
|
||||
nestJs,
|
||||
nextJs,
|
||||
nx,
|
||||
pnpm,
|
||||
postgreSql,
|
||||
prettier,
|
||||
react,
|
||||
sass,
|
||||
tailwindCss,
|
||||
typescript,
|
||||
} from '../helpers/skills';
|
||||
|
||||
const portfolioSectionData = {
|
||||
config: {
|
||||
title: 'Certifications',
|
||||
slug: 'certifications',
|
||||
icon: 'fa6-solid:rocket',
|
||||
visible: true,
|
||||
screenshots: {
|
||||
title: 'Screenshots',
|
||||
icon: 'fa6-solid:images',
|
||||
},
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'Trusted Tester',
|
||||
image: import('@/assets/portfolio/tt.png'),
|
||||
dates: [new Date('2022-08-02'), null],
|
||||
description: 'U.S. Department of Homeland Security',
|
||||
details: [
|
||||
{ label: 'Credential ID', value: 'TT-2208-03281' },
|
||||
// { label: 'Company', value: 'None' },
|
||||
// { label: 'Category', value: ['Web app', 'Open source'] },
|
||||
],
|
||||
pdfDetails: [
|
||||
// { label: 'Demo', value: 'https://golden-bulls-d73jd7.netlify.app', url: '#' },
|
||||
// { label: 'Repository', value: 'https://github.com/mark-freeman/golden-bulls', url: '#' },
|
||||
],
|
||||
screenshots: [
|
||||
{ src: import('@/assets/portfolio/tt-certificate.png'), alt: 'Trusted Tester Certification' },
|
||||
// { src: import('@/assets/portfolio/project-1-screenshot-2.jpg'), alt: 'Second screenshot' },
|
||||
// { src: import('@/assets/portfolio/project-1-screenshot-3.jpg'), alt: 'Third screenshot' },
|
||||
],
|
||||
//description: '',
|
||||
// 'In tristique vulputate augue vel egestas. Quisque ac imperdiet tortor, at lacinia ex. Duis vel ex hendrerit, commodo odio sed, aliquam enim. Ut arcu nulla, tincidunt eget arcu eget, molestie vulputate nisi. Nunc malesuada leo et est iaculis facilisis.',
|
||||
tagsList: {
|
||||
title: '', //'Technologies',
|
||||
tags: [], //[nextJs(), sass(), pnpm(), eslint(), prettier()],
|
||||
},
|
||||
links: [], //[mockups({ url: '#' }), demo({ url: '#' })],
|
||||
},
|
||||
{
|
||||
name: 'JAWS Certified',
|
||||
image: import('@/assets/portfolio/jaws.png'),
|
||||
dates: [new Date('2022-08-02'), null],
|
||||
description: 'Freedom Scientific',
|
||||
details: [
|
||||
//{ label: 'Credential ID', value: 'TT-2208-03281' },
|
||||
// { label: 'Company', value: 'None' },
|
||||
// { label: 'Category', value: ['Web app', 'Open source'] },
|
||||
],
|
||||
pdfDetails: [
|
||||
// { label: 'Demo', value: 'https://golden-bulls-d73jd7.netlify.app', url: '#' },
|
||||
// { label: 'Repository', value: 'https://github.com/mark-freeman/golden-bulls', url: '#' },
|
||||
],
|
||||
screenshots: [
|
||||
{ src: import('@/assets/portfolio/jaws-certificate.png'), alt: 'JAWS Cerficiation' },
|
||||
// { src: import('@/assets/portfolio/project-1-screenshot-2.jpg'), alt: 'Second screenshot' },
|
||||
// { src: import('@/assets/portfolio/project-1-screenshot-3.jpg'), alt: 'Third screenshot' },
|
||||
],
|
||||
//description: '',
|
||||
// 'In tristique vulputate augue vel egestas. Quisque ac imperdiet tortor, at lacinia ex. Duis vel ex hendrerit, commodo odio sed, aliquam enim. Ut arcu nulla, tincidunt eget arcu eget, molestie vulputate nisi. Nunc malesuada leo et est iaculis facilisis.',
|
||||
tagsList: {
|
||||
title: '', //'Technologies',
|
||||
tags: [], //[nextJs(), sass(), pnpm(), eslint(), prettier()],
|
||||
},
|
||||
links: [], //[mockups({ url: '#' }), demo({ url: '#' })],
|
||||
},
|
||||
],
|
||||
} as const satisfies ReadonlyDeep<PortfolioSection>;
|
||||
|
||||
export default portfolioSectionData;
|
||||
1
src/env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="@astrojs/image/client" />
|
||||
36
src/pages/index.astro
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
import Layout from '@/web/components/layout.astro';
|
||||
import Sidebar from '@/web/components/sidebar.astro';
|
||||
import ThemeToggle from '@/web/components/theme-toggle.astro';
|
||||
import MainSection from '@/web/sections/main/main-section.web.astro';
|
||||
import SkillsSection from '@/web/sections/skills/skills-section.web.astro';
|
||||
import ExperienceSection from '@/web/sections/experience/experience-section.web.astro';
|
||||
import PortfolioSection from '@/web/sections/portfolio/portfolio-section.web.astro';
|
||||
import EducationSection from '@/web/sections/education/education-section.web.astro';
|
||||
import MembershipSection from '@/web/sections/membership/membership-section.web.astro';
|
||||
import TestimonialsSection from '@/web/sections/testimonials/testimonials-section.web.astro';
|
||||
import FavoritesSection from '@/web/sections/favorites/favorites-section.web.astro';
|
||||
import cv from '@/data';
|
||||
|
||||
const { config, sections } = cv();
|
||||
---
|
||||
|
||||
<Layout {...config} sections={sections}>
|
||||
<ThemeToggle />
|
||||
<main class="w-full max-w-5xl space-y-4 px-2 py-3 sm:space-y-6 sm:px-8 sm:py-12 lg:space-y-8 lg:py-20">
|
||||
<MainSection {...sections.main} />
|
||||
<!--
|
||||
<SkillsSection {...sections.skills} />
|
||||
-->
|
||||
<ExperienceSection {...sections.experience} />
|
||||
<PortfolioSection {...sections.portfolio} />
|
||||
<EducationSection {...sections.education} />
|
||||
{sections.membership && <MembershipSection {...sections.membership} />}
|
||||
<!--
|
||||
<TestimonialsSection {...sections.testimonials} />
|
||||
<FavoritesSection {...sections.favorites} />
|
||||
-->
|
||||
</main>
|
||||
<Sidebar sections={sections} className="sticky top-8 mt-20" />
|
||||
<script src="../web/scripts/initialize-tooltips.ts"></script>
|
||||
</Layout>
|
||||
33
src/pages/pdf.astro
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
import Fonts from '@/components/fonts.astro';
|
||||
import Footer from '@/pdf/components/footer.astro';
|
||||
import EducationSection from '@/pdf/sections/education-section.pdf.astro';
|
||||
import ExperienceSection from '@/pdf/sections/experience-section.pdf.astro';
|
||||
import MainSection from '@/pdf/sections/main-section.pdf.astro';
|
||||
import PortfolioSection from '@/pdf/sections/portfolio-section.pdf.astro';
|
||||
import SkillsSection from '@/pdf/sections/skills-section.pdf.astro';
|
||||
|
||||
import cv from '@/data';
|
||||
|
||||
const { config, sections } = cv();
|
||||
|
||||
const shouldRenderSection = (section: keyof typeof sections) => sections[section] && sections[section].config.visible;
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang={config.i18n.locale.code}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>PDF preview</title>
|
||||
<Fonts />
|
||||
</head>
|
||||
<body class="flex flex-col bg-white p-[10mm] print:p-0">
|
||||
{shouldRenderSection('main') && <MainSection {...sections.main} />}
|
||||
<!-- {shouldRenderSection('skills') && <SkillsSection {...sections.skills} />} -->
|
||||
{shouldRenderSection('experience') && <ExperienceSection {...sections.experience} />}
|
||||
{shouldRenderSection('portfolio') && <PortfolioSection {...sections.portfolio} />}
|
||||
{shouldRenderSection('education') && <EducationSection {...sections.education} />}
|
||||
{config.pdf?.footer && <Footer>{config.pdf.footer}</Footer>}
|
||||
</body>
|
||||
</html>
|
||||
3
src/pdf/components/dashed-divider.astro
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div class="flex gap-1.5 overflow-hidden py-5 last:hidden">
|
||||
{[...Array(40).keys()].map(() => <hr class="min-w-[12px] bg-gray-200" />)}
|
||||
</div>
|
||||
20
src/pdf/components/date-range-tag.astro
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
import type { DateRange } from '@/types/shared';
|
||||
import formatDateRange from '@/utils/format-date-range';
|
||||
|
||||
export interface Props {
|
||||
class?: string;
|
||||
dates: DateRange;
|
||||
}
|
||||
|
||||
const { dates, ...props } = Astro.props;
|
||||
---
|
||||
|
||||
<div
|
||||
class:list={[
|
||||
'flex h-6 w-fit items-center whitespace-nowrap rounded bg-gray-800 px-2.5 text-sm font-medium text-white',
|
||||
props.class,
|
||||
]}
|
||||
>
|
||||
{formatDateRange(dates)}
|
||||
</div>
|
||||
12
src/pdf/components/description.astro
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
import Description from '@/components/description.astro';
|
||||
|
||||
export interface Props {
|
||||
content: string;
|
||||
}
|
||||
|
||||
const { content } = Astro.props;
|
||||
const baseClass = /* tw */ 'text-base font-normal text-gray-500';
|
||||
---
|
||||
|
||||
<Description content={content} classList={[baseClass]} />
|
||||
6
src/pdf/components/footer.astro
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<div
|
||||
id="footer"
|
||||
class="mt-4 flex w-full justify-center rounded border border-gray-100 bg-gray-50 px-2 py-1 text-justify text-[11px]"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
24
src/pdf/components/labelled-value.astro
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
import type { LabelledValue } from '@/types/shared';
|
||||
|
||||
export interface Props extends LabelledValue {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { label, value, url, ...props } = Astro.props;
|
||||
|
||||
const parsedValue = Array.isArray(value) ? value.join(', ') : value;
|
||||
---
|
||||
|
||||
<div class:list={['flex gap-1 text-base font-normal text-gray-500', props.class]}>
|
||||
<div class="text-base font-medium text-gray-700">{label}:</div>
|
||||
{
|
||||
url ? (
|
||||
<a href={url} class="underline">
|
||||
{parsedValue}
|
||||
</a>
|
||||
) : (
|
||||
<div>{parsedValue}</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
20
src/pdf/components/list-item-heading.astro
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
import type { DateRange } from '@/types/shared';
|
||||
import DateRangeTag from './date-range-tag.astro';
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
dates: DateRange;
|
||||
}
|
||||
|
||||
const { title, subtitle, dates } = Astro.props;
|
||||
---
|
||||
|
||||
<div>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="text-lg font-extrabold text-gray-900">{title}</div>
|
||||
<DateRangeTag class="mt-0.5" dates={dates} />
|
||||
</div>
|
||||
{subtitle && <div class="text-md -mt-0.5 font-medium text-gray-700">{subtitle}</div>}
|
||||
</div>
|
||||
24
src/pdf/components/photo.astro
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
import type { Photo } from '@/types/shared';
|
||||
import { Image } from '@astrojs/image/components';
|
||||
|
||||
export interface Props {
|
||||
src: Photo;
|
||||
alt: string;
|
||||
class?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const { src, ...props } = Astro.props;
|
||||
|
||||
const isRemoteImage = typeof src === 'string';
|
||||
---
|
||||
|
||||
{
|
||||
isRemoteImage ? (
|
||||
<img src={src} {...props} />
|
||||
) : (
|
||||
<Image format="webp" fit="cover" loading="eager" src={src} {...props} />
|
||||
)
|
||||
}
|
||||
4
src/pdf/components/section-heading.astro
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<div class="flex items-center gap-4 pb-5 pt-10">
|
||||
<div class="whitespace-nowrap text-2xl font-extrabold text-gray-900"><slot /></div>
|
||||
<hr class="w-full bg-gray-300" />
|
||||
</div>
|
||||
12
src/pdf/components/tags-list.astro
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
import type { TagsList } from '@/types/shared';
|
||||
|
||||
export interface Props extends TagsList {}
|
||||
|
||||
const { tags, title } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="text-base">
|
||||
<span class="font-medium text-gray-700">{title}:</span>
|
||||
<span class="font-normal text-gray-500">{tags.map((t) => t.name).join(', ')}</span>
|
||||
</div>
|
||||
28
src/pdf/sections/education-section.pdf.astro
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
import type { EducationSection } from '@/types/sections/education-section.types';
|
||||
import DashedDivider from '../components/dashed-divider.astro';
|
||||
import Description from '../components/description.astro';
|
||||
import ListItemHeading from '../components/list-item-heading.astro';
|
||||
import SectionHeading from '../components/section-heading.astro';
|
||||
|
||||
export interface Props extends EducationSection {}
|
||||
|
||||
const { config, diplomas } = Astro.props;
|
||||
---
|
||||
|
||||
<div>
|
||||
<SectionHeading>{config.title}</SectionHeading>
|
||||
<div class="flex flex-col">
|
||||
{
|
||||
diplomas.map(({ title, description, institution, dates }) => () => (
|
||||
<>
|
||||
<div class="flex flex-col gap-2">
|
||||
<ListItemHeading title={title} subtitle={institution} dates={dates} />
|
||||
<Description content={description} />
|
||||
</div>
|
||||
<DashedDivider />
|
||||
</>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
30
src/pdf/sections/experience-section.pdf.astro
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
import type { ExperienceSection } from '@/types/sections/experience-section.types';
|
||||
import DashedDivider from '../components/dashed-divider.astro';
|
||||
import Description from '../components/description.astro';
|
||||
import ListItemHeading from '../components/list-item-heading.astro';
|
||||
import SectionHeading from '../components/section-heading.astro';
|
||||
import TagsList from '../components/tags-list.astro';
|
||||
|
||||
export interface Props extends ExperienceSection {}
|
||||
|
||||
const { config, jobs } = Astro.props;
|
||||
---
|
||||
|
||||
<div>
|
||||
<SectionHeading>{config.title}</SectionHeading>
|
||||
<div class="flex flex-col">
|
||||
{
|
||||
jobs.map(({ company, role, description, tagsList, dates }) => () => (
|
||||
<>
|
||||
<div class="flex flex-col gap-2">
|
||||
<ListItemHeading title={role} subtitle={company} dates={dates} />
|
||||
<Description content={description} />
|
||||
<TagsList {...tagsList} />
|
||||
</div>
|
||||
<DashedDivider />
|
||||
</>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
28
src/pdf/sections/main-section.pdf.astro
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
import type { MainSection } from '@/types/sections/main-section.types';
|
||||
import Photo from '@/components/photo.astro';
|
||||
import Description from '@/pdf/components/description.astro';
|
||||
import LabelledValue from '@/pdf/components/labelled-value.astro';
|
||||
|
||||
export interface Props extends MainSection {}
|
||||
|
||||
const { image, fullName, role, details, pdfDetails, description } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex gap-6">
|
||||
<Photo src={image} alt={fullName} class="h-40 max-h-[160px] w-40 max-w-[160px] rounded-2xl" />
|
||||
<div>
|
||||
<div class="text-3xl font-extrabold text-gray-900">{fullName}</div>
|
||||
<div class="text-lg font-medium text-gray-700">{role}</div>
|
||||
<div class="grid grid-cols-[auto_auto] gap-x-4 gap-y-1 pt-4">
|
||||
{
|
||||
(pdfDetails ?? details).map((detail) => (
|
||||
<LabelledValue {...detail} class={detail.fullRow ? 'col-span-2' : undefined} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Description content={description} />
|
||||
</div>
|
||||
36
src/pdf/sections/portfolio-section.pdf.astro
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
import type { PortfolioSection } from '@/types/sections/portfolio-section.types';
|
||||
import DashedDivider from '../components/dashed-divider.astro';
|
||||
import Description from '../components/description.astro';
|
||||
import LabelledValue from '../components/labelled-value.astro';
|
||||
import ListItemHeading from '../components/list-item-heading.astro';
|
||||
import SectionHeading from '../components/section-heading.astro';
|
||||
import TagsList from '../components/tags-list.astro';
|
||||
|
||||
export interface Props extends PortfolioSection {}
|
||||
|
||||
const { config, projects } = Astro.props;
|
||||
---
|
||||
|
||||
<div>
|
||||
<SectionHeading>{config.title}</SectionHeading>
|
||||
<div class="flex flex-col">
|
||||
{
|
||||
projects.map(({ name, description, details, pdfDetails, tagsList, dates }) => () => (
|
||||
<>
|
||||
<div class="flex flex-col gap-2">
|
||||
<ListItemHeading title={name} dates={dates} />
|
||||
<Description content={description} />
|
||||
<div class="flex flex-col gap-1">
|
||||
{(pdfDetails ?? details).map((detail) => (
|
||||
<LabelledValue {...detail} />
|
||||
))}
|
||||
</div>
|
||||
<TagsList {...tagsList} />
|
||||
</div>
|
||||
<DashedDivider />
|
||||
</>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
48
src/pdf/sections/skills-section.pdf.astro
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
import type { SkillsSection } from '@/types/sections/skills-section.types';
|
||||
import SectionHeading from '../components/section-heading.astro';
|
||||
|
||||
export interface Props extends SkillsSection {}
|
||||
|
||||
const { config, skillSets } = Astro.props;
|
||||
---
|
||||
|
||||
<div>
|
||||
<SectionHeading>{config.title}</SectionHeading>
|
||||
<div class="flex flex-col gap-5">
|
||||
{
|
||||
skillSets.map((skillSet) => (
|
||||
<div>
|
||||
<div class="text-base font-extrabold text-gray-900">{skillSet.title}</div>
|
||||
<div class="mt-2 flex flex-wrap gap-3.5 text-sm text-gray-700">
|
||||
{skillSet.skills.map((skill) => {
|
||||
if ('level' in skill) {
|
||||
return (
|
||||
<div class="flex h-6 w-fit overflow-hidden rounded">
|
||||
<div class="flex items-center bg-gray-100 pl-2.5 pr-2 font-medium">{skill.name}</div>
|
||||
<div class="flex items-center bg-gray-200 pl-2 pr-2.5 font-normal">{skill.level}/5</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (skill.name.includes(' - ')) {
|
||||
return (
|
||||
<div class="flex h-6 w-fit overflow-hidden rounded">
|
||||
<div class="flex items-center bg-gray-100 pl-2.5 pr-2 font-medium">
|
||||
{skill.name.split(' - ')[0]}
|
||||
</div>
|
||||
<div class="flex items-center bg-gray-200 pl-2 pr-2.5 font-normal">
|
||||
{skill.name.split(' - ')[1]}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div class="flex h-6 w-fit items-center rounded bg-gray-100 px-2.5 font-medium">{skill.name}</div>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
23
src/types/config/i18n-config.types.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { Locale } from 'date-fns';
|
||||
|
||||
export interface I18nConfig {
|
||||
/**
|
||||
* Language code used for date formatting, translations, and value of the page `lang` attribute.
|
||||
*/
|
||||
locale: Locale;
|
||||
|
||||
/**
|
||||
* Date format used when displaying date ranges in some sections.
|
||||
*/
|
||||
dateFormat: string;
|
||||
|
||||
/**
|
||||
* List of translations used in the application.
|
||||
*/
|
||||
translations: {
|
||||
/**
|
||||
* Used in date ranges to represent the current date.
|
||||
*/
|
||||
now: string;
|
||||
};
|
||||
}
|
||||
55
src/types/config/meta-config.types.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
export interface MetaConfig {
|
||||
/**
|
||||
* [WEB] Page title.
|
||||
*
|
||||
* Displayed as browser tab title and in search results.
|
||||
* It's recommended to keep it between 30 and 60 characters.
|
||||
*
|
||||
* @see https://www.screamingfrog.co.uk/learn-seo/page-title
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* [WEB] Page description.
|
||||
*
|
||||
* Displayed under the title in search results.
|
||||
* It's recommended to keep it between 70 and 155 characters.
|
||||
*
|
||||
* @see https://www.screamingfrog.co.uk/learn-seo/meta-description
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* [WEB] Absolute path to the image used for favicons generation.
|
||||
*
|
||||
* Specified icon will be displayed next to the page title in browser tab.
|
||||
*/
|
||||
faviconPath: string;
|
||||
|
||||
/**
|
||||
* [WEB] Title used in open graph links.
|
||||
*
|
||||
* If not specified, the title property will be used.
|
||||
*
|
||||
* @see https://ahrefs.com/blog/open-graph-meta-tags
|
||||
*/
|
||||
ogTitle?: string;
|
||||
|
||||
/**
|
||||
* [WEB] Description used in open graph links.
|
||||
*
|
||||
* If not specified, the description property will be used.
|
||||
*
|
||||
* @see https://ahrefs.com/blog/open-graph-meta-tags
|
||||
*/
|
||||
ogDescription?: string;
|
||||
|
||||
/**
|
||||
* [WEB] Image used in open graph links.
|
||||
*
|
||||
* It's recommended to keep it between 30 and 60 characters.
|
||||
*
|
||||
* @see https://ahrefs.com/blog/open-graph-meta-tags
|
||||
*/
|
||||
ogImage?: string;
|
||||
}
|
||||
8
src/types/config/pdf-config.types.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export interface PdfConfig {
|
||||
/**
|
||||
* [PDF] Displays footer with specified content on each PDF page.
|
||||
*
|
||||
* You can use it to add the data processing clause.
|
||||
*/
|
||||
footer?: string;
|
||||
}
|
||||
85
src/types/data.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import type { I18nConfig } from './config/i18n-config.types';
|
||||
import type { PdfConfig } from './config/pdf-config.types';
|
||||
import type { MetaConfig } from './config/meta-config.types';
|
||||
import type { EducationSection } from './sections/education-section.types';
|
||||
import type { ExperienceSection } from './sections/experience-section.types';
|
||||
import type { FavoritesSection } from './sections/favorites-section.types';
|
||||
import type { MainSection } from './sections/main-section.types';
|
||||
import type { MembershipSection } from './sections/membership-section.types';
|
||||
import type { PortfolioSection } from './sections/portfolio-section.types';
|
||||
import type { SkillsSection } from './sections/skills-section.types';
|
||||
import type { TestimonialsSection } from './sections/testimonials-section.types';
|
||||
|
||||
export type Config = {
|
||||
/**
|
||||
* [WEB] Page metadata used for SEO and social media sharing.
|
||||
*/
|
||||
meta: MetaConfig;
|
||||
|
||||
/**
|
||||
* Language and date display configuration.
|
||||
*/
|
||||
i18n: I18nConfig;
|
||||
|
||||
/**
|
||||
* [PDF] Configuration of the pdf generation.
|
||||
*/
|
||||
pdf?: PdfConfig;
|
||||
};
|
||||
|
||||
export type Sections = {
|
||||
/**
|
||||
* Basic information about you.
|
||||
*/
|
||||
main: MainSection;
|
||||
|
||||
/**
|
||||
* Grouped lists of your skills.
|
||||
*/
|
||||
skills: SkillsSection;
|
||||
|
||||
/**
|
||||
* Your employment history.
|
||||
*/
|
||||
experience: ExperienceSection;
|
||||
|
||||
/**
|
||||
* Your projects and initiatives.
|
||||
*/
|
||||
portfolio: PortfolioSection;
|
||||
|
||||
/**
|
||||
* Your education degrees and certifications.
|
||||
*/
|
||||
education: EducationSection;
|
||||
|
||||
/**
|
||||
* Your professional memberships and associations.
|
||||
*/
|
||||
membership?: MembershipSection;
|
||||
|
||||
/**
|
||||
* [WEB] Recommendations from your previous employers and people you worked with.
|
||||
*/
|
||||
testimonials: TestimonialsSection;
|
||||
|
||||
/**
|
||||
* [WEB] List of sources you use to gain knowledge and inspiration.
|
||||
*/
|
||||
favorites: FavoritesSection;
|
||||
};
|
||||
|
||||
/**
|
||||
* All data used to generate the cv.
|
||||
*/
|
||||
export interface Data {
|
||||
/**
|
||||
* Global configuration of the web and pdf versions of the cv.
|
||||
*/
|
||||
config: Config;
|
||||
|
||||
/**
|
||||
* Configurations for the particular sections of the cv.
|
||||
*/
|
||||
sections: Sections;
|
||||
}
|
||||
44
src/types/sections/education-section.types.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import type { DateRange, LinkButton, Photo, Section } from '../shared';
|
||||
|
||||
export interface Diploma {
|
||||
/**
|
||||
* Name of the certificate or the degree you got.
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* Name of the institution that issued the certificate or degree.
|
||||
*/
|
||||
institution: string;
|
||||
|
||||
/**
|
||||
* [WEB] Logo of the institution.
|
||||
*
|
||||
* **Ratio**: 1:1
|
||||
*
|
||||
* **Display size**: 56x56px
|
||||
*/
|
||||
image?: Photo;
|
||||
|
||||
/**
|
||||
* Date range when you were studying in the institution.
|
||||
*/
|
||||
dates: DateRange;
|
||||
|
||||
/**
|
||||
* A short overview of your studies. You can use markdown syntax.
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* [WEB] Links related to your studies (e.g. course/university website, link to realized project).
|
||||
*/
|
||||
links: LinkButton[];
|
||||
}
|
||||
|
||||
export interface EducationSection extends Section {
|
||||
/**
|
||||
* List of your diplomas, certificates, .etc. Start with the most recent one.
|
||||
*/
|
||||
diplomas: Diploma[];
|
||||
}
|
||||
50
src/types/sections/experience-section.types.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import type { DateRange, LinkButton, Photo, Section, TagsList } from '../shared';
|
||||
|
||||
export interface Job {
|
||||
/**
|
||||
* Your position in the company.
|
||||
*/
|
||||
role: string;
|
||||
|
||||
/**
|
||||
* Name of the company.
|
||||
*/
|
||||
company: string;
|
||||
|
||||
/**
|
||||
* [WEB] Logo of the company.
|
||||
*
|
||||
* **Ratio**: 1:1
|
||||
*
|
||||
* **Display size**: 56x56px
|
||||
*/
|
||||
image?: Photo;
|
||||
|
||||
/**
|
||||
* Date range when you were working in the company.
|
||||
*/
|
||||
dates: DateRange;
|
||||
|
||||
/**
|
||||
* A short overview of your job. You can use markdown syntax.
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* Any information that you want to highlight.
|
||||
* We recommend to describe the technologies used in the project.
|
||||
*/
|
||||
tagsList: TagsList;
|
||||
|
||||
/**
|
||||
* [WEB] Links related to your job (e.g. production app, company's website, project website).
|
||||
*/
|
||||
links: LinkButton[];
|
||||
}
|
||||
|
||||
export interface ExperienceSection extends Section {
|
||||
/**
|
||||
* List of your jobs in a chronological order. Start with the most recent one.
|
||||
*/
|
||||
jobs: Job[];
|
||||
}
|
||||
129
src/types/sections/favorites-section.types.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import type { Photo, Section } from '../shared';
|
||||
|
||||
export interface Book {
|
||||
/**
|
||||
* [WEB] Book title.
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* [WEB] Book cover.
|
||||
*
|
||||
* **Ratio**: 3:4
|
||||
*
|
||||
* **Display size**: 300x400px
|
||||
*/
|
||||
image: Photo;
|
||||
|
||||
/**
|
||||
* [WEB] Full name of the book author.
|
||||
*/
|
||||
author: string;
|
||||
|
||||
/**
|
||||
* [WEB] Website to buy the book or read more about it.
|
||||
*/
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface Person {
|
||||
/**
|
||||
* [WEB] Full name of the person.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* [WEB] Photo of the person.
|
||||
*
|
||||
* **Ratio**: 1:1
|
||||
*
|
||||
* **Display size**: 200x200px
|
||||
*/
|
||||
image: Photo;
|
||||
|
||||
/**
|
||||
* [WEB] Main website related to the person.
|
||||
*/
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface Video {
|
||||
/**
|
||||
* [WEB] Title of the video.
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* [WEB] Thumbnail of the video.
|
||||
*
|
||||
* **Ratio**: 16:9
|
||||
*
|
||||
* **Display size**: 448x252px
|
||||
*/
|
||||
image: Photo;
|
||||
|
||||
/**
|
||||
* [WEB] Link to the video.
|
||||
*/
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface Media {
|
||||
/**
|
||||
* [WEB] Title of the media.
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* [WEB] Type of the media (e.g. podcast, blog, newsletter, YouTube channel, .etc).
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* [WEB] Logo of the media.
|
||||
*
|
||||
* **Ratio**: 1:1
|
||||
*
|
||||
* **Display size**: 200x200px
|
||||
*/
|
||||
image: Photo;
|
||||
|
||||
/**
|
||||
* [WEB] URL to the main website related to the media.
|
||||
*/
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface SubSection<Data = unknown> {
|
||||
/**
|
||||
* [WEB] Title that will be displayed above the list of items.
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* [WEB] List of items to display within the subsection.
|
||||
*/
|
||||
data: Data[];
|
||||
}
|
||||
|
||||
export interface FavoritesSection extends Section {
|
||||
/**
|
||||
* [WEB] List of your favorite books.
|
||||
*/
|
||||
books?: SubSection<Book>;
|
||||
|
||||
/**
|
||||
* [WEB] List of the people that inspire you.
|
||||
*/
|
||||
people?: SubSection<Person>;
|
||||
|
||||
/**
|
||||
* [WEB] List of the videos you learned the most from.
|
||||
*/
|
||||
videos?: SubSection<Video>;
|
||||
|
||||
/**
|
||||
* [WEB] List of other media types that helps you to growth in your field.
|
||||
*/
|
||||
medias?: SubSection<Media>;
|
||||
}
|
||||
58
src/types/sections/main-section.types.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import type { DownloadButton, Photo, LabelledValue, LinkButton, Section, Tag } from '../shared';
|
||||
|
||||
export interface MainSection extends Section {
|
||||
/**
|
||||
* Your image.
|
||||
*
|
||||
* **Ratio**: 1:1
|
||||
*
|
||||
* **Display size**: 208x208px
|
||||
*/
|
||||
image: Photo;
|
||||
|
||||
/**
|
||||
* Your name.
|
||||
*/
|
||||
fullName: string;
|
||||
|
||||
/**
|
||||
* Your current role.
|
||||
*/
|
||||
role: string;
|
||||
|
||||
/**
|
||||
* Label-value pairs with some key details about you.
|
||||
*
|
||||
* E.g. phone, email, location, expected salary.
|
||||
*/
|
||||
details: LabelledValue[];
|
||||
|
||||
/**
|
||||
* [PDF] Labeled-value pairs that will be used in the PDF version of your resume.
|
||||
*
|
||||
* You can use it to add your social media profiles as those listed under the `links` property aren't used in the PDF.
|
||||
*
|
||||
* If not provided, the `details` will be used instead.
|
||||
*/
|
||||
pdfDetails?: LabelledValue[];
|
||||
|
||||
/**
|
||||
* A short overview of you and your experience.
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* [WEB] Any information that you want to highlight.
|
||||
*/
|
||||
tags: Tag[];
|
||||
|
||||
/**
|
||||
* [WEB] A button that will be used to download your resume.
|
||||
*/
|
||||
action: DownloadButton;
|
||||
|
||||
/**
|
||||
* [WEB] Your social media profiles.
|
||||
*/
|
||||
links: LinkButton[];
|
||||
}
|
||||
75
src/types/sections/membership-section.types.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import type { DateRange, IconName, LinkButton, Photo, Section } from '../shared';
|
||||
|
||||
interface Screenshot {
|
||||
/**
|
||||
* [WEB] Source of the screenshot.
|
||||
*/
|
||||
src: Photo;
|
||||
|
||||
/**
|
||||
* [WEB] Alt text for the screenshot.
|
||||
*/
|
||||
alt: string;
|
||||
}
|
||||
|
||||
export interface Membership {
|
||||
/**
|
||||
* Name of the organization or association you are / were a member of.
|
||||
*/
|
||||
organization: string;
|
||||
|
||||
/**
|
||||
* [WEB] Logo of the organization.
|
||||
*
|
||||
* **Ratio**: 1:1
|
||||
*
|
||||
* **Display size**: 56x56px
|
||||
*/
|
||||
image?: Photo;
|
||||
|
||||
/**
|
||||
* Date range of your membership.
|
||||
*
|
||||
* If the second date is `null`, the membership is ongoing.
|
||||
*/
|
||||
dates: DateRange;
|
||||
|
||||
/**
|
||||
* A short description of the organization or your role. You can use markdown syntax.
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* [WEB] Links related to the membership (e.g. organization website).
|
||||
*/
|
||||
links: LinkButton[];
|
||||
|
||||
/**
|
||||
* [WEB] Screenshots related to the membership (e.g. certificate, badge).
|
||||
*/
|
||||
screenshots?: Screenshot[];
|
||||
}
|
||||
|
||||
export interface MembershipSection extends Section {
|
||||
config: Section['config'] & {
|
||||
/**
|
||||
* [WEB] Configuration of the button that displays membership screenshots.
|
||||
*/
|
||||
screenshots?: {
|
||||
/**
|
||||
* [WEB] Icon displayed within the button.
|
||||
*/
|
||||
icon?: IconName;
|
||||
|
||||
/**
|
||||
* [WEB] Title displayed when hovering the button.
|
||||
*/
|
||||
title?: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* List of your memberships. Start with the most recent one.
|
||||
*/
|
||||
memberships: Membership[];
|
||||
}
|
||||
93
src/types/sections/portfolio-section.types.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import type { DateRange, Photo, LabelledValue, LinkButton, Section, TagsList, IconName } from '../shared';
|
||||
|
||||
interface Screenshot {
|
||||
/**
|
||||
* [WEB] Source of the screenshot.
|
||||
*/
|
||||
src: Photo;
|
||||
|
||||
/**
|
||||
* [WEB] Alt text for the screenshot.
|
||||
*/
|
||||
alt: string;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
/**
|
||||
* Name of the project.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* [WEB] Logo of the project.
|
||||
*
|
||||
* **Ratio**: 1:1
|
||||
*
|
||||
* **Display size**: 56x56px
|
||||
*/
|
||||
image?: Photo;
|
||||
|
||||
/**
|
||||
* Date range when you were working on the project.
|
||||
*/
|
||||
dates: DateRange;
|
||||
|
||||
/**
|
||||
* Label-value pairs with some key details about the project.
|
||||
*/
|
||||
details: LabelledValue[];
|
||||
|
||||
/**
|
||||
* [PDF] Labeled-value pairs that will be used in the PDF version of your resume.
|
||||
*
|
||||
* You can use it to add some links related to your project as those listed under the `links` property aren't used in the PDF.
|
||||
*
|
||||
* If not provided, the `details` will be used instead.
|
||||
*/
|
||||
pdfDetails?: LabelledValue[];
|
||||
|
||||
/**
|
||||
* A short overview of the project. You can use markdown syntax.
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* [WEB] Screenshots of the project.
|
||||
*/
|
||||
screenshots?: Screenshot[];
|
||||
|
||||
/**
|
||||
* Any information that you want to highlight.
|
||||
* We recommend to describe the technologies used in the project.
|
||||
*/
|
||||
tagsList: TagsList;
|
||||
|
||||
/**
|
||||
* [WEB] Links related to your project (e.g. GitHub repository, live demo, mockups).
|
||||
*/
|
||||
links: LinkButton[];
|
||||
}
|
||||
|
||||
export interface PortfolioSection extends Section {
|
||||
/**
|
||||
* List of your projects in a chronological order. Start with the most recent one.
|
||||
*/
|
||||
projects: Project[];
|
||||
|
||||
config: Section['config'] & {
|
||||
/**
|
||||
* [WEB] Configuration of the button that displays project's screenshots.
|
||||
*/
|
||||
screenshots?: {
|
||||
/**
|
||||
* [WEB] Icon displayed within the button.
|
||||
*/
|
||||
icon?: IconName;
|
||||
|
||||
/**
|
||||
* [WEB] Title displayed when hovering the button.
|
||||
*/
|
||||
title?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
34
src/types/sections/skills-section.types.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { Section, Tag } from '../shared';
|
||||
|
||||
export interface Skill extends Tag {}
|
||||
|
||||
export type SkillLevel = 1 | 2 | 3 | 4 | 5;
|
||||
|
||||
export interface LevelledSkill extends Skill {
|
||||
/**
|
||||
* Your level of skill proficiency in a 1-5 scale.
|
||||
*/
|
||||
level: SkillLevel;
|
||||
}
|
||||
|
||||
export interface SkillSet {
|
||||
/**
|
||||
* Title that will be displayed above the list of skills.
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* List of skills with or without levels.
|
||||
*
|
||||
* If you want to displays skills with levels, we recommend to also provide the `description` property.
|
||||
* This way anyone viewing your resume will know what is the meaning of each level.
|
||||
*/
|
||||
skills: Skill[] | LevelledSkill[];
|
||||
}
|
||||
|
||||
export interface SkillsSection extends Section {
|
||||
/**
|
||||
* Grouped lists of your skills.
|
||||
*/
|
||||
skillSets: SkillSet[];
|
||||
}
|
||||
39
src/types/sections/testimonials-section.types.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import type { Photo, LinkButton, Section } from '../shared';
|
||||
|
||||
export interface Testimonial {
|
||||
/**
|
||||
* [WEB] Photo of the testimonial author.
|
||||
*
|
||||
* **Ratio**: 1:1
|
||||
*
|
||||
* **Display size**: 56x56px
|
||||
*/
|
||||
image: Photo;
|
||||
|
||||
/**
|
||||
* [WEB] Full name of the testimonial author.
|
||||
*/
|
||||
author: string;
|
||||
|
||||
/**
|
||||
* [WEB] Your relation to the testimonial author (e.g. supervisor, colleague, subordinate).
|
||||
*/
|
||||
relation: string;
|
||||
|
||||
/**
|
||||
* [WEB] Content of the testimonial. You can use markdown syntax.
|
||||
*/
|
||||
content: string;
|
||||
|
||||
/**
|
||||
* [WEB] Social media (e.g. LinkedIn profile, website) of the testimonial author.
|
||||
*/
|
||||
links: LinkButton[];
|
||||
}
|
||||
|
||||
export interface TestimonialsSection extends Section {
|
||||
/**
|
||||
* [WEB] List of your testimonials in a chronological order. Start with the most recent one.
|
||||
*/
|
||||
testimonials: Testimonial[];
|
||||
}
|
||||