Compare commits
223 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 25021d1b20 | |||
| 53fdfdf8db | |||
| 8a45f2de4d | |||
| 4ccc7bae40 | |||
| 2585c47de8 | |||
| b49798950a | |||
| 03f15f91b3 | |||
| 8e13d9c3ac | |||
| 80932eb80c | |||
| b2ae2c05d8 | |||
| e349fc9bd1 | |||
| 53213dfb26 | |||
| 40f27a53b6 | |||
| 2d900600bf | |||
| 047e9c248f | |||
| 1cc1c39903 | |||
| f6f2a29a7a | |||
| 4368f6a887 | |||
| f6db9fb387 | |||
| afa3804bea | |||
| e2f43b4931 | |||
| 067fdd0921 | |||
| 1296e6bd45 | |||
| d883edb51f | |||
| 51fcf13d37 | |||
| 91ef87b4e0 | |||
| 4c0cc947a2 | |||
| 6fcb7a4845 | |||
| 213f96b189 | |||
| e8d6d4ad3a | |||
| 87370cfdf0 | |||
| 112a644927 | |||
| 2977e3528a | |||
| fd7b5e0dd4 | |||
| 21fd079f94 | |||
| 9bdc61b50d | |||
| 2de24d5b55 | |||
| 1cba9d0fb9 | |||
| 8f532bf4a6 | |||
| 4efdabd475 | |||
| 8803101dcd | |||
| a4a9381b65 | |||
| 618d42019e | |||
| d9b56cfb5b | |||
| 5624dc4f83 | |||
| 313fe47050 | |||
| 772332661f | |||
| 4c6b512455 | |||
| cc576f5220 | |||
| b193329c23 | |||
| 8e1459f092 | |||
| e19b55ff65 | |||
| 687161df98 | |||
| c54cfebf5f | |||
| e8d0ed1370 | |||
| 87a17a5196 | |||
| d19df3389c | |||
| faa4c606e5 | |||
| 210fbd18ac | |||
| 3ba0141ab8 | |||
| 279a2ddaeb | |||
| ea7ee3dd7a | |||
| a55bea1e07 | |||
| ed5cb7f17b | |||
| c7f8daaff3 | |||
| ac48ad78db | |||
| c18e18cce8 | |||
| 65ba13e503 | |||
| 44ec9e1d43 | |||
| 7c53949741 | |||
| 6d37769e38 | |||
| c525f8d2cc | |||
| 04dfcae898 | |||
| 9b5a99a8ca | |||
| c44f1c5282 | |||
| 24dfa99034 | |||
| 35c0177729 | |||
| 098d67cd8c | |||
| d8e0ced54c | |||
| 8e9b409bae | |||
| b995a6b6c0 | |||
| 3e76a52306 | |||
| 4e91a2e2ef | |||
| 4314912d5a | |||
| 93da5157ff | |||
| cc7bc4ffb8 | |||
| 78eefe9da1 | |||
| 7f7c4acdcb | |||
| a23dbfa3df | |||
| 249104e7a3 | |||
| 76bbe7de6b | |||
| 1994dde1f2 | |||
| a8626e400d | |||
| f5136da681 | |||
| 8efc243e43 | |||
| cd21860535 | |||
| 7054623678 | |||
| 37781d51f3 | |||
| ec4e43d4fc | |||
| 1d4529128f | |||
| 60ed3e2a8d | |||
| 5b67e7c0b4 | |||
| 1399d3c44b | |||
| eb543cf32d | |||
| 9c6d9833d6 | |||
| f50cbd71b7 | |||
| 3d5b3db321 | |||
| e438602773 | |||
| f42b29e4ac | |||
| 92995d9c2b | |||
| b21f1648c4 | |||
| 94c04b44df | |||
| 54ed0678bf | |||
| 817ec96963 | |||
| 8ad5458e2a | |||
| a87c5edd60 | |||
| e4327736bd | |||
| 1fddbe5f92 | |||
| 2c482e7df8 | |||
| c8edcd3dad | |||
| a82c25c7cb | |||
| 73b423030f | |||
| 809551d0f8 | |||
| e795ec64d6 | |||
| f8373e4798 | |||
| a31c434fbc | |||
| 1fa8aae80a | |||
| 27b60a4df9 | |||
| d21983aab4 | |||
| 9406d78653 | |||
| c7ae0e94d7 | |||
| 308a8e3ae3 | |||
| 4c90cc1838 | |||
| 460a40711e | |||
| 18cf814779 | |||
| a9656afbbf | |||
| 385fe008ce | |||
| 7e25e853d7 | |||
| de5adbe3d2 | |||
| c239ae3f49 | |||
| 1bfdff5b30 | |||
| 63db927924 | |||
| 9a34e4af27 | |||
| b5589338ec | |||
| a19059aa76 | |||
| 15f962310b | |||
| a32def2086 | |||
| 21af624096 | |||
| 227870ac78 | |||
| 33cb3dbd6a | |||
| eb7813ac6f | |||
| 0f8f2fe560 | |||
| 51f38f0884 | |||
| 6b93fd179d | |||
| 9385f36832 | |||
| a5dc15dc08 | |||
| eab996f7e7 | |||
| 43c5a33773 | |||
| 7fb0226ddc | |||
| db6e7a7480 | |||
| 6335ad1571 | |||
| 2d62504895 | |||
| e34d0cebe5 | |||
| 0053d696ff | |||
| 6fb0a72a56 | |||
| 5b3e91e34d | |||
| d1a5a41e4d | |||
| d0a07686a5 | |||
| ffa4747ed6 | |||
| b963813910 | |||
| 58950ed0ef | |||
| 1c7a6c952f | |||
| 3ae651fece | |||
| bd52983780 | |||
| 39daed3502 | |||
| dda47f51ec | |||
| 374b9bcc58 | |||
| f0b18019d5 | |||
| 5c10f3d866 | |||
| 5d839e5420 | |||
| febbdefc0b | |||
| 6110440682 | |||
| d0a174d7b7 | |||
| 6708570c49 | |||
| bb4bbf4174 | |||
| a3ef6520e7 | |||
| 0fec5ce86d | |||
| 26e34b6b83 | |||
| 007243f2c3 | |||
| 9d0a0bba86 | |||
| 89a44cc33a | |||
| 92856b6f06 | |||
| c968188080 | |||
| db36bc9770 | |||
| 35df043f66 | |||
| 3c4a26bc06 | |||
| 62046c49b5 | |||
| 3b73dcf29d | |||
| 9b20c46348 | |||
| 996ef650db | |||
| 1c36ac1d68 | |||
| 198c269790 | |||
| 58ef309b68 | |||
| 54bace451c | |||
| 193b15edc1 | |||
| e4ec678512 | |||
| 29c18c1e89 | |||
| 1c5c4d0117 | |||
| d6fee1e3a6 | |||
| d84aeee968 | |||
| c87242142d | |||
| 2c84976e28 | |||
| 31ed9f41a0 | |||
| 700b98fcb7 | |||
| 3f01a9e58e | |||
| b9de35f0d9 | |||
| 5827576ffb | |||
| acc9becf1a | |||
| c2837838ee | |||
| 304fd93ece | |||
| 491bbcadcc | |||
| 5bc0230a5a | |||
| c9a2c27b2d |
11
.env.example
@ -68,3 +68,14 @@ STORAGE_SKIP_BUCKET_CHECK=false
|
|||||||
# GOOGLE_CLIENT_ID=
|
# GOOGLE_CLIENT_ID=
|
||||||
# GOOGLE_CLIENT_SECRET=
|
# GOOGLE_CLIENT_SECRET=
|
||||||
# GOOGLE_CALLBACK_URL=http://localhost:5173/api/auth/google/callback
|
# GOOGLE_CALLBACK_URL=http://localhost:5173/api/auth/google/callback
|
||||||
|
|
||||||
|
# OpenID (Optional)
|
||||||
|
# VITE_OPENID_NAME=
|
||||||
|
# OPENID_AUTHORIZATION_URL=
|
||||||
|
# OPENID_CALLBACK_URL=http://localhost:5173/api/auth/openid/callback
|
||||||
|
# OPENID_CLIENT_ID=
|
||||||
|
# OPENID_CLIENT_SECRET=
|
||||||
|
# OPENID_ISSUER=
|
||||||
|
# OPENID_SCOPE=openid profile email
|
||||||
|
# OPENID_TOKEN_URL=
|
||||||
|
# OPENID_USER_INFO_URL=
|
||||||
|
|||||||
@ -5,10 +5,10 @@
|
|||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
"extends": ["plugin:prettier/recommended"],
|
|
||||||
"plugins": ["simple-import-sort", "unused-imports"],
|
"plugins": ["simple-import-sort", "unused-imports"],
|
||||||
"rules": {
|
"rules": {
|
||||||
// eslint
|
// eslint
|
||||||
|
"no-console": "error",
|
||||||
"no-return-await": "off",
|
"no-return-await": "off",
|
||||||
|
|
||||||
// simple-import-sort
|
// simple-import-sort
|
||||||
@ -41,14 +41,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
|
||||||
|
|
||||||
// prettier
|
|
||||||
"prettier/prettier": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
"endOfLine": "auto"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -77,6 +69,7 @@
|
|||||||
"@typescript-eslint/no-misused-promises": "off",
|
"@typescript-eslint/no-misused-promises": "off",
|
||||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||||
|
"@typescript-eslint/consistent-type-imports": "error",
|
||||||
"@typescript-eslint/restrict-template-expressions": "off",
|
"@typescript-eslint/restrict-template-expressions": "off",
|
||||||
"@typescript-eslint/no-redundant-type-constituents": "off",
|
"@typescript-eslint/no-redundant-type-constituents": "off",
|
||||||
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
|
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
|
||||||
|
|||||||
1
.github/.well-known/funding-manifest-urls
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
https://rxresu.me/funding.json
|
||||||
3
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@ -76,7 +76,10 @@ body:
|
|||||||
- Bronzor
|
- Bronzor
|
||||||
- Chikorita
|
- Chikorita
|
||||||
- Ditto
|
- Ditto
|
||||||
|
- Gengar
|
||||||
|
- Glalie
|
||||||
- Kakuna
|
- Kakuna
|
||||||
|
- Leafish
|
||||||
- Nosepass
|
- Nosepass
|
||||||
- Onyx
|
- Onyx
|
||||||
- Pikachu
|
- Pikachu
|
||||||
|
|||||||
20
.github/workflows/lint-test-build.yml
vendored
@ -19,33 +19,27 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.2.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v3.0.0
|
uses: pnpm/action-setup@v4.0.0
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.1.0
|
||||||
with:
|
with:
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
node-version: 20.17.0
|
node-version: 22
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: pnpm lint
|
run: pnpm run lint
|
||||||
|
|
||||||
- name: Format
|
|
||||||
run: pnpm format:check
|
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: pnpm test
|
run: pnpm run test
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: pnpm build
|
run: pnpm run build
|
||||||
env:
|
|
||||||
NODE_ENV: production
|
|
||||||
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
|
||||||
|
|||||||
8
.github/workflows/publish-docker-image.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
platform:
|
platform:
|
||||||
- linux/amd64
|
- linux/amd64
|
||||||
# - linux/arm64 # Uncomment this if you want to build for ARM64, disabled by default due to slower build times
|
- linux/arm64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
@ -152,7 +152,11 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
- uses: sarisia/actions-status-discord@v1.14.3
|
- name: Deploy the latest image on rxresu.me
|
||||||
|
run: curl -kX POST ${{ secrets.SERVICE_WEBHOOK }}
|
||||||
|
|
||||||
|
- name: Inform about the release on Discord
|
||||||
|
uses: sarisia/actions-status-discord@v1.14.3
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
username: ReleaseBot
|
username: ReleaseBot
|
||||||
|
|||||||
1
.gitignore
vendored
@ -40,6 +40,7 @@ Thumbs.db
|
|||||||
# Generated Files
|
# Generated Files
|
||||||
.nx
|
.nx
|
||||||
.swc
|
.swc
|
||||||
|
.turbo
|
||||||
fly.toml
|
fly.toml
|
||||||
stats.html
|
stats.html
|
||||||
|
|
||||||
|
|||||||
11
.ncurc.json
@ -1,7 +1,16 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/raineorshine/npm-check-updates/main/src/types/RunOptions.json",
|
"$schema": "https://raw.githubusercontent.com/raineorshine/npm-check-updates/main/src/types/RunOptions.json",
|
||||||
"upgrade": true,
|
"upgrade": true,
|
||||||
|
"target": "minor",
|
||||||
"install": "always",
|
"install": "always",
|
||||||
"packageManager": "pnpm",
|
"packageManager": "pnpm",
|
||||||
"reject": ["eslint", "eslint-plugin-unused-imports", "@reactive-resume/*"]
|
"reject": [
|
||||||
|
"nx",
|
||||||
|
"eslint",
|
||||||
|
"@nx/*",
|
||||||
|
"@swc/*",
|
||||||
|
"@swc-node/*",
|
||||||
|
"@reactive-resume/*",
|
||||||
|
"eslint-plugin-unused-imports"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"endOfLine": "auto"
|
"endOfLine": "auto",
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss"],
|
||||||
|
"tailwindFunctions": ["cn", "cva"]
|
||||||
}
|
}
|
||||||
|
|||||||
2
.vscode/extensions.json
vendored
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": ["nrwl.angular-console", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
|
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
|
||||||
}
|
}
|
||||||
|
|||||||
12
.vscode/settings.json
vendored
@ -1,15 +1,9 @@
|
|||||||
{
|
{
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"css.validate": false,
|
||||||
|
"i18n-ally.localesPaths": ["apps/client/src/locales"],
|
||||||
"tailwindCSS.experimental.classRegex": [
|
"tailwindCSS.experimental.classRegex": [
|
||||||
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
|
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
|
||||||
["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
||||||
],
|
],
|
||||||
"yaml.schemas": {
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
"https://json.schemastore.org/github-workflow.json": ".github/workflows/*.yml",
|
|
||||||
"https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json": [
|
|
||||||
"tools/compose/*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"i18n-ally.localesPaths": ["apps/client/src/locales"],
|
|
||||||
"vitest.disableWorkspaceWarning": true
|
|
||||||
}
|
}
|
||||||
|
|||||||
132
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||||
|
identity and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
- Demonstrating empathy and kindness toward other people
|
||||||
|
- Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
- Giving and gracefully accepting constructive feedback
|
||||||
|
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
- Focusing on what is best not just for us as individuals, but for the overall
|
||||||
|
community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
- The use of sexualized language or imagery, and sexual attention or advances of
|
||||||
|
any kind
|
||||||
|
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
- Public or private harassment
|
||||||
|
- Publishing others' private information, such as a physical or email address,
|
||||||
|
without their explicit permission
|
||||||
|
- Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official email address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
[INSERT CONTACT METHOD].
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series of
|
||||||
|
actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or permanent
|
||||||
|
ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||||
|
community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.1, available at
|
||||||
|
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||||
|
[https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||||
|
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||||
|
[FAQ]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
||||||
@ -9,7 +9,7 @@ To run the development environment of the application locally on your computer,
|
|||||||
#### Requirements
|
#### Requirements
|
||||||
|
|
||||||
- Docker (with Docker Compose)
|
- Docker (with Docker Compose)
|
||||||
- Node.js 18 or higher (with pnpm)
|
- Node.js 20 or higher (with pnpm)
|
||||||
|
|
||||||
### 1. Fork and Clone the Repository
|
### 1. Fork and Clone the Repository
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ ARG NX_CLOUD_ACCESS_TOKEN
|
|||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
|
||||||
RUN corepack enable pnpm && corepack prepare pnpm --activate
|
RUN corepack enable
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
41
README.md
@ -1,4 +1,15 @@
|
|||||||

|
<div align="center" markdown="1">
|
||||||
|
<a href="https://go.warp.dev/Reactive-Resume">
|
||||||
|
<img alt="Warp Sponsorship" width="400" src="https://github.com/warpdotdev/brand-assets/blob/main/Github/Sponsor/Warp-Github-LG-03.png?raw=true" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### [Warp, built for coding with multiple AI agents.](https://go.warp.dev/Reactive-Resume)
|
||||||
|
|
||||||
|
[Available for MacOS, Linux, & Windows](https://go.warp.dev/Reactive-Resume)<br>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<img alt="Reactive Resume" width="800" src="https://i.imgur.com/FFc4nyZ.jpg" />
|
||||||
|
|
||||||

|

|
||||||
[](https://hub.docker.com/repository/docker/amruthpillai/reactive-resume)
|
[](https://hub.docker.com/repository/docker/amruthpillai/reactive-resume)
|
||||||
@ -12,6 +23,8 @@ A free and open-source resume builder that simplifies the process of creating, u
|
|||||||
|
|
||||||
### [Go to App](https://rxresu.me/) | [Docs](https://docs.rxresu.me/)
|
### [Go to App](https://rxresu.me/) | [Docs](https://docs.rxresu.me/)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Reactive Resume is a free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume. With zero user tracking or advertising, your privacy is a top priority. The platform is extremely user-friendly and can be self-hosted in less than 30 seconds if you wish to own your data completely.
|
Reactive Resume is a free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume. With zero user tracking or advertising, your privacy is a top priority. The platform is extremely user-friendly and can be self-hosted in less than 30 seconds if you wish to own your data completely.
|
||||||
@ -24,17 +37,21 @@ Start creating your standout resume with Reactive Resume today!
|
|||||||
|
|
||||||
## Templates
|
## Templates
|
||||||
|
|
||||||
| Azurill | Bronzor | Chikorita |
|
| Azurill | Bronzor | Chikorita |
|
||||||
| ------------------------------------------------------------ | ----------------------------------------------------------- | ----------------------------------------------------------- |
|
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||||
| <img src="https://i.imgur.com/jKgo04C.jpeg" width="200px" /> | <img src="https://i.imgur.com/DFNQZP2.jpg" width="200px" /> | <img src="https://i.imgur.com/Dwv8Y7f.jpg" width="200px" /> |
|
| <img src="./apps/client/public/templates/jpg/azurill.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/bronzor.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/chikorita.jpg" width="200px" /> |
|
||||||
|
|
||||||
| Ditto | Kakuna | Nosepass |
|
| Ditto | Gengar | Glalie |
|
||||||
| ----------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- |
|
| -------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||||
| <img src="https://i.imgur.com/6c5lASL.jpg" width="200px" /> | <img src="https://i.imgur.com/268ML3t.jpg" width="200px" /> | <img src="https://i.imgur.com/npRLsPS.jpg" width="200px" /> |
|
| <img src="./apps/client/public/templates/jpg/ditto.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/gengar.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/glalie.jpg" width="200px" /> |
|
||||||
|
|
||||||
| Onyx | Pikachu | Rhyhorn |
|
| Kakuna | Leafish | Nosepass |
|
||||||
| ----------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- |
|
| ---------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||||
| <img src="https://i.imgur.com/cxplXOW.jpg" width="200px" /> | <img src="https://i.imgur.com/Y9f7qsh.jpg" width="200px" /> | <img src="https://i.imgur.com/h4kQxy2.jpg" width="200px" /> |
|
| <img src="./apps/client/public/templates/jpg/kakuna.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/leafish.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/nosepass.jpg" width="200px" /> |
|
||||||
|
|
||||||
|
| Onyx | Pikachu | Rhyhorn |
|
||||||
|
| ------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||||
|
| <img src="./apps/client/public/templates/jpg/onyx.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/pikachu.jpg" width="200px" /> | <img src="./apps/client/public/templates/jpg/rhyhorn.jpg" width="200px" /> |
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@ -48,7 +65,7 @@ Start creating your standout resume with Reactive Resume today!
|
|||||||
- **Bring your own OpenAI API key** and unlock features such as improving your writing, fixing spelling and grammar or changing the tone of your text in one-click
|
- **Bring your own OpenAI API key** and unlock features such as improving your writing, fixing spelling and grammar or changing the tone of your text in one-click
|
||||||
- Translate your resume into any language using ChatGPT and import it back for easier editing
|
- Translate your resume into any language using ChatGPT and import it back for easier editing
|
||||||
- Create single page resumes or a resume that spans multiple pages easily
|
- Create single page resumes or a resume that spans multiple pages easily
|
||||||
- Customize the colours and layouts to add a personal touch to your resume.
|
- Customize the colours and layouts to add a personal touch to your resume
|
||||||
- Customise your page layout as you like just by dragging-and-dropping sections
|
- Customise your page layout as you like just by dragging-and-dropping sections
|
||||||
- Create custom sections that are specific to your industry if the existing ones don't fit
|
- Create custom sections that are specific to your industry if the existing ones don't fit
|
||||||
- Jot down personal notes specific to your resume that's only visible to you
|
- Jot down personal notes specific to your resume that's only visible to you
|
||||||
@ -93,7 +110,7 @@ _By the community, for the community._
|
|||||||
A passion project by [Amruth Pillai](https://www.amruthpillai.com/)
|
A passion project by [Amruth Pillai](https://www.amruthpillai.com/)
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="https://www.digitalocean.com/?utm_medium=opensource&utm_source=Reactive-Resume">
|
<a href="https://m.do.co/c/ceae1fff245e">
|
||||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" width="200px">
|
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" width="200px">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -4,13 +4,6 @@
|
|||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
"extends": ["plugin:tailwindcss/recommended"],
|
|
||||||
"settings": {
|
|
||||||
"tailwindcss": {
|
|
||||||
"callees": ["cn", "clsx", "cva"],
|
|
||||||
"config": "tailwind.config.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rules": {
|
"rules": {
|
||||||
// eslint
|
// eslint
|
||||||
"@typescript-eslint/no-require-imports": "off",
|
"@typescript-eslint/no-require-imports": "off",
|
||||||
@ -28,10 +21,7 @@
|
|||||||
],
|
],
|
||||||
|
|
||||||
// react-hooks
|
// react-hooks
|
||||||
"react-hooks/exhaustive-deps": "off",
|
"react-hooks/exhaustive-deps": "off"
|
||||||
|
|
||||||
// tailwindcss
|
|
||||||
"tailwindcss/no-custom-classname": "off"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -39,6 +39,6 @@
|
|||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
|
||||||
<!-- Phosphor Icons -->
|
<!-- Phosphor Icons -->
|
||||||
<script src="https://unpkg.com/@phosphor-icons/web"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/@phosphor-icons/web"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
24
apps/artboard/src/components/brand-icon.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { forwardRef } from "react";
|
||||||
|
|
||||||
|
type BrandIconProps = {
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BrandIcon = forwardRef<HTMLImageElement, BrandIconProps>(({ slug }, ref) => {
|
||||||
|
if (slug.toLowerCase() === "linkedin") {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
ref={ref}
|
||||||
|
alt="LinkedIn"
|
||||||
|
className="size-4"
|
||||||
|
src={`${window.location.origin}/support-logos/linkedin.svg`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img ref={ref} alt={slug} className="size-4" src={`https://cdn.simpleicons.org/${slug}`} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
BrandIcon.displayName = "BrandIcon";
|
||||||
@ -14,8 +14,8 @@ export const Picture = ({ className }: PictureProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={picture.url}
|
|
||||||
alt="Profile"
|
alt="Profile"
|
||||||
|
src={picture.url}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative z-20 object-cover",
|
"relative z-20 object-cover",
|
||||||
picture.effects.border && "border-primary",
|
picture.effects.border && "border-primary",
|
||||||
|
|||||||
5
apps/artboard/src/constants/helmet.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { HelmetData } from "react-helmet-async";
|
||||||
|
|
||||||
|
export const helmetData = new HelmetData({});
|
||||||
|
|
||||||
|
export const helmetContext = helmetData.context;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import * as ReactDOM from "react-dom/client";
|
import * as ReactDOM from "react-dom/client";
|
||||||
import { RouterProvider } from "react-router-dom";
|
import { RouterProvider } from "react-router";
|
||||||
|
|
||||||
import { router } from "./router";
|
import { router } from "./router";
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
|
import { isLocalFont } from "@reactive-resume/utils";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { Outlet } from "react-router-dom";
|
import { Helmet } from "react-helmet-async";
|
||||||
|
import { Outlet } from "react-router";
|
||||||
import webfontloader from "webfontloader";
|
import webfontloader from "webfontloader";
|
||||||
|
|
||||||
import { useArtboardStore } from "../store/artboard";
|
import { useArtboardStore } from "../store/artboard";
|
||||||
|
|
||||||
export const ArtboardPage = () => {
|
export const ArtboardPage = () => {
|
||||||
|
const name = useArtboardStore((state) => state.resume.basics.name);
|
||||||
const metadata = useArtboardStore((state) => state.resume.metadata);
|
const metadata = useArtboardStore((state) => state.resume.metadata);
|
||||||
|
|
||||||
const fontString = useMemo(() => {
|
const fontString = useMemo(() => {
|
||||||
@ -16,6 +19,18 @@ export const ArtboardPage = () => {
|
|||||||
}, [metadata.typography.font]);
|
}, [metadata.typography.font]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const family = metadata.typography.font.family;
|
||||||
|
if (isLocalFont(family)) {
|
||||||
|
let frame = 0;
|
||||||
|
frame = requestAnimationFrame(() => {
|
||||||
|
const width = window.document.body.offsetWidth;
|
||||||
|
const height = window.document.body.offsetHeight;
|
||||||
|
const message = { type: "PAGE_LOADED", payload: { width, height } };
|
||||||
|
window.postMessage(message, "*");
|
||||||
|
});
|
||||||
|
return () => { cancelAnimationFrame(frame); };
|
||||||
|
}
|
||||||
|
|
||||||
webfontloader.load({
|
webfontloader.load({
|
||||||
google: { families: [fontString] },
|
google: { families: [fontString] },
|
||||||
active: () => {
|
active: () => {
|
||||||
@ -55,5 +70,18 @@ export const ArtboardPage = () => {
|
|||||||
}
|
}
|
||||||
}, [metadata]);
|
}, [metadata]);
|
||||||
|
|
||||||
return <Outlet />;
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>{name} | Reactive Resume</title>
|
||||||
|
{metadata.css.visible && (
|
||||||
|
<style id="custom-css" lang="css">
|
||||||
|
{metadata.css.value}
|
||||||
|
</style>
|
||||||
|
)}
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<Outlet />
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,17 +1,22 @@
|
|||||||
import { SectionKey } from "@reactive-resume/schema";
|
import type { SectionKey } from "@reactive-resume/schema";
|
||||||
import { pageSizeMap, Template } from "@reactive-resume/utils";
|
import type { Template } from "@reactive-resume/utils";
|
||||||
|
import { pageSizeMap } from "@reactive-resume/utils";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { ReactZoomPanPinchRef, TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
import type { ReactZoomPanPinchRef } from "react-zoom-pan-pinch";
|
||||||
|
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
||||||
|
|
||||||
import { MM_TO_PX, Page } from "../components/page";
|
import { MM_TO_PX, Page } from "../components/page";
|
||||||
import { useArtboardStore } from "../store/artboard";
|
import { useArtboardStore } from "../store/artboard";
|
||||||
import { getTemplate } from "../templates";
|
import { getTemplate } from "../templates";
|
||||||
|
|
||||||
export const BuilderLayout = () => {
|
export const BuilderLayout = () => {
|
||||||
|
const [wheelPanning, setWheelPanning] = useState(true);
|
||||||
|
|
||||||
const transformRef = useRef<ReactZoomPanPinchRef>(null);
|
const transformRef = useRef<ReactZoomPanPinchRef>(null);
|
||||||
const format = useArtboardStore((state) => state.resume.metadata.page.format);
|
|
||||||
const layout = useArtboardStore((state) => state.resume.metadata.layout);
|
const layout = useArtboardStore((state) => state.resume.metadata.layout);
|
||||||
|
const format = useArtboardStore((state) => state.resume.metadata.page.format);
|
||||||
const template = useArtboardStore((state) => state.resume.metadata.template as Template);
|
const template = useArtboardStore((state) => state.resume.metadata.template as Template);
|
||||||
|
|
||||||
const Template = useMemo(() => getTemplate(template), [template]);
|
const Template = useMemo(() => getTemplate(template), [template]);
|
||||||
@ -27,6 +32,9 @@ export const BuilderLayout = () => {
|
|||||||
transformRef.current?.resetTransform(0);
|
transformRef.current?.resetTransform(0);
|
||||||
setTimeout(() => transformRef.current?.centerView(0.8, 0), 10);
|
setTimeout(() => transformRef.current?.centerView(0.8, 0), 10);
|
||||||
}
|
}
|
||||||
|
if (event.data.type === "TOGGLE_PAN_MODE") {
|
||||||
|
setWheelPanning(event.data.panMode);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("message", handleMessage);
|
window.addEventListener("message", handleMessage);
|
||||||
@ -44,6 +52,8 @@ export const BuilderLayout = () => {
|
|||||||
minScale={0.4}
|
minScale={0.4}
|
||||||
initialScale={0.8}
|
initialScale={0.8}
|
||||||
limitToBounds={false}
|
limitToBounds={false}
|
||||||
|
wheel={{ wheelDisabled: wheelPanning }}
|
||||||
|
panning={{ wheelPanning: wheelPanning }}
|
||||||
>
|
>
|
||||||
<TransformComponent
|
<TransformComponent
|
||||||
wrapperClass="!w-screen !h-screen"
|
wrapperClass="!w-screen !h-screen"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { SectionKey } from "@reactive-resume/schema";
|
import type { SectionKey } from "@reactive-resume/schema";
|
||||||
import { Template } from "@reactive-resume/utils";
|
import type { Template } from "@reactive-resume/utils";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { Page } from "../components/page";
|
import { Page } from "../components/page";
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Outlet } from "react-router-dom";
|
import { HelmetProvider } from "react-helmet-async";
|
||||||
|
import { Outlet } from "react-router";
|
||||||
|
|
||||||
|
import { helmetContext } from "../constants/helmet";
|
||||||
import { useArtboardStore } from "../store/artboard";
|
import { useArtboardStore } from "../store/artboard";
|
||||||
|
|
||||||
export const Providers = () => {
|
export const Providers = () => {
|
||||||
@ -10,35 +12,28 @@ export const Providers = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMessage = (event: MessageEvent) => {
|
const handleMessage = (event: MessageEvent) => {
|
||||||
if (event.origin !== window.location.origin) return;
|
if (event.origin !== window.location.origin) return;
|
||||||
|
|
||||||
if (event.data.type === "SET_RESUME") setResume(event.data.payload);
|
if (event.data.type === "SET_RESUME") setResume(event.data.payload);
|
||||||
if (event.data.type === "SET_THEME") {
|
|
||||||
event.data.payload === "dark"
|
|
||||||
? document.documentElement.classList.add("dark")
|
|
||||||
: document.documentElement.classList.remove("dark");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const resumeData = window.localStorage.getItem("resume");
|
window.addEventListener("message", handleMessage, false);
|
||||||
if (resumeData) {
|
|
||||||
setResume(JSON.parse(resumeData));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("message", handleMessage);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("message", handleMessage);
|
window.removeEventListener("message", handleMessage, false);
|
||||||
};
|
};
|
||||||
}, [setResume]);
|
}, []);
|
||||||
|
|
||||||
// Only for testing, in production this will be fetched from window.postMessage
|
useEffect(() => {
|
||||||
// useEffect(() => {
|
const resumeData = window.localStorage.getItem("resume");
|
||||||
// setResume(sampleResume);
|
|
||||||
// }, [setResume]);
|
if (resumeData) setResume(JSON.parse(resumeData));
|
||||||
|
}, [window.localStorage.getItem("resume")]);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
if (!resume) return null;
|
if (!resume) return null;
|
||||||
|
|
||||||
return <Outlet />;
|
return (
|
||||||
|
<HelmetProvider context={helmetContext}>
|
||||||
|
<Outlet />
|
||||||
|
</HelmetProvider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { createBrowserRouter, createRoutesFromChildren, Route } from "react-router-dom";
|
import { createBrowserRouter, createRoutesFromChildren, Route } from "react-router";
|
||||||
|
|
||||||
import { ArtboardPage } from "../pages/artboard";
|
import { ArtboardPage } from "../pages/artboard";
|
||||||
import { BuilderLayout } from "../pages/builder";
|
import { BuilderLayout } from "../pages/builder";
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ResumeData } from "@reactive-resume/schema";
|
import type { ResumeData } from "@reactive-resume/schema";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
export type ArtboardStore = {
|
export type ArtboardStore = {
|
||||||
|
|||||||
@ -8,6 +8,10 @@
|
|||||||
@apply border-current;
|
@apply border-current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
@apply antialiased;
|
@apply antialiased;
|
||||||
}
|
}
|
||||||
@ -21,5 +25,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wysiwyg {
|
.wysiwyg {
|
||||||
@apply prose max-w-none prose-foreground prose-headings:mt-0 prose-headings:mb-2 prose-p:mt-0 prose-p:mb-2 prose-ul:mt-0 prose-ul:mb-2 prose-li:mt-0 prose-li:mb-2 prose-ol:mt-0 prose-ol:mb-2 prose-img:mt-0 prose-img:mb-2 prose-hr:mt-0 prose-hr:mb-2 prose-p:leading-normal prose-li:leading-normal prose-a:break-all;
|
@apply prose-foreground prose max-w-none prose-headings:mb-2 prose-headings:mt-0 prose-p:mb-2 prose-p:mt-0 prose-p:leading-normal prose-a:break-all prose-ol:mb-2 prose-ol:mt-0 prose-ul:mb-2 prose-ul:mt-0 prose-li:mb-2 prose-li:mt-0 prose-li:leading-normal prose-img:mb-2 prose-img:mt-0 prose-hr:mb-2 prose-hr:mt-0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import {
|
import type {
|
||||||
Award,
|
Award,
|
||||||
Certification,
|
Certification,
|
||||||
CustomSection,
|
CustomSection,
|
||||||
CustomSectionGroup,
|
CustomSectionGroup,
|
||||||
Education,
|
|
||||||
Experience,
|
|
||||||
Interest,
|
Interest,
|
||||||
Language,
|
Language,
|
||||||
Profile,
|
Profile,
|
||||||
@ -15,15 +13,16 @@ import {
|
|||||||
SectionWithItem,
|
SectionWithItem,
|
||||||
Skill,
|
Skill,
|
||||||
URL,
|
URL,
|
||||||
Volunteer,
|
|
||||||
} from "@reactive-resume/schema";
|
} from "@reactive-resume/schema";
|
||||||
import { cn, isEmptyString, isUrl, linearTransform } from "@reactive-resume/utils";
|
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||||
|
import { cn, isEmptyString, isUrl, linearTransform, sanitize } from "@reactive-resume/utils";
|
||||||
import get from "lodash.get";
|
import get from "lodash.get";
|
||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
|
|
||||||
|
import { BrandIcon } from "../components/brand-icon";
|
||||||
import { Picture } from "../components/picture";
|
import { Picture } from "../components/picture";
|
||||||
import { useArtboardStore } from "../store/artboard";
|
import { useArtboardStore } from "../store/artboard";
|
||||||
import { TemplateProps } from "../types/template";
|
import type { TemplateProps } from "../types/template";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const basics = useArtboardStore((state) => state.resume.basics);
|
const basics = useArtboardStore((state) => state.resume.basics);
|
||||||
@ -99,9 +98,9 @@ const Summary = () => {
|
|||||||
<div className="absolute left-[-4.5px] top-[8px] hidden size-[8px] rounded-full bg-primary group-[.main]:block" />
|
<div className="absolute left-[-4.5px] top-[8px] hidden size-[8px] rounded-full bg-primary group-[.main]:block" />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||||
className="wysiwyg"
|
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
|
className="wysiwyg"
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
</section>
|
</section>
|
||||||
@ -132,13 +131,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
|||||||
if (!isUrl(url.href)) return null;
|
if (!isUrl(url.href)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5 break-all">
|
||||||
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
||||||
<a
|
<a
|
||||||
href={url.href}
|
href={url.href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("line-clamp-1 max-w-fit", className)}
|
||||||
>
|
>
|
||||||
{label ?? (url.label || url.href)}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
@ -187,7 +186,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || section.items.length === 0) return null;
|
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -225,7 +224,10 @@ const Section = <T,>({
|
|||||||
<div>{children?.(item as T)}</div>
|
<div>{children?.(item as T)}</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||||
|
className="wysiwyg"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -247,26 +249,13 @@ const Section = <T,>({
|
|||||||
|
|
||||||
const Profiles = () => {
|
const Profiles = () => {
|
||||||
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
||||||
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section<Profile> section={section}>
|
<Section<Profile> section={section}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div>
|
<div>
|
||||||
{isUrl(item.url.href) ? (
|
{isUrl(item.url.href) ? (
|
||||||
<Link
|
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
|
||||||
url={item.url}
|
|
||||||
label={item.username}
|
|
||||||
icon={
|
|
||||||
<img
|
|
||||||
className="ph"
|
|
||||||
width={fontSize}
|
|
||||||
height={fontSize}
|
|
||||||
alt={item.network}
|
|
||||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<p>{item.username}</p>
|
<p>{item.username}</p>
|
||||||
)}
|
)}
|
||||||
@ -564,7 +553,7 @@ export const Azurill = ({ columns, isFirstPage = false }: TemplateProps) => {
|
|||||||
const [main, sidebar] = columns;
|
const [main, sidebar] = columns;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-custom space-y-3">
|
<div className="space-y-3 p-custom">
|
||||||
{isFirstPage && <Header />}
|
{isFirstPage && <Header />}
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-x-4">
|
<div className="grid grid-cols-3 gap-x-4">
|
||||||
@ -574,7 +563,9 @@ export const Azurill = ({ columns, isFirstPage = false }: TemplateProps) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="main group col-span-2 space-y-4">
|
<div
|
||||||
|
className={cn("main group space-y-4", sidebar.length > 0 ? "col-span-2" : "col-span-3")}
|
||||||
|
>
|
||||||
{main.map((section) => (
|
{main.map((section) => (
|
||||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import {
|
import type {
|
||||||
Award,
|
Award,
|
||||||
Certification,
|
Certification,
|
||||||
CustomSection,
|
CustomSection,
|
||||||
CustomSectionGroup,
|
CustomSectionGroup,
|
||||||
Education,
|
|
||||||
Experience,
|
|
||||||
Interest,
|
Interest,
|
||||||
Language,
|
Language,
|
||||||
Profile,
|
Profile,
|
||||||
@ -15,15 +13,16 @@ import {
|
|||||||
SectionWithItem,
|
SectionWithItem,
|
||||||
Skill,
|
Skill,
|
||||||
URL,
|
URL,
|
||||||
Volunteer,
|
|
||||||
} from "@reactive-resume/schema";
|
} from "@reactive-resume/schema";
|
||||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||||
|
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||||
import get from "lodash.get";
|
import get from "lodash.get";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
import { BrandIcon } from "../components/brand-icon";
|
||||||
import { Picture } from "../components/picture";
|
import { Picture } from "../components/picture";
|
||||||
import { useArtboardStore } from "../store/artboard";
|
import { useArtboardStore } from "../store/artboard";
|
||||||
import { TemplateProps } from "../types/template";
|
import type { TemplateProps } from "../types/template";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const basics = useArtboardStore((state) => state.resume.basics);
|
const basics = useArtboardStore((state) => state.resume.basics);
|
||||||
@ -90,9 +89,9 @@ const Summary = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||||
className="wysiwyg col-span-4"
|
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
|
className="wysiwyg col-span-4"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -123,13 +122,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
|||||||
if (!isUrl(url.href)) return null;
|
if (!isUrl(url.href)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5 break-all">
|
||||||
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
||||||
<a
|
<a
|
||||||
href={url.href}
|
href={url.href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("line-clamp-1 max-w-fit", className)}
|
||||||
>
|
>
|
||||||
{label ?? (url.label || url.href)}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
@ -178,7 +177,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || section.items.length === 0) return null;
|
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid grid-cols-5 border-t pt-2.5">
|
<section id={section.id} className="grid grid-cols-5 border-t pt-2.5">
|
||||||
@ -206,7 +205,10 @@ const Section = <T,>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||||
|
className="wysiwyg"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -224,26 +226,13 @@ const Section = <T,>({
|
|||||||
|
|
||||||
const Profiles = () => {
|
const Profiles = () => {
|
||||||
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
||||||
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section<Profile> section={section}>
|
<Section<Profile> section={section}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div>
|
<div>
|
||||||
{isUrl(item.url.href) ? (
|
{isUrl(item.url.href) ? (
|
||||||
<Link
|
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
|
||||||
url={item.url}
|
|
||||||
label={item.username}
|
|
||||||
icon={
|
|
||||||
<img
|
|
||||||
className="ph"
|
|
||||||
width={fontSize}
|
|
||||||
height={fontSize}
|
|
||||||
alt={item.network}
|
|
||||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<p>{item.username}</p>
|
<p>{item.username}</p>
|
||||||
)}
|
)}
|
||||||
@ -579,7 +568,7 @@ export const Bronzor = ({ columns, isFirstPage = false }: TemplateProps) => {
|
|||||||
const [main, sidebar] = columns;
|
const [main, sidebar] = columns;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-custom space-y-4">
|
<div className="space-y-4 p-custom">
|
||||||
{isFirstPage && <Header />}
|
{isFirstPage && <Header />}
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import {
|
import type {
|
||||||
Award,
|
Award,
|
||||||
Certification,
|
Certification,
|
||||||
CustomSection,
|
CustomSection,
|
||||||
CustomSectionGroup,
|
CustomSectionGroup,
|
||||||
Education,
|
|
||||||
Experience,
|
|
||||||
Interest,
|
Interest,
|
||||||
Language,
|
Language,
|
||||||
Profile,
|
Profile,
|
||||||
@ -15,15 +13,16 @@ import {
|
|||||||
SectionWithItem,
|
SectionWithItem,
|
||||||
Skill,
|
Skill,
|
||||||
URL,
|
URL,
|
||||||
Volunteer,
|
|
||||||
} from "@reactive-resume/schema";
|
} from "@reactive-resume/schema";
|
||||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||||
|
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||||
import get from "lodash.get";
|
import get from "lodash.get";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
import { BrandIcon } from "../components/brand-icon";
|
||||||
import { Picture } from "../components/picture";
|
import { Picture } from "../components/picture";
|
||||||
import { useArtboardStore } from "../store/artboard";
|
import { useArtboardStore } from "../store/artboard";
|
||||||
import { TemplateProps } from "../types/template";
|
import type { TemplateProps } from "../types/template";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const basics = useArtboardStore((state) => state.resume.basics);
|
const basics = useArtboardStore((state) => state.resume.basics);
|
||||||
@ -90,9 +89,9 @@ const Summary = () => {
|
|||||||
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
|
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||||
className="wysiwyg"
|
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
|
className="wysiwyg"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -126,17 +125,19 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
|||||||
if (!isUrl(url.href)) return null;
|
if (!isUrl(url.href)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5 break-all">
|
||||||
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
{!iconOnRight &&
|
||||||
|
(icon ?? <i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-white" />)}
|
||||||
<a
|
<a
|
||||||
href={url.href}
|
href={url.href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("line-clamp-1 max-w-fit", className)}
|
||||||
>
|
>
|
||||||
{label ?? (url.label || url.href)}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
{iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
{iconOnRight &&
|
||||||
|
(icon ?? <i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-white" />)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -181,7 +182,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || section.items.length === 0) return null;
|
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -207,7 +208,10 @@ const Section = <T,>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||||
|
className="wysiwyg group-[.sidebar]:prose-invert"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -280,26 +284,13 @@ const Education = () => {
|
|||||||
|
|
||||||
const Profiles = () => {
|
const Profiles = () => {
|
||||||
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
||||||
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section<Profile> section={section}>
|
<Section<Profile> section={section}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div>
|
<div>
|
||||||
{isUrl(item.url.href) ? (
|
{isUrl(item.url.href) ? (
|
||||||
<Link
|
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
|
||||||
url={item.url}
|
|
||||||
label={item.username}
|
|
||||||
icon={
|
|
||||||
<img
|
|
||||||
className="ph"
|
|
||||||
width={fontSize}
|
|
||||||
height={fontSize}
|
|
||||||
alt={item.network}
|
|
||||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<p>{item.username}</p>
|
<p>{item.username}</p>
|
||||||
)}
|
)}
|
||||||
@ -578,7 +569,12 @@ export const Chikorita = ({ columns, isFirstPage = false }: TemplateProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid min-h-[inherit] grid-cols-3">
|
<div className="grid min-h-[inherit] grid-cols-3">
|
||||||
<div className="main p-custom group col-span-2 space-y-4">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"main group space-y-4 p-custom",
|
||||||
|
sidebar.length > 0 ? "col-span-2" : "col-span-3",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{isFirstPage && <Header />}
|
{isFirstPage && <Header />}
|
||||||
|
|
||||||
{main.map((section) => (
|
{main.map((section) => (
|
||||||
@ -586,7 +582,12 @@ export const Chikorita = ({ columns, isFirstPage = false }: TemplateProps) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sidebar p-custom group h-full space-y-4 bg-primary text-background">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"sidebar group h-full space-y-4 bg-primary p-custom text-background",
|
||||||
|
sidebar.length === 0 && "hidden",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{sidebar.map((section) => (
|
{sidebar.map((section) => (
|
||||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import {
|
import type {
|
||||||
Award,
|
Award,
|
||||||
Certification,
|
Certification,
|
||||||
CustomSection,
|
CustomSection,
|
||||||
CustomSectionGroup,
|
CustomSectionGroup,
|
||||||
Education,
|
|
||||||
Experience,
|
|
||||||
Interest,
|
Interest,
|
||||||
Language,
|
Language,
|
||||||
Profile,
|
Profile,
|
||||||
@ -15,21 +13,22 @@ import {
|
|||||||
SectionWithItem,
|
SectionWithItem,
|
||||||
Skill,
|
Skill,
|
||||||
URL,
|
URL,
|
||||||
Volunteer,
|
|
||||||
} from "@reactive-resume/schema";
|
} from "@reactive-resume/schema";
|
||||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||||
|
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||||
import get from "lodash.get";
|
import get from "lodash.get";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
import { BrandIcon } from "../components/brand-icon";
|
||||||
import { Picture } from "../components/picture";
|
import { Picture } from "../components/picture";
|
||||||
import { useArtboardStore } from "../store/artboard";
|
import { useArtboardStore } from "../store/artboard";
|
||||||
import { TemplateProps } from "../types/template";
|
import type { TemplateProps } from "../types/template";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const basics = useArtboardStore((state) => state.resume.basics);
|
const basics = useArtboardStore((state) => state.resume.basics);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-custom relative grid grid-cols-3 space-x-4 pb-0">
|
<div className="relative grid grid-cols-3 space-x-4 p-custom pb-0">
|
||||||
<Picture className="mx-auto" />
|
<Picture className="mx-auto" />
|
||||||
|
|
||||||
<div className="relative z-10 col-span-2 text-background">
|
<div className="relative z-10 col-span-2 text-background">
|
||||||
@ -110,9 +109,9 @@ const Summary = () => {
|
|||||||
<h4 className="mb-2 text-base font-bold">{section.name}</h4>
|
<h4 className="mb-2 text-base font-bold">{section.name}</h4>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||||
className="wysiwyg"
|
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
|
className="wysiwyg"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -143,13 +142,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
|||||||
if (!isUrl(url.href)) return null;
|
if (!isUrl(url.href)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5 break-all">
|
||||||
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
||||||
<a
|
<a
|
||||||
href={url.href}
|
href={url.href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("line-clamp-1 max-w-fit", className)}
|
||||||
>
|
>
|
||||||
{label ?? (url.label || url.href)}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
@ -198,7 +197,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || section.items.length === 0) return null;
|
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -231,7 +230,10 @@ const Section = <T,>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||||
|
className="wysiwyg"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -251,26 +253,13 @@ const Section = <T,>({
|
|||||||
|
|
||||||
const Profiles = () => {
|
const Profiles = () => {
|
||||||
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
||||||
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section<Profile> section={section}>
|
<Section<Profile> section={section}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div>
|
<div>
|
||||||
{isUrl(item.url.href) ? (
|
{isUrl(item.url.href) ? (
|
||||||
<Link
|
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
|
||||||
url={item.url}
|
|
||||||
label={item.username}
|
|
||||||
icon={
|
|
||||||
<img
|
|
||||||
className="ph"
|
|
||||||
width={fontSize}
|
|
||||||
height={fontSize}
|
|
||||||
alt={item.network}
|
|
||||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<p>{item.username}</p>
|
<p>{item.username}</p>
|
||||||
)}
|
)}
|
||||||
@ -615,13 +604,18 @@ export const Ditto = ({ columns, isFirstPage = false }: TemplateProps) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-3">
|
<div className="grid grid-cols-3">
|
||||||
<div className="sidebar p-custom group space-y-4">
|
<div className="sidebar group space-y-4 p-custom">
|
||||||
{sidebar.map((section) => (
|
{sidebar.map((section) => (
|
||||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="main p-custom group col-span-2 space-y-4">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"main group space-y-4 p-custom",
|
||||||
|
sidebar.length > 0 ? "col-span-2" : "col-span-3",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{main.map((section) => (
|
{main.map((section) => (
|
||||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import {
|
import type {
|
||||||
Award,
|
Award,
|
||||||
Certification,
|
Certification,
|
||||||
CustomSection,
|
CustomSection,
|
||||||
CustomSectionGroup,
|
CustomSectionGroup,
|
||||||
Education,
|
|
||||||
Experience,
|
|
||||||
Interest,
|
Interest,
|
||||||
Language,
|
Language,
|
||||||
Profile,
|
Profile,
|
||||||
@ -15,21 +13,22 @@ import {
|
|||||||
SectionWithItem,
|
SectionWithItem,
|
||||||
Skill,
|
Skill,
|
||||||
URL,
|
URL,
|
||||||
Volunteer,
|
|
||||||
} from "@reactive-resume/schema";
|
} from "@reactive-resume/schema";
|
||||||
import { cn, hexToRgb, isEmptyString, isUrl } from "@reactive-resume/utils";
|
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||||
|
import { cn, hexToRgb, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||||
import get from "lodash.get";
|
import get from "lodash.get";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
import { BrandIcon } from "../components/brand-icon";
|
||||||
import { Picture } from "../components/picture";
|
import { Picture } from "../components/picture";
|
||||||
import { useArtboardStore } from "../store/artboard";
|
import { useArtboardStore } from "../store/artboard";
|
||||||
import { TemplateProps } from "../types/template";
|
import type { TemplateProps } from "../types/template";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const basics = useArtboardStore((state) => state.resume.basics);
|
const basics = useArtboardStore((state) => state.resume.basics);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-custom space-y-4 bg-primary text-background">
|
<div className="space-y-4 bg-primary p-custom text-background">
|
||||||
<Picture className="border-background" />
|
<Picture className="border-background" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -82,17 +81,20 @@ const Header = () => {
|
|||||||
|
|
||||||
const Summary = () => {
|
const Summary = () => {
|
||||||
const section = useArtboardStore((state) => state.resume.sections.summary);
|
const section = useArtboardStore((state) => state.resume.sections.summary);
|
||||||
|
const primaryColor = useArtboardStore((state) => state.resume.metadata.theme.primary);
|
||||||
|
|
||||||
if (!section.visible || isEmptyString(section.content)) return null;
|
if (!section.visible || isEmptyString(section.content)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id}>
|
<div className="space-y-4 p-custom" style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}>
|
||||||
<div
|
<section id={section.id}>
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
<div
|
||||||
className="wysiwyg"
|
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
/>
|
className="wysiwyg"
|
||||||
</section>
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -121,7 +123,7 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
|||||||
if (!isUrl(url.href)) return null;
|
if (!isUrl(url.href)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5 break-all">
|
||||||
{!iconOnRight &&
|
{!iconOnRight &&
|
||||||
(icon ?? (
|
(icon ?? (
|
||||||
<i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-background" />
|
<i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-background" />
|
||||||
@ -130,7 +132,7 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
|||||||
href={url.href}
|
href={url.href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("line-clamp-1 max-w-fit", className)}
|
||||||
>
|
>
|
||||||
{label ?? (url.label || url.href)}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
@ -182,7 +184,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || section.items.length === 0) return null;
|
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -208,7 +210,10 @@ const Section = <T,>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||||
|
className="wysiwyg"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -226,26 +231,13 @@ const Section = <T,>({
|
|||||||
|
|
||||||
const Profiles = () => {
|
const Profiles = () => {
|
||||||
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
||||||
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section<Profile> section={section}>
|
<Section<Profile> section={section}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div>
|
<div>
|
||||||
{isUrl(item.url.href) ? (
|
{isUrl(item.url.href) ? (
|
||||||
<Link
|
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
|
||||||
url={item.url}
|
|
||||||
label={item.username}
|
|
||||||
icon={
|
|
||||||
<img
|
|
||||||
className="ph"
|
|
||||||
width={fontSize}
|
|
||||||
height={fontSize}
|
|
||||||
alt={item.network}
|
|
||||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<p>{item.username}</p>
|
<p>{item.username}</p>
|
||||||
)}
|
)}
|
||||||
@ -539,6 +531,11 @@ const mapSectionToComponent = (section: SectionKey) => {
|
|||||||
case "education": {
|
case "education": {
|
||||||
return <Education />;
|
return <Education />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "summary": {
|
||||||
|
return <Summary />;
|
||||||
|
}
|
||||||
|
|
||||||
case "awards": {
|
case "awards": {
|
||||||
return <Awards />;
|
return <Awards />;
|
||||||
}
|
}
|
||||||
@ -590,7 +587,7 @@ export const Gengar = ({ columns, isFirstPage = false }: TemplateProps) => {
|
|||||||
{isFirstPage && <Header />}
|
{isFirstPage && <Header />}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="p-custom flex-1 space-y-4"
|
className="flex-1 space-y-4 p-custom"
|
||||||
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
|
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
|
||||||
>
|
>
|
||||||
{sidebar.map((section) => (
|
{sidebar.map((section) => (
|
||||||
@ -599,17 +596,8 @@ export const Gengar = ({ columns, isFirstPage = false }: TemplateProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="main group col-span-2">
|
<div className={cn("main group", sidebar.length > 0 ? "col-span-2" : "col-span-3")}>
|
||||||
{isFirstPage && (
|
<div className="space-y-4 p-custom">
|
||||||
<div
|
|
||||||
className="p-custom space-y-4"
|
|
||||||
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
|
|
||||||
>
|
|
||||||
<Summary />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="p-custom space-y-4">
|
|
||||||
{main.map((section) => (
|
{main.map((section) => (
|
||||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import {
|
import type {
|
||||||
Award,
|
Award,
|
||||||
Certification,
|
Certification,
|
||||||
CustomSection,
|
CustomSection,
|
||||||
CustomSectionGroup,
|
CustomSectionGroup,
|
||||||
Education,
|
|
||||||
Experience,
|
|
||||||
Interest,
|
Interest,
|
||||||
Language,
|
Language,
|
||||||
Profile,
|
Profile,
|
||||||
@ -15,15 +13,23 @@ import {
|
|||||||
SectionWithItem,
|
SectionWithItem,
|
||||||
Skill,
|
Skill,
|
||||||
URL,
|
URL,
|
||||||
Volunteer,
|
|
||||||
} from "@reactive-resume/schema";
|
} from "@reactive-resume/schema";
|
||||||
import { cn, hexToRgb, isEmptyString, isUrl, linearTransform } from "@reactive-resume/utils";
|
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||||
|
import {
|
||||||
|
cn,
|
||||||
|
hexToRgb,
|
||||||
|
isEmptyString,
|
||||||
|
isUrl,
|
||||||
|
linearTransform,
|
||||||
|
sanitize,
|
||||||
|
} from "@reactive-resume/utils";
|
||||||
import get from "lodash.get";
|
import get from "lodash.get";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
import { BrandIcon } from "../components/brand-icon";
|
||||||
import { Picture } from "../components/picture";
|
import { Picture } from "../components/picture";
|
||||||
import { useArtboardStore } from "../store/artboard";
|
import { useArtboardStore } from "../store/artboard";
|
||||||
import { TemplateProps } from "../types/template";
|
import type { TemplateProps } from "../types/template";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const basics = useArtboardStore((state) => state.resume.basics);
|
const basics = useArtboardStore((state) => state.resume.basics);
|
||||||
@ -90,9 +96,9 @@ const Summary = () => {
|
|||||||
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
|
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||||
className="wysiwyg"
|
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
|
className="wysiwyg"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -129,14 +135,14 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
|||||||
if (!isUrl(url.href)) return null;
|
if (!isUrl(url.href)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5 break-all">
|
||||||
{!iconOnRight &&
|
{!iconOnRight &&
|
||||||
(icon ?? <i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-primary" />)}
|
(icon ?? <i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-primary" />)}
|
||||||
<a
|
<a
|
||||||
href={url.href}
|
href={url.href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("line-clamp-1 max-w-fit", className)}
|
||||||
>
|
>
|
||||||
{label ?? (url.label || url.href)}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
@ -186,7 +192,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || section.items.length === 0) return null;
|
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -214,7 +220,10 @@ const Section = <T,>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||||
|
className="wysiwyg"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -287,26 +296,13 @@ const Education = () => {
|
|||||||
|
|
||||||
const Profiles = () => {
|
const Profiles = () => {
|
||||||
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
||||||
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section<Profile> section={section}>
|
<Section<Profile> section={section}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div>
|
<div>
|
||||||
{isUrl(item.url.href) ? (
|
{isUrl(item.url.href) ? (
|
||||||
<Link
|
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
|
||||||
url={item.url}
|
|
||||||
label={item.username}
|
|
||||||
icon={
|
|
||||||
<img
|
|
||||||
className="ph"
|
|
||||||
width={fontSize}
|
|
||||||
height={fontSize}
|
|
||||||
alt={item.network}
|
|
||||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<p>{item.username}</p>
|
<p>{item.username}</p>
|
||||||
)}
|
)}
|
||||||
@ -591,7 +587,7 @@ export const Glalie = ({ columns, isFirstPage = false }: TemplateProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className="grid min-h-[inherit] grid-cols-3">
|
<div className="grid min-h-[inherit] grid-cols-3">
|
||||||
<div
|
<div
|
||||||
className="sidebar p-custom group space-y-4"
|
className={cn("sidebar group space-y-4 p-custom", sidebar.length === 0 && "hidden")}
|
||||||
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
|
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
|
||||||
>
|
>
|
||||||
{isFirstPage && <Header />}
|
{isFirstPage && <Header />}
|
||||||
@ -601,7 +597,12 @@ export const Glalie = ({ columns, isFirstPage = false }: TemplateProps) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="main p-custom group col-span-2 space-y-4">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"main group space-y-4 p-custom",
|
||||||
|
sidebar.length > 0 ? "col-span-2" : "col-span-3",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{main.map((section) => (
|
{main.map((section) => (
|
||||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Template } from "@reactive-resume/utils";
|
import type { Template } from "@reactive-resume/utils";
|
||||||
|
|
||||||
import { Azurill } from "./azurill";
|
import { Azurill } from "./azurill";
|
||||||
import { Bronzor } from "./bronzor";
|
import { Bronzor } from "./bronzor";
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import {
|
import type {
|
||||||
Award,
|
Award,
|
||||||
Certification,
|
Certification,
|
||||||
CustomSection,
|
CustomSection,
|
||||||
CustomSectionGroup,
|
CustomSectionGroup,
|
||||||
Education,
|
|
||||||
Experience,
|
|
||||||
Interest,
|
Interest,
|
||||||
Language,
|
Language,
|
||||||
Project,
|
Project,
|
||||||
@ -14,20 +12,20 @@ import {
|
|||||||
SectionWithItem,
|
SectionWithItem,
|
||||||
Skill,
|
Skill,
|
||||||
URL,
|
URL,
|
||||||
Volunteer,
|
|
||||||
} from "@reactive-resume/schema";
|
} from "@reactive-resume/schema";
|
||||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||||
|
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||||
import get from "lodash.get";
|
import get from "lodash.get";
|
||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
|
|
||||||
|
import { BrandIcon } from "../components/brand-icon";
|
||||||
import { Picture } from "../components/picture";
|
import { Picture } from "../components/picture";
|
||||||
import { useArtboardStore } from "../store/artboard";
|
import { useArtboardStore } from "../store/artboard";
|
||||||
import { TemplateProps } from "../types/template";
|
import type { TemplateProps } from "../types/template";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const basics = useArtboardStore((state) => state.resume.basics);
|
const basics = useArtboardStore((state) => state.resume.basics);
|
||||||
const profiles = useArtboardStore((state) => state.resume.sections.profiles);
|
const profiles = useArtboardStore((state) => state.resume.sections.profiles);
|
||||||
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center space-y-2 pb-2 text-center">
|
<div className="flex flex-col items-center justify-center space-y-2 pb-2 text-center">
|
||||||
@ -61,7 +59,9 @@ const Header = () => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Link url={basics.url} />
|
<Link url={basics.url} />
|
||||||
|
|
||||||
{basics.customFields.map((item) => (
|
{basics.customFields.map((item) => (
|
||||||
<div key={item.id} className="flex items-center gap-x-1.5">
|
<div key={item.id} className="flex items-center gap-x-1.5">
|
||||||
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
|
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
|
||||||
@ -86,15 +86,7 @@ const Header = () => {
|
|||||||
url={item.url}
|
url={item.url}
|
||||||
label={item.username}
|
label={item.username}
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
icon={
|
icon={<BrandIcon slug={item.icon} />}
|
||||||
<img
|
|
||||||
className="ph"
|
|
||||||
width={fontSize}
|
|
||||||
height={fontSize}
|
|
||||||
alt={item.network}
|
|
||||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -116,9 +108,9 @@ const Summary = () => {
|
|||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||||
className="wysiwyg"
|
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
|
className="wysiwyg"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -149,13 +141,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
|||||||
if (!isUrl(url.href)) return null;
|
if (!isUrl(url.href)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5 break-all">
|
||||||
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
||||||
<a
|
<a
|
||||||
href={url.href}
|
href={url.href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("line-clamp-1 max-w-fit", className)}
|
||||||
>
|
>
|
||||||
{label ?? (url.label || url.href)}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
@ -204,7 +196,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || section.items.length === 0) return null;
|
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -229,7 +221,10 @@ const Section = <T,>({
|
|||||||
<div>{children?.(item as T)}</div>
|
<div>{children?.(item as T)}</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||||
|
className="wysiwyg"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -529,7 +524,7 @@ export const Kakuna = ({ columns, isFirstPage = false }: TemplateProps) => {
|
|||||||
const [main, sidebar] = columns;
|
const [main, sidebar] = columns;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-custom space-y-4">
|
<div className="space-y-4 p-custom">
|
||||||
{isFirstPage && <Header />}
|
{isFirstPage && <Header />}
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import {
|
import type {
|
||||||
Award,
|
Award,
|
||||||
Certification,
|
Certification,
|
||||||
CustomSection,
|
CustomSection,
|
||||||
CustomSectionGroup,
|
CustomSectionGroup,
|
||||||
Education,
|
|
||||||
Experience,
|
|
||||||
Interest,
|
Interest,
|
||||||
Language,
|
Language,
|
||||||
Project,
|
Project,
|
||||||
@ -14,27 +12,27 @@ import {
|
|||||||
SectionWithItem,
|
SectionWithItem,
|
||||||
Skill,
|
Skill,
|
||||||
URL,
|
URL,
|
||||||
Volunteer,
|
|
||||||
} from "@reactive-resume/schema";
|
} from "@reactive-resume/schema";
|
||||||
import { cn, hexToRgb, isEmptyString, isUrl } from "@reactive-resume/utils";
|
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||||
|
import { cn, hexToRgb, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||||
import get from "lodash.get";
|
import get from "lodash.get";
|
||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
|
|
||||||
|
import { BrandIcon } from "../components/brand-icon";
|
||||||
import { Picture } from "../components/picture";
|
import { Picture } from "../components/picture";
|
||||||
import { useArtboardStore } from "../store/artboard";
|
import { useArtboardStore } from "../store/artboard";
|
||||||
import { TemplateProps } from "../types/template";
|
import type { TemplateProps } from "../types/template";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const basics = useArtboardStore((state) => state.resume.basics);
|
const basics = useArtboardStore((state) => state.resume.basics);
|
||||||
const section = useArtboardStore((state) => state.resume.sections.summary);
|
const section = useArtboardStore((state) => state.resume.sections.summary);
|
||||||
const profiles = useArtboardStore((state) => state.resume.sections.profiles);
|
const profiles = useArtboardStore((state) => state.resume.sections.profiles);
|
||||||
const primaryColor = useArtboardStore((state) => state.resume.metadata.theme.primary);
|
const primaryColor = useArtboardStore((state) => state.resume.metadata.theme.primary);
|
||||||
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className="p-custom flex items-center space-x-8"
|
className="flex items-center space-x-8 p-custom"
|
||||||
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
|
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
|
||||||
>
|
>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@ -44,16 +42,16 @@ const Header = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||||
className="wysiwyg"
|
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
|
className="wysiwyg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Picture />
|
<Picture />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-custom space-y-3" style={{ backgroundColor: hexToRgb(primaryColor, 0.4) }}>
|
<div className="space-y-3 p-custom" style={{ backgroundColor: hexToRgb(primaryColor, 0.4) }}>
|
||||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-0.5 text-sm">
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-0.5 text-sm">
|
||||||
{basics.location && (
|
{basics.location && (
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5">
|
||||||
@ -102,15 +100,7 @@ const Header = () => {
|
|||||||
url={item.url}
|
url={item.url}
|
||||||
label={item.username}
|
label={item.username}
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
icon={
|
icon={<BrandIcon slug={item.icon} />}
|
||||||
<img
|
|
||||||
className="ph"
|
|
||||||
width={fontSize}
|
|
||||||
height={fontSize}
|
|
||||||
alt={item.network}
|
|
||||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -146,13 +136,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
|||||||
if (!isUrl(url.href)) return null;
|
if (!isUrl(url.href)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5 break-all">
|
||||||
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
||||||
<a
|
<a
|
||||||
href={url.href}
|
href={url.href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("line-clamp-1 max-w-fit", className)}
|
||||||
>
|
>
|
||||||
{label ?? (url.label || url.href)}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
@ -201,7 +191,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || section.items.length === 0) return null;
|
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -226,7 +216,10 @@ const Section = <T,>({
|
|||||||
<div>{children?.(item as T)}</div>
|
<div>{children?.(item as T)}</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||||
|
className="wysiwyg"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -526,14 +519,14 @@ export const Leafish = ({ columns, isFirstPage = false }: TemplateProps) => {
|
|||||||
<div>
|
<div>
|
||||||
{isFirstPage && <Header />}
|
{isFirstPage && <Header />}
|
||||||
|
|
||||||
<div className="p-custom grid grid-cols-2 items-start space-x-6">
|
<div className="grid grid-cols-2 items-start space-x-6 p-custom">
|
||||||
<div className="grid gap-y-4">
|
<div className={cn("grid gap-y-4", sidebar.length === 0 && "col-span-2")}>
|
||||||
{main.map((section) => (
|
{main.map((section) => (
|
||||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-y-4">
|
<div className={cn("grid gap-y-4", sidebar.length === 0 && "hidden")}>
|
||||||
{sidebar.map((section) => (
|
{sidebar.map((section) => (
|
||||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import {
|
import type {
|
||||||
Award,
|
Award,
|
||||||
Certification,
|
Certification,
|
||||||
CustomSection,
|
CustomSection,
|
||||||
CustomSectionGroup,
|
CustomSectionGroup,
|
||||||
Education,
|
|
||||||
Experience,
|
|
||||||
Interest,
|
Interest,
|
||||||
Language,
|
Language,
|
||||||
Profile,
|
Profile,
|
||||||
@ -15,15 +13,16 @@ import {
|
|||||||
SectionWithItem,
|
SectionWithItem,
|
||||||
Skill,
|
Skill,
|
||||||
URL,
|
URL,
|
||||||
Volunteer,
|
|
||||||
} from "@reactive-resume/schema";
|
} from "@reactive-resume/schema";
|
||||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||||
|
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||||
import get from "lodash.get";
|
import get from "lodash.get";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
import { BrandIcon } from "../components/brand-icon";
|
||||||
import { Picture } from "../components/picture";
|
import { Picture } from "../components/picture";
|
||||||
import { useArtboardStore } from "../store/artboard";
|
import { useArtboardStore } from "../store/artboard";
|
||||||
import { TemplateProps } from "../types/template";
|
import type { TemplateProps } from "../types/template";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const basics = useArtboardStore((state) => state.resume.basics);
|
const basics = useArtboardStore((state) => state.resume.basics);
|
||||||
@ -106,9 +105,9 @@ const Summary = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||||
className="wysiwyg"
|
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
|
className="wysiwyg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -127,13 +126,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
|||||||
if (!isUrl(url.href)) return null;
|
if (!isUrl(url.href)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5 break-all">
|
||||||
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
||||||
<a
|
<a
|
||||||
href={url.href}
|
href={url.href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("line-clamp-1 max-w-fit", className)}
|
||||||
>
|
>
|
||||||
{label ?? (url.label || url.href)}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
@ -181,7 +180,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || section.items.length === 0) return null;
|
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className={cn("grid", dateKey !== undefined && "gap-y-4")}>
|
<section id={section.id} className={cn("grid", dateKey !== undefined && "gap-y-4")}>
|
||||||
@ -218,7 +217,10 @@ const Section = <T,>({
|
|||||||
{url !== undefined && section.separateLinks && <Link url={url} />}
|
{url !== undefined && section.separateLinks && <Link url={url} />}
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||||
|
className="wysiwyg"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{keywords !== undefined && keywords.length > 0 && (
|
{keywords !== undefined && keywords.length > 0 && (
|
||||||
@ -253,7 +255,10 @@ const Section = <T,>({
|
|||||||
{url !== undefined && section.separateLinks && <Link url={url} />}
|
{url !== undefined && section.separateLinks && <Link url={url} />}
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||||
|
className="wysiwyg"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{keywords !== undefined && keywords.length > 0 && (
|
{keywords !== undefined && keywords.length > 0 && (
|
||||||
@ -271,26 +276,13 @@ const Section = <T,>({
|
|||||||
|
|
||||||
const Profiles = () => {
|
const Profiles = () => {
|
||||||
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
||||||
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section<Profile> section={section}>
|
<Section<Profile> section={section}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div>
|
<div>
|
||||||
{isUrl(item.url.href) ? (
|
{isUrl(item.url.href) ? (
|
||||||
<Link
|
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
|
||||||
url={item.url}
|
|
||||||
label={item.username}
|
|
||||||
icon={
|
|
||||||
<img
|
|
||||||
className="ph"
|
|
||||||
width={fontSize}
|
|
||||||
height={fontSize}
|
|
||||||
alt={item.network}
|
|
||||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<p>{item.username}</p>
|
<p>{item.username}</p>
|
||||||
)}
|
)}
|
||||||
@ -583,7 +575,7 @@ export const Nosepass = ({ columns, isFirstPage = false }: TemplateProps) => {
|
|||||||
const [main, sidebar] = columns;
|
const [main, sidebar] = columns;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-custom space-y-6">
|
<div className="space-y-6 p-custom">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<img alt="Europass Logo" className="h-[42px]" src="/assets/europass.png" />
|
<img alt="Europass Logo" className="h-[42px]" src="/assets/europass.png" />
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import {
|
import type {
|
||||||
Award,
|
Award,
|
||||||
Certification,
|
Certification,
|
||||||
CustomSection,
|
CustomSection,
|
||||||
CustomSectionGroup,
|
CustomSectionGroup,
|
||||||
Education,
|
|
||||||
Experience,
|
|
||||||
Interest,
|
Interest,
|
||||||
Language,
|
Language,
|
||||||
Project,
|
Project,
|
||||||
@ -14,20 +12,20 @@ import {
|
|||||||
SectionWithItem,
|
SectionWithItem,
|
||||||
Skill,
|
Skill,
|
||||||
URL,
|
URL,
|
||||||
Volunteer,
|
|
||||||
} from "@reactive-resume/schema";
|
} from "@reactive-resume/schema";
|
||||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||||
|
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||||
import get from "lodash.get";
|
import get from "lodash.get";
|
||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
|
|
||||||
|
import { BrandIcon } from "../components/brand-icon";
|
||||||
import { Picture } from "../components/picture";
|
import { Picture } from "../components/picture";
|
||||||
import { useArtboardStore } from "../store/artboard";
|
import { useArtboardStore } from "../store/artboard";
|
||||||
import { TemplateProps } from "../types/template";
|
import type { TemplateProps } from "../types/template";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const basics = useArtboardStore((state) => state.resume.basics);
|
const basics = useArtboardStore((state) => state.resume.basics);
|
||||||
const profiles = useArtboardStore((state) => state.resume.sections.profiles);
|
const profiles = useArtboardStore((state) => state.resume.sections.profiles);
|
||||||
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between space-x-4 border-b border-primary pb-5">
|
<div className="flex items-center justify-between space-x-4 border-b border-primary pb-5">
|
||||||
@ -91,15 +89,7 @@ const Header = () => {
|
|||||||
url={item.url}
|
url={item.url}
|
||||||
label={item.username}
|
label={item.username}
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
icon={
|
icon={<BrandIcon slug={item.icon} />}
|
||||||
<img
|
|
||||||
className="ph"
|
|
||||||
width={fontSize}
|
|
||||||
height={fontSize}
|
|
||||||
alt={item.network}
|
|
||||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -119,9 +109,9 @@ const Summary = () => {
|
|||||||
<h4 className="font-bold text-primary">{section.name}</h4>
|
<h4 className="font-bold text-primary">{section.name}</h4>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||||
className="wysiwyg"
|
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
|
className="wysiwyg"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -152,13 +142,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
|||||||
if (!isUrl(url.href)) return null;
|
if (!isUrl(url.href)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5 break-all">
|
||||||
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
||||||
<a
|
<a
|
||||||
href={url.href}
|
href={url.href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("line-clamp-1 max-w-fit", className)}
|
||||||
>
|
>
|
||||||
{label ?? (url.label || url.href)}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
@ -207,7 +197,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || section.items.length === 0) return null;
|
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -233,7 +223,10 @@ const Section = <T,>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||||
|
className="wysiwyg"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -571,7 +564,7 @@ export const Onyx = ({ columns, isFirstPage = false }: TemplateProps) => {
|
|||||||
const [main, sidebar] = columns;
|
const [main, sidebar] = columns;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-custom space-y-4">
|
<div className="space-y-4 p-custom">
|
||||||
{isFirstPage && <Header />}
|
{isFirstPage && <Header />}
|
||||||
|
|
||||||
{main.map((section) => (
|
{main.map((section) => (
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import {
|
import type {
|
||||||
Award,
|
Award,
|
||||||
Certification,
|
Certification,
|
||||||
CustomSection,
|
CustomSection,
|
||||||
CustomSectionGroup,
|
CustomSectionGroup,
|
||||||
Education,
|
|
||||||
Experience,
|
|
||||||
Interest,
|
Interest,
|
||||||
Language,
|
Language,
|
||||||
Profile,
|
Profile,
|
||||||
@ -15,15 +13,16 @@ import {
|
|||||||
SectionWithItem,
|
SectionWithItem,
|
||||||
Skill,
|
Skill,
|
||||||
URL,
|
URL,
|
||||||
Volunteer,
|
|
||||||
} from "@reactive-resume/schema";
|
} from "@reactive-resume/schema";
|
||||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||||
|
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||||
import get from "lodash.get";
|
import get from "lodash.get";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
import { BrandIcon } from "../components/brand-icon";
|
||||||
import { Picture } from "../components/picture";
|
import { Picture } from "../components/picture";
|
||||||
import { useArtboardStore } from "../store/artboard";
|
import { useArtboardStore } from "../store/artboard";
|
||||||
import { TemplateProps } from "../types/template";
|
import type { TemplateProps } from "../types/template";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const basics = useArtboardStore((state) => state.resume.basics);
|
const basics = useArtboardStore((state) => state.resume.basics);
|
||||||
@ -111,9 +110,9 @@ const Summary = () => {
|
|||||||
<h4 className="mb-2 border-b border-primary text-base font-bold">{section.name}</h4>
|
<h4 className="mb-2 border-b border-primary text-base font-bold">{section.name}</h4>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||||
className="wysiwyg"
|
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
|
className="wysiwyg"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -127,15 +126,11 @@ const Rating = ({ level }: RatingProps) => (
|
|||||||
<i
|
<i
|
||||||
key={index}
|
key={index}
|
||||||
className={cn(
|
className={cn(
|
||||||
"ph ph-diamond text-primary",
|
"ph ph-bold ph-diamond text-primary",
|
||||||
level > index && "ph-fill",
|
level > index && "ph-fill",
|
||||||
level <= index && "ph-bold",
|
level <= index && "ph-bold",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
// <div
|
|
||||||
// key={index}
|
|
||||||
// className={cn("h-2 w-4 border border-primary", level > index && "bg-primary")}
|
|
||||||
// />
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -152,7 +147,7 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
|||||||
if (!isUrl(url.href)) return null;
|
if (!isUrl(url.href)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5 break-all">
|
||||||
{!iconOnRight &&
|
{!iconOnRight &&
|
||||||
(icon ?? (
|
(icon ?? (
|
||||||
<i className="ph ph-bold ph-link text-primary group-[.summary]:text-background" />
|
<i className="ph ph-bold ph-link text-primary group-[.summary]:text-background" />
|
||||||
@ -161,7 +156,7 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
|||||||
href={url.href}
|
href={url.href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("line-clamp-1 max-w-fit", className)}
|
||||||
>
|
>
|
||||||
{label ?? (url.label || url.href)}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
@ -213,7 +208,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || section.items.length === 0) return null;
|
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -239,7 +234,10 @@ const Section = <T,>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||||
|
className="wysiwyg"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -257,26 +255,13 @@ const Section = <T,>({
|
|||||||
|
|
||||||
const Profiles = () => {
|
const Profiles = () => {
|
||||||
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
||||||
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section<Profile> section={section}>
|
<Section<Profile> section={section}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div>
|
<div>
|
||||||
{isUrl(item.url.href) ? (
|
{isUrl(item.url.href) ? (
|
||||||
<Link
|
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
|
||||||
url={item.url}
|
|
||||||
label={item.username}
|
|
||||||
icon={
|
|
||||||
<img
|
|
||||||
className="ph"
|
|
||||||
width={fontSize}
|
|
||||||
height={fontSize}
|
|
||||||
alt={item.network}
|
|
||||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<p>{item.username}</p>
|
<p>{item.username}</p>
|
||||||
)}
|
)}
|
||||||
@ -612,7 +597,7 @@ export const Pikachu = ({ columns, isFirstPage = false }: TemplateProps) => {
|
|||||||
const [main, sidebar] = columns;
|
const [main, sidebar] = columns;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-custom grid grid-cols-3 space-x-6">
|
<div className="grid grid-cols-3 space-x-6 p-custom">
|
||||||
<div className="sidebar group space-y-4">
|
<div className="sidebar group space-y-4">
|
||||||
{isFirstPage && <Picture className="w-full !max-w-none" />}
|
{isFirstPage && <Picture className="w-full !max-w-none" />}
|
||||||
|
|
||||||
@ -621,7 +606,7 @@ export const Pikachu = ({ columns, isFirstPage = false }: TemplateProps) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="main group col-span-2 space-y-4">
|
<div className={cn("main group space-y-4", sidebar.length > 0 ? "col-span-2" : "col-span-3")}>
|
||||||
{isFirstPage && <Header />}
|
{isFirstPage && <Header />}
|
||||||
|
|
||||||
{main.map((section) => (
|
{main.map((section) => (
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import {
|
import type {
|
||||||
Award,
|
Award,
|
||||||
Certification,
|
Certification,
|
||||||
CustomSection,
|
CustomSection,
|
||||||
CustomSectionGroup,
|
CustomSectionGroup,
|
||||||
Education,
|
|
||||||
Experience,
|
|
||||||
Interest,
|
Interest,
|
||||||
Language,
|
Language,
|
||||||
Profile,
|
Profile,
|
||||||
@ -15,15 +13,16 @@ import {
|
|||||||
SectionWithItem,
|
SectionWithItem,
|
||||||
Skill,
|
Skill,
|
||||||
URL,
|
URL,
|
||||||
Volunteer,
|
|
||||||
} from "@reactive-resume/schema";
|
} from "@reactive-resume/schema";
|
||||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
import { Education, Experience, Volunteer } from "@reactive-resume/schema";
|
||||||
|
import { cn, isEmptyString, isUrl, sanitize } from "@reactive-resume/utils";
|
||||||
import get from "lodash.get";
|
import get from "lodash.get";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
import { BrandIcon } from "../components/brand-icon";
|
||||||
import { Picture } from "../components/picture";
|
import { Picture } from "../components/picture";
|
||||||
import { useArtboardStore } from "../store/artboard";
|
import { useArtboardStore } from "../store/artboard";
|
||||||
import { TemplateProps } from "../types/template";
|
import type { TemplateProps } from "../types/template";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const basics = useArtboardStore((state) => state.resume.basics);
|
const basics = useArtboardStore((state) => state.resume.basics);
|
||||||
@ -91,9 +90,9 @@ const Summary = () => {
|
|||||||
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
|
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
|
||||||
className="wysiwyg"
|
|
||||||
style={{ columns: section.columns }}
|
style={{ columns: section.columns }}
|
||||||
|
className="wysiwyg"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -124,13 +123,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
|||||||
if (!isUrl(url.href)) return null;
|
if (!isUrl(url.href)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5 break-all border-r pr-2 last:border-r-0 last:pr-0">
|
||||||
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
||||||
<a
|
<a
|
||||||
href={url.href}
|
href={url.href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener nofollow"
|
rel="noreferrer noopener nofollow"
|
||||||
className={cn("inline-block", className)}
|
className={cn("line-clamp-1 max-w-fit", className)}
|
||||||
>
|
>
|
||||||
{label ?? (url.label || url.href)}
|
{label ?? (url.label || url.href)}
|
||||||
</a>
|
</a>
|
||||||
@ -179,7 +178,7 @@ const Section = <T,>({
|
|||||||
summaryKey,
|
summaryKey,
|
||||||
keywordsKey,
|
keywordsKey,
|
||||||
}: SectionProps<T>) => {
|
}: SectionProps<T>) => {
|
||||||
if (!section.visible || section.items.length === 0) return null;
|
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className="grid">
|
<section id={section.id} className="grid">
|
||||||
@ -205,7 +204,10 @@ const Section = <T,>({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{summary !== undefined && !isEmptyString(summary) && (
|
{summary !== undefined && !isEmptyString(summary) && (
|
||||||
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
|
||||||
|
className="wysiwyg"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{level !== undefined && level > 0 && <Rating level={level} />}
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
||||||
@ -223,26 +225,13 @@ const Section = <T,>({
|
|||||||
|
|
||||||
const Profiles = () => {
|
const Profiles = () => {
|
||||||
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
||||||
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section<Profile> section={section}>
|
<Section<Profile> section={section}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div>
|
<div>
|
||||||
{isUrl(item.url.href) ? (
|
{isUrl(item.url.href) ? (
|
||||||
<Link
|
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
|
||||||
url={item.url}
|
|
||||||
label={item.username}
|
|
||||||
icon={
|
|
||||||
<img
|
|
||||||
className="ph"
|
|
||||||
width={fontSize}
|
|
||||||
height={fontSize}
|
|
||||||
alt={item.network}
|
|
||||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<p>{item.username}</p>
|
<p>{item.username}</p>
|
||||||
)}
|
)}
|
||||||
@ -578,7 +567,7 @@ export const Rhyhorn = ({ columns, isFirstPage = false }: TemplateProps) => {
|
|||||||
const [main, sidebar] = columns;
|
const [main, sidebar] = columns;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-custom space-y-4">
|
<div className="space-y-4 p-custom">
|
||||||
{isFirstPage && <Header />}
|
{isFirstPage && <Header />}
|
||||||
|
|
||||||
{main.map((section) => (
|
{main.map((section) => (
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { SectionKey } from "@reactive-resume/schema";
|
import type { SectionKey } from "@reactive-resume/schema";
|
||||||
|
|
||||||
export type TemplateProps = {
|
export type TemplateProps = {
|
||||||
columns: SectionKey[][];
|
columns: SectionKey[][];
|
||||||
|
|||||||
@ -3,17 +3,10 @@
|
|||||||
"ignorePatterns": ["!**/*"],
|
"ignorePatterns": ["!**/*"],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
"files": ["*.ts", "*.tsx"],
|
||||||
"extends": [
|
"extends": ["plugin:@tanstack/eslint-plugin-query/recommended"],
|
||||||
"plugin:tailwindcss/recommended",
|
"parserOptions": {
|
||||||
"plugin:@tanstack/eslint-plugin-query/recommended"
|
"projectService": "./apps/client/tsconfig.json"
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"tailwindcss": {
|
|
||||||
"callees": ["cn", "clsx", "cva"],
|
|
||||||
"config": "tailwind.config.js",
|
|
||||||
"whitelist": ["ph", "ph-"]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"plugins": ["lingui"],
|
"plugins": ["lingui"],
|
||||||
"rules": {
|
"rules": {
|
||||||
@ -39,8 +32,59 @@
|
|||||||
"lingui/no-unlocalized-strings": [
|
"lingui/no-unlocalized-strings": [
|
||||||
2,
|
2,
|
||||||
{
|
{
|
||||||
"ignoreFunction": ["cn"],
|
"ignore": [
|
||||||
"ignoreAttribute": ["alt"]
|
// Ignore strings which are a single "word" (no spaces)
|
||||||
|
// and doesn't start with an uppercase letter
|
||||||
|
"^(?![A-Z])\\S+$",
|
||||||
|
// Ignore UPPERCASE literals
|
||||||
|
// Example: const test = "FOO"
|
||||||
|
"^[A-Z0-9_-]+$"
|
||||||
|
],
|
||||||
|
"ignoreNames": [
|
||||||
|
// Ignore matching className (case-insensitive)
|
||||||
|
{ "regex": { "pattern": "className", "flags": "i" } },
|
||||||
|
// Ignore UPPERCASE names
|
||||||
|
// Example: test.FOO = "ola!"
|
||||||
|
{ "regex": { "pattern": "^[A-Z0-9_-]+$" } },
|
||||||
|
"id",
|
||||||
|
"src",
|
||||||
|
"srcSet",
|
||||||
|
"styleName",
|
||||||
|
"placeholder",
|
||||||
|
"alt",
|
||||||
|
"type",
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"displayName",
|
||||||
|
"Authorization"
|
||||||
|
],
|
||||||
|
"ignoreFunctions": [
|
||||||
|
"cn",
|
||||||
|
"cva",
|
||||||
|
"track",
|
||||||
|
"Error",
|
||||||
|
"console.*",
|
||||||
|
"*headers.set",
|
||||||
|
"*.addEventListener",
|
||||||
|
"*.removeEventListener",
|
||||||
|
"*.postMessage",
|
||||||
|
"*.getElementById",
|
||||||
|
"*.dispatch",
|
||||||
|
"*.commit",
|
||||||
|
"*.includes",
|
||||||
|
"*.indexOf",
|
||||||
|
"*.endsWith",
|
||||||
|
"*.startsWith",
|
||||||
|
"require"
|
||||||
|
],
|
||||||
|
// Following settings require typed linting https://typescript-eslint.io/getting-started/typed-linting/
|
||||||
|
"useTsTypes": true,
|
||||||
|
"ignoreMethodsOnTypes": [
|
||||||
|
// Ignore specified methods on Map and Set types
|
||||||
|
"Map.get",
|
||||||
|
"Map.has",
|
||||||
|
"Set.has"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"lingui/t-call-in-function": 2,
|
"lingui/t-call-in-function": 2,
|
||||||
|
|||||||
@ -35,13 +35,13 @@
|
|||||||
<!-- Styles -->
|
<!-- Styles -->
|
||||||
<link rel="stylesheet" href="/src/styles/main.css" />
|
<link rel="stylesheet" href="/src/styles/main.css" />
|
||||||
</head>
|
</head>
|
||||||
<body class="text-sm antialiased bg-background text-foreground print:bg-white print:m-0">
|
<body class="bg-background text-sm text-foreground antialiased print:m-0 print:bg-white">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
|
||||||
<!-- Phosphor Icons -->
|
<!-- Phosphor Icons -->
|
||||||
<script src="https://unpkg.com/@phosphor-icons/web"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/@phosphor-icons/web"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
68
apps/client/public/funding.json
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://fundingjson.org/schema/v1.0.0/funding.schema.json",
|
||||||
|
"version": "v1.0.0",
|
||||||
|
"entity": {
|
||||||
|
"type": "individual",
|
||||||
|
"role": "maintainer",
|
||||||
|
"name": "Amruth Pillai",
|
||||||
|
"email": "im.amruth@gmail.com",
|
||||||
|
"description": "Software Engineer",
|
||||||
|
"webpageUrl": {
|
||||||
|
"url": "https://rxresu.me/funding.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"projects": [
|
||||||
|
{
|
||||||
|
"guid": "reactive-resume",
|
||||||
|
"name": "Reactive Resume",
|
||||||
|
"description": "A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.",
|
||||||
|
"webpageUrl": {
|
||||||
|
"url": "https://rxresu.me"
|
||||||
|
},
|
||||||
|
"repositoryUrl": {
|
||||||
|
"url": "https://github.com/AmruthPillai/Reactive-Resume",
|
||||||
|
"wellKnown": "https://github.com/AmruthPillai/Reactive-Resume/blob/main/.github/.well-known/funding-manifest-urls"
|
||||||
|
},
|
||||||
|
"licenses": ["spdx:MIT"],
|
||||||
|
"tags": ["data", "design", "productivity", "resume-builder"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"plans": [
|
||||||
|
{
|
||||||
|
"guid": "sponsor",
|
||||||
|
"status": "active",
|
||||||
|
"name": "Sponsor",
|
||||||
|
"description": "Support the project on a recurring basis by becoming a sponsor.",
|
||||||
|
"amount": 10,
|
||||||
|
"currency": "EUR",
|
||||||
|
"frequency": "monthly",
|
||||||
|
"channels": ["github", "open-collective"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"guid": "donation",
|
||||||
|
"status": "active",
|
||||||
|
"name": "Donation",
|
||||||
|
"description": "Show your support for the project by making a one-time donation.",
|
||||||
|
"amount": 0,
|
||||||
|
"currency": "EUR",
|
||||||
|
"frequency": "one-time",
|
||||||
|
"channels": ["github", "open-collective"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"channels": [
|
||||||
|
{
|
||||||
|
"guid": "github",
|
||||||
|
"type": "payment-provider",
|
||||||
|
"description": "GitHub Sponsors",
|
||||||
|
"address": "https://github.com/sponsors/AmruthPillai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"guid": "open-collective",
|
||||||
|
"type": "payment-provider",
|
||||||
|
"description": "Open Collective",
|
||||||
|
"address": "https://opencollective.com/reactive-resume"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
155
apps/client/public/styles/prism-dark.css
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
color: #f8f8f2;
|
||||||
|
background: none;
|
||||||
|
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
|
||||||
|
-webkit-hyphens: none;
|
||||||
|
-moz-hyphens: none;
|
||||||
|
-ms-hyphens: none;
|
||||||
|
hyphens: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks */
|
||||||
|
pre[class*="language-"] {
|
||||||
|
padding: 1em;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(pre) > code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
background: #2b2b2b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline code */
|
||||||
|
:not(pre) > code[class*="language-"] {
|
||||||
|
padding: 0.1em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: #d4d0ab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation {
|
||||||
|
color: #fefefe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.property,
|
||||||
|
.token.tag,
|
||||||
|
.token.constant,
|
||||||
|
.token.symbol,
|
||||||
|
.token.deleted {
|
||||||
|
color: #ffa07a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.boolean,
|
||||||
|
.token.number {
|
||||||
|
color: #00e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.selector,
|
||||||
|
.token.attr-name,
|
||||||
|
.token.string,
|
||||||
|
.token.char,
|
||||||
|
.token.builtin,
|
||||||
|
.token.inserted {
|
||||||
|
color: #abe338;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string,
|
||||||
|
.token.variable {
|
||||||
|
color: #00e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.function {
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.keyword {
|
||||||
|
color: #00e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.regex,
|
||||||
|
.token.important {
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.important,
|
||||||
|
.token.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.entity {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (forced-colors: active) {
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
color: windowText;
|
||||||
|
background: window;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(pre) > code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
background: window;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.important {
|
||||||
|
background: highlight;
|
||||||
|
color: window;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.function,
|
||||||
|
.token.keyword,
|
||||||
|
.token.operator,
|
||||||
|
.token.selector {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.attr-value,
|
||||||
|
.token.comment,
|
||||||
|
.token.doctype,
|
||||||
|
.token.function,
|
||||||
|
.token.keyword,
|
||||||
|
.token.operator,
|
||||||
|
.token.property,
|
||||||
|
.token.string {
|
||||||
|
color: highlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.attr-value,
|
||||||
|
.token.url {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
167
apps/client/public/styles/prism-light.css
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
color: #393a34;
|
||||||
|
font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace;
|
||||||
|
direction: ltr;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: normal;
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 1.2em;
|
||||||
|
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
|
||||||
|
-webkit-hyphens: none;
|
||||||
|
-moz-hyphens: none;
|
||||||
|
-ms-hyphens: none;
|
||||||
|
hyphens: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > code[class*="language-"] {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[class*="language-"]::-moz-selection,
|
||||||
|
pre[class*="language-"] ::-moz-selection,
|
||||||
|
code[class*="language-"]::-moz-selection,
|
||||||
|
code[class*="language-"] ::-moz-selection {
|
||||||
|
background: #c1def1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[class*="language-"]::selection,
|
||||||
|
pre[class*="language-"] ::selection,
|
||||||
|
code[class*="language-"]::selection,
|
||||||
|
code[class*="language-"] ::selection {
|
||||||
|
background: #c1def1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks */
|
||||||
|
pre[class*="language-"] {
|
||||||
|
padding: 1em;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #dddddd;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline code */
|
||||||
|
:not(pre) > code[class*="language-"] {
|
||||||
|
padding: 0.2em;
|
||||||
|
padding-top: 1px;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border: 1px solid #dddddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: #008000;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.namespace {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.string {
|
||||||
|
color: #a31515;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation,
|
||||||
|
.token.operator {
|
||||||
|
color: #393a34; /* no highlight */
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.url,
|
||||||
|
.token.symbol,
|
||||||
|
.token.number,
|
||||||
|
.token.boolean,
|
||||||
|
.token.variable,
|
||||||
|
.token.constant,
|
||||||
|
.token.inserted {
|
||||||
|
color: #36acaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.atrule,
|
||||||
|
.token.keyword,
|
||||||
|
.token.attr-value,
|
||||||
|
.language-autohotkey .token.selector,
|
||||||
|
.language-json .token.boolean,
|
||||||
|
.language-json .token.number,
|
||||||
|
code[class*="language-css"] {
|
||||||
|
color: #0000ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.function {
|
||||||
|
color: #393a34;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.deleted,
|
||||||
|
.language-autohotkey .token.tag {
|
||||||
|
color: #9a050f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.selector,
|
||||||
|
.language-autohotkey .token.keyword {
|
||||||
|
color: #00009f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.important {
|
||||||
|
color: #e90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.important,
|
||||||
|
.token.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.class-name,
|
||||||
|
.language-json .token.property {
|
||||||
|
color: #2b91af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.tag,
|
||||||
|
.token.selector {
|
||||||
|
color: #800000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.attr-name,
|
||||||
|
.token.property,
|
||||||
|
.token.regex,
|
||||||
|
.token.entity {
|
||||||
|
color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.directive.tag .tag {
|
||||||
|
background: #ffff00;
|
||||||
|
color: #393a34;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* overrides color-values for the Line Numbers plugin
|
||||||
|
* http://prismjs.com/plugins/line-numbers/
|
||||||
|
*/
|
||||||
|
.line-numbers.line-numbers .line-numbers-rows {
|
||||||
|
border-right-color: #a5a5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers .line-numbers-rows > span:before {
|
||||||
|
color: #2b91af;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* overrides color-values for the Line Highlight plugin
|
||||||
|
* http://prismjs.com/plugins/line-highlight/
|
||||||
|
*/
|
||||||
|
.line-highlight.line-highlight {
|
||||||
|
background: rgba(193, 222, 241, 0.2);
|
||||||
|
background: -webkit-linear-gradient(left, rgba(193, 222, 241, 0.2) 70%, rgba(221, 222, 241, 0));
|
||||||
|
background: linear-gradient(to right, rgba(193, 222, 241, 0.2) 70%, rgba(221, 222, 241, 0));
|
||||||
|
}
|
||||||
8
apps/client/public/support-logos/linkedin.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<!-- Original Author: (Simple Icons)[https://github.com/simple-icons/simple-icons] -->
|
||||||
|
|
||||||
|
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<title>LinkedIn</title>
|
||||||
|
<path
|
||||||
|
fill="#0a66c2"
|
||||||
|
d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 730 B |
0
apps/client/public/templates/jpg/azurill.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
0
apps/client/public/templates/jpg/bronzor.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
0
apps/client/public/templates/jpg/chikorita.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 145 KiB |
0
apps/client/public/templates/jpg/ditto.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
0
apps/client/public/templates/jpg/kakuna.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
0
apps/client/public/templates/jpg/nosepass.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
0
apps/client/public/templates/jpg/onyx.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
0
apps/client/public/templates/jpg/pikachu.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
0
apps/client/public/templates/jpg/rhyhorn.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
@ -289,7 +289,7 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"css": {
|
"css": {
|
||||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||||
"visible": false
|
"visible": false
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
|
|||||||
@ -314,7 +314,7 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"css": {
|
"css": {
|
||||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||||
"visible": false
|
"visible": false
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
|
|||||||
@ -314,7 +314,7 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"css": {
|
"css": {
|
||||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||||
"visible": false
|
"visible": false
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
|
|||||||
@ -315,7 +315,7 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"css": {
|
"css": {
|
||||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||||
"visible": false
|
"visible": false
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
|
|||||||
@ -289,7 +289,7 @@
|
|||||||
[[], []]
|
[[], []]
|
||||||
],
|
],
|
||||||
"css": {
|
"css": {
|
||||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||||
"visible": false
|
"visible": false
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
|
|||||||
@ -288,7 +288,7 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"css": {
|
"css": {
|
||||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||||
"visible": false
|
"visible": false
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
|
|||||||
@ -287,7 +287,7 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"css": {
|
"css": {
|
||||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||||
"visible": false
|
"visible": false
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
|
|||||||
@ -289,7 +289,7 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"css": {
|
"css": {
|
||||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||||
"visible": false
|
"visible": false
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
|
|||||||
@ -306,7 +306,7 @@
|
|||||||
[["projects", "certifications", "skills", "languages", "references"], []]
|
[["projects", "certifications", "skills", "languages", "references"], []]
|
||||||
],
|
],
|
||||||
"css": {
|
"css": {
|
||||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||||
"visible": false
|
"visible": false
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
|
|||||||
@ -287,7 +287,7 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"css": {
|
"css": {
|
||||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||||
"visible": false
|
"visible": false
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
|
|||||||
@ -315,7 +315,7 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"css": {
|
"css": {
|
||||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||||
"visible": false
|
"visible": false
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
|
|||||||
@ -288,7 +288,7 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"css": {
|
"css": {
|
||||||
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
|
||||||
"visible": false
|
"visible": false
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { t } from "@lingui/macro";
|
import { t } from "@lingui/macro";
|
||||||
import {
|
import {
|
||||||
CaretDown,
|
CaretDownIcon,
|
||||||
ChatTeardropText,
|
ChatTeardropTextIcon,
|
||||||
CircleNotch,
|
CircleNotchIcon,
|
||||||
Exam,
|
ExamIcon,
|
||||||
MagicWand,
|
MagicWandIcon,
|
||||||
PenNib,
|
PenNibIcon,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
@ -75,27 +75,31 @@ export const AiActions = ({ value, onChange, className }: Props) => {
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
className="-rotate-90 bg-background px-2 text-[10px] leading-[10px]"
|
className="-rotate-90 bg-background px-2 text-[10px] leading-[10px]"
|
||||||
>
|
>
|
||||||
<MagicWand size={10} className="mr-1" />
|
<MagicWandIcon size={10} className="mr-1" />
|
||||||
{t`AI`}
|
{t`AI`}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button size="sm" variant="outline" disabled={!!loading} onClick={() => onClick("improve")}>
|
<Button size="sm" variant="outline" disabled={!!loading} onClick={() => onClick("improve")}>
|
||||||
{loading === "improve" ? <CircleNotch className="animate-spin" /> : <PenNib />}
|
{loading === "improve" ? <CircleNotchIcon className="animate-spin" /> : <PenNibIcon />}
|
||||||
<span className="ml-2 text-xs">{t`Improve Writing`}</span>
|
<span className="ml-2 text-xs">{t`Improve Writing`}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button size="sm" variant="outline" disabled={!!loading} onClick={() => onClick("fix")}>
|
<Button size="sm" variant="outline" disabled={!!loading} onClick={() => onClick("fix")}>
|
||||||
{loading === "fix" ? <CircleNotch className="animate-spin" /> : <Exam />}
|
{loading === "fix" ? <CircleNotchIcon className="animate-spin" /> : <ExamIcon />}
|
||||||
<span className="ml-2 text-xs">{t`Fix Spelling & Grammar`}</span>
|
<span className="ml-2 text-xs">{t`Fix Spelling & Grammar`}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button size="sm" variant="outline" disabled={!!loading}>
|
<Button size="sm" variant="outline" disabled={!!loading}>
|
||||||
{loading === "tone" ? <CircleNotch className="animate-spin" /> : <ChatTeardropText />}
|
{loading === "tone" ? (
|
||||||
|
<CircleNotchIcon className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ChatTeardropTextIcon />
|
||||||
|
)}
|
||||||
<span className="mx-2 text-xs">{t`Change Tone`}</span>
|
<span className="mx-2 text-xs">{t`Change Tone`}</span>
|
||||||
<CaretDown />
|
<CaretDownIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
|
|||||||
38
apps/client/src/components/brand-icon.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { forwardRef, useEffect } from "react";
|
||||||
|
import { useDebounceValue } from "usehooks-ts";
|
||||||
|
|
||||||
|
type BrandIconProps = {
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BrandIcon = forwardRef<HTMLImageElement, BrandIconProps>(({ slug }, ref) => {
|
||||||
|
const [debouncedSlug, setValue] = useDebounceValue(slug, 600);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(slug);
|
||||||
|
}, [slug]);
|
||||||
|
|
||||||
|
if (!slug) return null;
|
||||||
|
|
||||||
|
if (debouncedSlug === "linkedin") {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
ref={ref}
|
||||||
|
alt="LinkedIn"
|
||||||
|
className="size-5"
|
||||||
|
src={`${window.location.origin}/support-logos/linkedin.svg`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
ref={ref}
|
||||||
|
alt={debouncedSlug}
|
||||||
|
className="size-5"
|
||||||
|
src={`https://cdn.simpleicons.org/${debouncedSlug}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
BrandIcon.displayName = "BrandIcon";
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { t } from "@lingui/macro";
|
import { t } from "@lingui/macro";
|
||||||
import { CaretDown, Check } from "@phosphor-icons/react";
|
import { CaretDownIcon, CheckIcon } from "@phosphor-icons/react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Command,
|
Command,
|
||||||
@ -61,7 +61,7 @@ export const LocaleCombobox = ({ value, onValueChange }: Props) => {
|
|||||||
onValueChange(result.original.locale);
|
onValueChange(result.original.locale);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Check
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 size-4 opacity-0",
|
"mr-2 size-4 opacity-0",
|
||||||
value === original.locale && "opacity-100",
|
value === original.locale && "opacity-100",
|
||||||
@ -104,7 +104,7 @@ export const LocaleComboboxPopover = ({ value, onValueChange }: Props) => {
|
|||||||
<span className="line-clamp-1 text-left font-normal">
|
<span className="line-clamp-1 text-left font-normal">
|
||||||
{selected?.name} <span className="ml-1 text-xs opacity-50">({selected?.locale})</span>
|
{selected?.name} <span className="ml-1 text-xs opacity-50">({selected?.locale})</span>
|
||||||
</span>
|
</span>
|
||||||
<CaretDown
|
<CaretDownIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-2 size-4 shrink-0 rotate-0 opacity-50 transition-transform",
|
"ml-2 size-4 shrink-0 rotate-0 opacity-50 transition-transform",
|
||||||
open && "rotate-180",
|
open && "rotate-180",
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
|
import { t } from "@lingui/macro";
|
||||||
import { useLingui } from "@lingui/react";
|
import { useLingui } from "@lingui/react";
|
||||||
import { Translate } from "@phosphor-icons/react";
|
import { TranslateIcon } from "@phosphor-icons/react";
|
||||||
import { Button, Popover, PopoverContent, PopoverTrigger } from "@reactive-resume/ui";
|
import { Button, Popover, PopoverContent, PopoverTrigger } from "@reactive-resume/ui";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
@ -13,8 +14,8 @@ export const LocaleSwitch = () => {
|
|||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button size="icon" variant="ghost">
|
<Button size="icon" variant="ghost" aria-label={t`Change Language`}>
|
||||||
<Translate size={20} />
|
<TranslateIcon size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent align="end" className="p-0">
|
<PopoverContent align="end" className="p-0">
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
import { CloudSun, Moon, Sun } from "@phosphor-icons/react";
|
import { t } from "@lingui/macro";
|
||||||
|
import { CloudSunIcon, MoonIcon, SunIcon } from "@phosphor-icons/react";
|
||||||
import { useTheme } from "@reactive-resume/hooks";
|
import { useTheme } from "@reactive-resume/hooks";
|
||||||
import { Button } from "@reactive-resume/ui";
|
import { Button } from "@reactive-resume/ui";
|
||||||
import { motion, Variants } from "framer-motion";
|
import type { Variants } from "framer-motion";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
size?: number;
|
size?: number;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ThemeSwitch = ({ size = 20 }: Props) => {
|
export const ThemeSwitch = ({ size = 20, className }: Props) => {
|
||||||
const { theme, toggleTheme } = useTheme();
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
|
||||||
const variants: Variants = useMemo(() => {
|
const variants: Variants = useMemo(() => {
|
||||||
@ -20,12 +23,12 @@ export const ThemeSwitch = ({ size = 20 }: Props) => {
|
|||||||
}, [size]);
|
}, [size]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button size="icon" variant="ghost" onClick={toggleTheme}>
|
<Button size="icon" variant="ghost" className={className} onClick={toggleTheme}>
|
||||||
<div className="cursor-pointer overflow-hidden" style={{ width: size, height: size }}>
|
<div className="cursor-pointer overflow-hidden" style={{ width: size, height: size }}>
|
||||||
<motion.div animate={theme} variants={variants} className="flex">
|
<motion.div animate={theme} variants={variants} className="flex">
|
||||||
<Sun size={size} className="shrink-0" />
|
<SunIcon size={size} className="shrink-0" aria-label={t`Switch to Light Mode`} />
|
||||||
<CloudSun size={size} className="shrink-0" />
|
<CloudSunIcon size={size} className="shrink-0" aria-label={t`Use System Theme`} />
|
||||||
<Moon size={size} className="shrink-0" />
|
<MoonIcon size={size} className="shrink-0" aria-label={t`Switch to Dark Mode`} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
KeyboardShortcut,
|
KeyboardShortcut,
|
||||||
} from "@reactive-resume/ui";
|
} from "@reactive-resume/ui";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
import { useLogout } from "../services/auth";
|
import { useLogout } from "../services/auth";
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ export const UserOptions = ({ children }: Props) => {
|
|||||||
<DropdownMenuContent side="top" align="start" className="w-48">
|
<DropdownMenuContent side="top" align="start" className="w-48">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate("/dashboard/settings");
|
void navigate("/dashboard/settings");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t`Settings`}
|
{t`Settings`}
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export const DEFAULT_MODEL = "gpt-3.5-turbo";
|
export const DEFAULT_MODEL = "gpt-3.5-turbo";
|
||||||
export const DEFAULT_MAX_TOKENS = 1024;
|
export const DEFAULT_MAX_TOKENS = 1024;
|
||||||
|
export const DEFAULT_AZURE_API_VERSION = "2024-10-21";
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ReactParallaxTiltProps } from "react-parallax-tilt";
|
import type { ReactParallaxTiltProps } from "react-parallax-tilt";
|
||||||
|
|
||||||
export const defaultTiltProps: ReactParallaxTiltProps = {
|
export const defaultTiltProps: ReactParallaxTiltProps = {
|
||||||
scale: 1.05,
|
scale: 1.05,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { QueryKey } from "@tanstack/react-query";
|
import type { QueryKey } from "@tanstack/react-query";
|
||||||
|
|
||||||
export const USER_KEY: QueryKey = ["user"];
|
export const USER_KEY: QueryKey = ["user"];
|
||||||
export const AUTH_PROVIDERS_KEY: QueryKey = ["auth", "providers"];
|
export const AUTH_PROVIDERS_KEY: QueryKey = ["auth", "providers"];
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { createId } from "@paralleldrive/cuid2";
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
import { ToastActionElement, ToastProps } from "@reactive-resume/ui";
|
import type { ToastActionElement, ToastProps } from "@reactive-resume/ui";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
const TOAST_LIMIT = 1;
|
const TOAST_LIMIT = 1;
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { t } from "@lingui/macro";
|
import { t } from "@lingui/macro";
|
||||||
import { deepSearchAndParseDates, ErrorMessage } from "@reactive-resume/utils";
|
import type { ErrorMessage } from "@reactive-resume/utils";
|
||||||
|
import { deepSearchAndParseDates } from "@reactive-resume/utils";
|
||||||
import _axios from "axios";
|
import _axios from "axios";
|
||||||
import createAuthRefreshInterceptor from "axios-auth-refresh";
|
import createAuthRefreshInterceptor from "axios-auth-refresh";
|
||||||
import { redirect } from "react-router-dom";
|
import { redirect } from "react-router";
|
||||||
|
|
||||||
import { refreshToken } from "@/client/services/auth";
|
import { refreshToken } from "@/client/services/auth";
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export const dayjsLocales: Record<string, () => Promise<ILocale>> = {
|
|||||||
"af-ZA": () => import("dayjs/locale/af"),
|
"af-ZA": () => import("dayjs/locale/af"),
|
||||||
"am-ET": () => import("dayjs/locale/am"),
|
"am-ET": () => import("dayjs/locale/am"),
|
||||||
"ar-SA": () => import("dayjs/locale/ar-sa"),
|
"ar-SA": () => import("dayjs/locale/ar-sa"),
|
||||||
|
"az-AZ": () => import("dayjs/locale/az"),
|
||||||
"bg-BG": () => import("dayjs/locale/bg"),
|
"bg-BG": () => import("dayjs/locale/bg"),
|
||||||
"bn-BD": () => import("dayjs/locale/bn"),
|
"bn-BD": () => import("dayjs/locale/bn"),
|
||||||
"ca-ES": () => import("dayjs/locale/ca"),
|
"ca-ES": () => import("dayjs/locale/ca"),
|
||||||
@ -35,17 +36,21 @@ export const dayjsLocales: Record<string, () => Promise<ILocale>> = {
|
|||||||
"kn-IN": () => import("dayjs/locale/kn"),
|
"kn-IN": () => import("dayjs/locale/kn"),
|
||||||
"ko-KR": () => import("dayjs/locale/ko"),
|
"ko-KR": () => import("dayjs/locale/ko"),
|
||||||
"lt-LT": () => import("dayjs/locale/lt"),
|
"lt-LT": () => import("dayjs/locale/lt"),
|
||||||
|
"lv-LV": () => import("dayjs/locale/lv"),
|
||||||
"ml-IN": () => import("dayjs/locale/ml"),
|
"ml-IN": () => import("dayjs/locale/ml"),
|
||||||
"mr-IN": () => import("dayjs/locale/mr"),
|
"mr-IN": () => import("dayjs/locale/mr"),
|
||||||
|
"ms-MY": () => import("dayjs/locale/ms-my"),
|
||||||
"ne-NP": () => import("dayjs/locale/ne"),
|
"ne-NP": () => import("dayjs/locale/ne"),
|
||||||
"nl-NL": () => import("dayjs/locale/nl"),
|
"nl-NL": () => import("dayjs/locale/nl"),
|
||||||
"no-NO": () => import("dayjs/locale/en"),
|
"no-NO": () => import("dayjs/locale/nb"),
|
||||||
"or-IN": () => import("dayjs/locale/en"),
|
"or-IN": () => import("dayjs/locale/en"),
|
||||||
"pl-PL": () => import("dayjs/locale/pl"),
|
"pl-PL": () => import("dayjs/locale/pl"),
|
||||||
"pt-BR": () => import("dayjs/locale/pt-br"),
|
"pt-BR": () => import("dayjs/locale/pt-br"),
|
||||||
"pt-PT": () => import("dayjs/locale/pt"),
|
"pt-PT": () => import("dayjs/locale/pt"),
|
||||||
"ro-RO": () => import("dayjs/locale/ro"),
|
"ro-RO": () => import("dayjs/locale/ro"),
|
||||||
"ru-RU": () => import("dayjs/locale/ru"),
|
"ru-RU": () => import("dayjs/locale/ru"),
|
||||||
|
"sk-SK": () => import("dayjs/locale/sk"),
|
||||||
|
"sq-AL": () => import("dayjs/locale/sq"),
|
||||||
"sr-SP": () => import("dayjs/locale/sr"),
|
"sr-SP": () => import("dayjs/locale/sr"),
|
||||||
"sv-SE": () => import("dayjs/locale/sv"),
|
"sv-SE": () => import("dayjs/locale/sv"),
|
||||||
"ta-IN": () => import("dayjs/locale/ta"),
|
"ta-IN": () => import("dayjs/locale/ta"),
|
||||||
@ -53,6 +58,7 @@ export const dayjsLocales: Record<string, () => Promise<ILocale>> = {
|
|||||||
"th-TH": () => import("dayjs/locale/th"),
|
"th-TH": () => import("dayjs/locale/th"),
|
||||||
"tr-TR": () => import("dayjs/locale/tr"),
|
"tr-TR": () => import("dayjs/locale/tr"),
|
||||||
"uk-UA": () => import("dayjs/locale/uk"),
|
"uk-UA": () => import("dayjs/locale/uk"),
|
||||||
|
"uz-UZ": () => import("dayjs/locale/uz"),
|
||||||
"vi-VN": () => import("dayjs/locale/vi"),
|
"vi-VN": () => import("dayjs/locale/vi"),
|
||||||
"zh-CN": () => import("dayjs/locale/zh-cn"),
|
"zh-CN": () => import("dayjs/locale/zh-cn"),
|
||||||
"zh-TW": () => import("dayjs/locale/zh-tw"),
|
"zh-TW": () => import("dayjs/locale/zh-tw"),
|
||||||
|
|||||||
@ -17,7 +17,8 @@ export async function dynamicActivate(locale: string) {
|
|||||||
if (dayjsLocales[locale]) {
|
if (dayjsLocales[locale]) {
|
||||||
dayjs.locale(await dayjsLocales[locale]());
|
dayjs.locale(await dayjsLocales[locale]());
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.error(error);
|
// eslint-disable-next-line lingui/no-unlocalized-strings
|
||||||
|
throw new Error(`Failed to load messages for locale: ${locale}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||