Compare commits

...

155 Commits
v4.3.5 ... main

Author SHA1 Message Date
25021d1b20 Set up CI with Azure Pipelines
[skip ci]
2025-11-07 12:11:28 +01:00
53fdfdf8db Merge pull request #2437 from Sahilbhatane/main
fix(artboard): bypass Google Fonts for local system fonts and post PAGE_LOADED immediately; add utils docstrings and unit tests for isLocalFont issue #2435
2025-11-06 17:19:33 +01:00
8a45f2de4d fixed issue [Bug] <Error MIME al cargar fuente “Arial” desde Google Fonts en exportaciones públicas>
Fixes #2435
2025-11-05 19:08:38 +05:30
4ccc7bae40 Fixed issue [Bug] <Error MIME al cargar fuente “Arial” desde Google Fonts en exportaciones públicas>
Fixes #2435
2025-11-05 18:59:20 +05:30
2585c47de8 Fixed bug [Bug] <Error MIME al cargar fuente “Arial” desde Google Fonts en exportaciones públicas>
Fixes #2435
2025-11-05 16:33:56 +05:30
b49798950a FIxed bug [Bug] <Error MIME al cargar fuente “Arial” desde Google Fonts en exportaciones públicas>
Fixes #2435
2025-11-05 16:12:57 +05:30
03f15f91b3 chore(package): bump version to 4.5.5 2025-11-04 18:36:52 +01:00
8e13d9c3ac Merge pull request #2434 from AmruthPillai/2275-bug-certifications-texts-dont-fit-inside-the-page
Bugfix: links/urls don't fit inside the page
2025-11-04 18:34:42 +01:00
80932eb80c Fix bug: links/urls don't fit inside the page 2025-11-04 18:31:47 +01:00
b2ae2c05d8 chore(package): bump version to 4.5.4 2025-11-04 16:07:10 +01:00
e349fc9bd1 Merge pull request #2421 from LinuxKunaL/main
fix(rich-input.tsx): replace default scrollbar with themed scrollbar in Summary Editor, fixed issue #2420
2025-11-04 15:35:36 +01:00
53213dfb26 Refactor RichInput component: adjust ScrollArea padding and EditorContent min-height 2025-11-04 15:27:40 +01:00
40f27a53b6 Merge branch 'main' into LinuxKunaL/main 2025-11-04 15:23:22 +01:00
2d900600bf update translations 2025-11-04 15:22:53 +01:00
047e9c248f Merge pull request #2426 from abhas20/feature
Added collapsible feature for each section
2025-11-04 15:21:36 +01:00
1cc1c39903 implement expand/collapse all sections 2025-11-04 15:21:17 +01:00
f6f2a29a7a Merge remote-tracking branch 'origin/main' into feature 2025-11-04 14:39:50 +01:00
4368f6a887 Merge pull request #2432 from Ofsen/main
chore(puppeteer): using puppeteer-core instead of puppeteer
2025-11-04 14:39:06 +01:00
f6db9fb387 Merge branch 'main' into Ofsen/main 2025-11-04 14:37:41 +01:00
afa3804bea add wellKnown url 2025-11-04 14:36:22 +01:00
e2f43b4931 add funding-manifest-urls 2025-11-04 14:34:57 +01:00
067fdd0921 remove format script 2025-11-04 12:35:11 +01:00
1296e6bd45 Merge branch 'main' of github.com:AmruthPillai/Reactive-Resume 2025-11-03 23:21:53 +01:00
d883edb51f Add funding.json file and update package.json dependencies
- Introduced a new funding.json file to support project funding details.
- Updated package.json to version 4.5.3 and modified various dependencies, including @babel/core, @babel/preset-react, and others to their latest versions.
- Adjusted pnpm-lock.yaml to reflect the changes in dependencies and their versions.
2025-11-03 23:21:29 +01:00
51fcf13d37 chore(puppeteer): using puppeteer-core instead of puppeteer 2025-11-03 20:24:57 +01:00
91ef87b4e0 Fix(builder-page) typo: renamed toogleSection 2025-10-25 08:10:02 +05:30
4c0cc947a2 updated(builder-page): added collapsible feature for each section 2025-10-24 18:38:52 +05:30
6fcb7a4845 Merge pull request #2419 from AmruthPillai/l10n
New Translations from Crowdin
2025-10-18 22:32:56 +02:00
213f96b189 Merge pull request #2415 from GouravNG/feat/rich-input-highlight
[Feature] Highlight the selected options under Rich inputs.
2025-10-18 22:32:23 +02:00
e8d6d4ad3a Merge pull request #2412 from alay-dev/main
fix(template): fix gengar template summary section not rendering in sidebar
2025-10-18 22:31:10 +02:00
87370cfdf0 Apply suggestion from @AmruthPillai 2025-10-18 22:30:51 +02:00
112a644927 Merge pull request #2410 from JuanJesusAlejoSillero/main
docs: add Gengar, Glalie, and Leafish templates while also updating image paths to relative in-repo URLs in README.md
2025-10-18 22:29:49 +02:00
2977e3528a New Crowdin translations by GitHub Action 2025-10-18 00:10:39 +00:00
fd7b5e0dd4 fix(rich-input.tsx): replace default scrollbar with themed scrollbar in Summary Editor, fixed #2420 2025-10-17 01:22:42 +05:30
21fd079f94 feat(rich-input): updating datastate on pressing rich input options
- when selecting the rich input option earlier only aria-pressed="true" was changing and not datastate now updated the code to manually update the data-state
2025-10-13 22:52:25 +05:30
9bdc61b50d feat(rich-input): Highlight the selected options of rich inputs 2025-10-13 22:07:01 +05:30
2de24d5b55 fix(template): fix gengar template summary section not rendering in sidebar 2025-10-12 18:49:44 +05:30
1cba9d0fb9 Merge branch 'AmruthPillai:main' into main 2025-10-11 11:02:20 +05:30
8f532bf4a6 docs: add Gengar, Glalie, and Leafish templates while also updating image paths to relative in-repo URLs in README.md 2025-10-11 01:24:51 +02:00
4efdabd475 Merge branch 'main' of github.com:AmruthPillai/Reactive-Resume 2025-10-09 00:10:28 +02:00
8803101dcd fix for long waiting times 2025-10-09 00:10:25 +02:00
a4a9381b65 Merge pull request #2408 from AmruthPillai/dependabot/npm_and_yarn/npm_and_yarn-2752d6d8c0
Bump nodemailer from 6.10.1 to 7.0.7 in the npm_and_yarn group across 1 directory
2025-10-08 21:55:09 +02:00
618d42019e Bump nodemailer in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [nodemailer](https://github.com/nodemailer/nodemailer).


Updates `nodemailer` from 6.10.1 to 7.0.7
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v6.10.1...v7.0.7)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 7.0.7
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-08 19:51:38 +00:00
d9b56cfb5b - fixes #2401 2025-10-08 21:49:07 +02:00
5624dc4f83 Merge pull request #2402 from GouravNG/revert/EnvExampleFile
revert: readded the .env.example file
2025-10-07 21:25:48 +02:00
313fe47050 Merge pull request #1 from GouravNG/revert/EnvExampleFile
revert: readded the .env.example file
2025-10-07 21:56:25 +05:30
772332661f revert: readded the .env.example file 2025-10-07 21:54:59 +05:30
4c6b512455 Update version to 4.5.0 in package.json and adjust PO revision dates across multiple language files to reflect recent changes. 2025-10-01 09:46:55 +00:00
cc576f5220 Merge pull request #2219 from mtdvlpr/main
fix(templates): don't show section when all items are hidden
2025-10-01 11:30:33 +02:00
b193329c23 Merge pull request #2205 from ImDarkShadow/main
fix(linkedin): use case-insensitive slug check to prevent missing image
2025-10-01 11:29:55 +02:00
8e1459f092 Merge pull request #2244 from m4dd0c/draggable-tags
feat(dialogs): skills, custom-section, and interests dialogs support draggable tags.
2025-10-01 11:27:55 +02:00
e19b55ff65 refactor(auth): streamline logout process by clearing query data 2025-10-01 11:25:04 +02:00
687161df98 Merge pull request #2249 from MagicalGlove/main
fix(bug): clear resumes on logout to prevent seeing resumes from previously logged in accounts
2025-10-01 11:24:30 +02:00
c54cfebf5f Merge branch 'main' of github.com:AmruthPillai/Reactive-Resume 2025-10-01 11:21:09 +02:00
e8d0ed1370 refactor(dashboard): replace useState with useLocalStorage for layout persistence 2025-10-01 11:21:06 +02:00
87a17a5196 Merge pull request #2274 from th3fallen/patch-1
Fix regex for parsing linkedin array links
2025-10-01 11:19:35 +02:00
d19df3389c fix translation for zh-CN 2025-10-01 11:18:00 +02:00
faa4c606e5 Merge branch 'main' of github.com:AmruthPillai/Reactive-Resume 2025-10-01 11:17:03 +02:00
210fbd18ac update translations 2025-10-01 11:16:59 +02:00
3ba0141ab8 Merge pull request #2296 from cleves0315/main
fix(builder): update "Separate Links" translation
2025-10-01 11:16:41 +02:00
279a2ddaeb Merge branch 'main' into main 2025-10-01 11:16:30 +02:00
ea7ee3dd7a Merge pull request #2299 from Moamal-2000/improve-accessibility
fix(homepage): resolve missing button labels and incorrect heading order
2025-10-01 11:14:16 +02:00
a55bea1e07 Merge branch 'main' into improve-accessibility 2025-10-01 11:13:56 +02:00
ed5cb7f17b bump version to v4.4.8 2025-10-01 11:11:59 +02:00
c7f8daaff3 Merge pull request #2375 from Avinash99b/main
fix(printer.service.ts): fix pdf rendering, issue #2374 solved
2025-10-01 11:01:31 +02:00
ac48ad78db Merge pull request #2382 from glconti/main
feat(openai): add Azure OpenAI support with configuration options
2025-10-01 10:56:07 +02:00
c18e18cce8 Merge pull request #2233 from SukkaW/jsd
replace unpkg w/ jsdelivr
2025-10-01 10:55:31 +02:00
65ba13e503 update dependencies 2025-10-01 10:46:46 +02:00
44ec9e1d43 remove version in lint-test-build.yml 2025-10-01 10:40:35 +02:00
7c53949741 fix formatting 2025-10-01 10:39:49 +02:00
6d37769e38 Update dependencies and refactor icon imports across the codebase 2025-10-01 10:38:50 +02:00
c525f8d2cc fix security issue, with notes being visible in public api response 2025-10-01 10:07:04 +02:00
04dfcae898 update links to digitalocean 2025-10-01 09:44:26 +02:00
9b5a99a8ca update hyperlinks 2025-10-01 09:39:26 +02:00
c44f1c5282 update hyperlinks 2025-10-01 09:39:00 +02:00
24dfa99034 Update README with Warp Sponsorship
Added Warp sponsorship section and re-format app description.
2025-10-01 09:38:10 +02:00
35c0177729 fix(openai): update Azure API version and improve error handling 2025-09-15 22:44:28 +02:00
098d67cd8c feat(openai): add Azure OpenAI support with configuration options 2025-09-09 10:14:34 +02:00
d8e0ced54c fix(printer-service): ensure profile image fully loads before PDF render 2025-09-02 08:25:41 +05:30
8e9b409bae fix(printer.service.ts): fix pdf rendering, issue #2374 solved 2025-09-02 07:45:20 +05:30
b995a6b6c0 Merge pull request #2346 from l0b0/main
chore: Remove executable flag from images
2025-08-08 08:58:38 +00:00
3e76a52306 Merge branch 'main' into main 2025-07-13 16:40:03 +02:00
4e91a2e2ef fix build error for ci workflow 2025-07-03 15:40:09 +02:00
4314912d5a security updates, dependencies house-keeping, add new translations etc. 2025-07-03 15:35:00 +02:00
93da5157ff chore: Remove executable flag from images 2025-06-30 14:52:56 +02:00
cc7bc4ffb8 fix(homepage): use h3 instead of h4 to maintain heading order in FeaturesSection component 2025-05-10 12:53:51 +03:00
78eefe9da1 fix(homepage): add accessible names to footer language button and theme toggle buttons 2025-05-10 12:45:13 +03:00
7f7c4acdcb fix(builder): update "Separate Links" translation 2025-05-09 18:32:23 +08:00
a23dbfa3df Fix regex for parsing linkedin array links
The second replace was incorrect, resulting in a link like `https://google.com]` which failed zod validation
2025-04-25 12:55:35 -04:00
249104e7a3 Update logout.ts 2025-03-24 11:14:28 +01:00
76bbe7de6b fix(bug): clear resumes on logout
fix(bug): When logged out, resumes aren't cleared and are therefore visible to next logged in user.
2025-03-24 10:23:14 +01:00
1994dde1f2 chore: handleDragOver moved to outer scope for better perf 2025-03-22 23:22:15 +05:30
a8626e400d feat(dialogs): skills, custom-section, and interests dialogs supports draggable tags 2025-03-20 02:17:34 +05:30
f5136da681 replace unpkg w/ jsdelivr 2025-03-15 22:44:18 +08:00
8efc243e43 fix(templates): don't show section when all items are hidden 2025-02-21 11:00:09 +01:00
cd21860535 Update typography.tsx
fixes #2061
2025-02-09 12:10:14 +01:00
7054623678 chore(dependencies): update TypeScript ESLint and Vitest to latest versions
- Bump @typescript-eslint packages to 8.23.0
- Update Vitest and related packages to 2.1.9
- Minor version upgrades for ts-api-utils and other related dependencies
2025-02-03 19:51:39 +01:00
37781d51f3 fix(linkedin): use case-insensitive slug check to prevent missing image 2025-02-04 00:02:43 +05:30
ec4e43d4fc sync translations from crowdin 2025-02-03 10:14:15 +01:00
1d4529128f fix lint issues 2025-02-03 09:41:21 +01:00
60ed3e2a8d Update lint-test-build.yml 2025-02-02 16:41:31 +01:00
5b67e7c0b4 Update package.json 2025-02-02 16:40:12 +01:00
1399d3c44b Merge pull request #2202 from Pieczasz/feat/2169-project-tags-order
feat(projects): allow reordering of project item tags
2025-02-02 16:39:47 +01:00
eb543cf32d feat(projects): allow reordering of project item tags
Related to #2169
2025-02-02 13:33:39 +01:00
9c6d9833d6 Merge pull request #2201 from AmruthPillai/l10n
New Translations from Crowdin
2025-02-01 04:16:05 +01:00
f50cbd71b7 New Crowdin translations by GitHub Action 2025-02-01 00:10:44 +00:00
3d5b3db321 Merge pull request #2198 from omimouni/main
feat(router): add global error boundary for route errors
2025-01-31 16:08:21 +01:00
e438602773 fix(errorpage): added localProvider, replaced a with Link and added custom messages
- Add LocaleProvider wrapper for i18n support
- Replace a tag with React Router Link components
- Add custom error messages for different HTTP status codes
- Implement proper translation support using Trans and t components
2025-01-31 14:38:17 +01:00
f42b29e4ac feat(router): add global error boundary for route errors
- Use React Router’s useRouteError to get error details.
- Create an ErrorPage component in pages/public/error.tsx.
- Show useful error messages based on status codes.
Add a button to help users go back or retry.
2025-01-31 14:01:18 +01:00
92995d9c2b fix formatting and lint errors 2025-01-30 17:57:14 +01:00
b21f1648c4 Merge pull request #2188 from AmruthPillai/l10n
New Translations from Crowdin
2025-01-30 17:46:57 +01:00
94c04b44df Merge pull request #2194 from Creative-Geek/patch-1
Allow Usage of OpenAI Compatible APIs
2025-01-30 17:46:31 +01:00
54ed0678bf Merge pull request #2195 from ADecametre/main
Fix broken images
2025-01-30 17:45:17 +01:00
817ec96963 Fix TypeError 2025-01-30 09:14:44 -05:00
8ad5458e2a Fix broken images
- Allowed img tags to be used in summaries
2025-01-30 08:56:43 -05:00
a87c5edd60 attempt to fix ci errors 2025-01-30 10:41:38 +01:00
e4327736bd fix formatting issues 2025-01-30 09:35:10 +01:00
1fddbe5f92 Update package.json 2025-01-30 08:19:25 +01:00
2c482e7df8 Merge pull request #2186 from ADecametre/main
Fix broken hyperlinks, images and custom CSS (#2182)
2025-01-30 08:18:47 +01:00
c8edcd3dad Allow usage of openai compatible apis
Changed front-end fields verification to allow the user to enter any openai compatible endpoint and api key.
to do: make a verify connection button and edit paragraphs to reflect the change
2025-01-30 08:49:22 +02:00
a82c25c7cb New Crowdin translations by GitHub Action 2025-01-30 00:09:54 +00:00
73b423030f Removed unused import 2025-01-27 23:10:29 -05:00
809551d0f8 Fix #2182 2025-01-27 19:23:28 -05:00
e795ec64d6 update translations 2025-01-27 09:59:37 +01:00
f8373e4798 Merge pull request #2179 from AmruthPillai/l10n
New Translations from Crowdin
2025-01-26 21:25:38 +01:00
a31c434fbc New Crowdin translations by GitHub Action 2025-01-26 00:10:57 +00:00
1fa8aae80a bump version to v4.4.2 2025-01-26 01:00:49 +01:00
27b60a4df9 fixes #2176, text align was not being reflected in summary sections 2025-01-26 01:00:38 +01:00
d21983aab4 update translations 2025-01-25 00:49:33 +01:00
9406d78653 - include more local fonts, such as Times New Roman and Arial (fixes #2170)
- remove embedding fonts to PDF as it wasn't doing anything
2025-01-25 00:47:39 +01:00
c7ae0e94d7 sanitize all user inputs, fix #2172 2025-01-24 23:53:45 +01:00
308a8e3ae3 update translations 2025-01-24 21:20:12 +01:00
4c90cc1838 fix security vulnerability with update password API route 2025-01-24 21:13:24 +01:00
460a40711e fix issue with missing DialogTitle/DialogDescription, fix issue with hot reloads 2025-01-19 22:01:37 +01:00
18cf814779 fixes #2161, remove v1 validation requirement for openai api url 2025-01-17 22:38:45 +01:00
a9656afbbf sync translations from crowdin 2025-01-17 22:33:13 +01:00
385fe008ce Merge branch 'main' of github.com:AmruthPillai/Reactive-Resume 2025-01-17 22:32:06 +01:00
7e25e853d7 update dependencies 2025-01-17 22:31:57 +01:00
de5adbe3d2 Merge pull request #2160 from AmruthPillai/l10n
New Translations from Crowdin
2025-01-16 22:36:05 +01:00
c239ae3f49 update dependencies 2025-01-16 12:09:14 +01:00
1bfdff5b30 New Crowdin translations by GitHub Action 2025-01-16 00:10:16 +00:00
63db927924 - fixes #2153, attempt to fix 401 unauthorized error when implementing OIDC 2025-01-15 16:32:43 +01:00
9a34e4af27 Merge pull request #2155 from AmruthPillai/l10n
New Translations from Crowdin
2025-01-15 15:59:02 +01:00
b5589338ec New Crowdin translations by GitHub Action 2025-01-15 00:10:04 +00:00
a19059aa76 Merge pull request #2152 from AmruthPillai/2151-bug-openid-authentication-only-works-when-node_env-is-development
fixes #2151: apply secure cookie session only if using SSL (https)
2025-01-14 10:24:11 +01:00
15f962310b fix lint issue 2025-01-14 09:52:26 +01:00
a32def2086 fixes #2151, apply secure cookie session only if using SSL (https) 2025-01-14 09:45:57 +01:00
21af624096 Merge pull request #2149 from AmruthPillai/feat/implement-openid-connect-strategy
Implement OpenID Connect Authentication Strategy
2025-01-13 16:22:43 +01:00
227870ac78 fix issues suggested by coderabbit 2025-01-13 16:22:29 +01:00
33cb3dbd6a ensure secure cookies are used in express-session 2025-01-13 16:04:27 +01:00
eb7813ac6f Implement OpenID Connect Authentication Strategy (works with Keycloak, Authentik etc.) 2025-01-13 15:56:29 +01:00
0f8f2fe560 cleanup internal package resolution 2025-01-13 11:53:42 +01:00
51f38f0884 update translations 2025-01-13 01:50:39 +01:00
6b93fd179d Merge pull request #2147 from AmruthPillai/l10n
New Translations from Crowdin
2025-01-13 01:15:29 +01:00
9385f36832 New Crowdin translations by GitHub Action 2025-01-13 00:10:39 +00:00
309 changed files with 34076 additions and 22155 deletions

View File

@ -68,3 +68,14 @@ STORAGE_SKIP_BUCKET_CHECK=false
# GOOGLE_CLIENT_ID=
# GOOGLE_CLIENT_SECRET=
# 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=

View File

@ -5,7 +5,6 @@
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"extends": ["plugin:prettier/recommended"],
"plugins": ["simple-import-sort", "unused-imports"],
"rules": {
// eslint
@ -42,14 +41,6 @@
}
]
}
],
// prettier
"prettier/prettier": [
"warn",
{
"endOfLine": "auto"
}
]
}
},
@ -78,6 +69,7 @@
"@typescript-eslint/no-misused-promises": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/no-redundant-type-constituents": "off",
"@typescript-eslint/consistent-type-definitions": ["error", "type"],

View File

@ -0,0 +1 @@
https://rxresu.me/funding.json

View File

@ -38,9 +38,6 @@ jobs:
- name: Lint
run: pnpm run lint
- name: Format
run: pnpm run format:check
- name: Test
run: pnpm run test

1
.gitignore vendored
View File

@ -40,6 +40,7 @@ Thumbs.db
# Generated Files
.nx
.swc
.turbo
fly.toml
stats.html

View File

@ -5,7 +5,9 @@
"install": "always",
"packageManager": "pnpm",
"reject": [
"nx",
"eslint",
"@nx/*",
"@swc/*",
"@swc-node/*",
"@reactive-resume/*",

View File

@ -1,3 +1,3 @@
{
"recommendations": ["nrwl.angular-console", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
}

View File

@ -1,9 +1,9 @@
{
"css.validate": false,
"vitest.disableWorkspaceWarning": true,
"typescript.tsdk": "node_modules/typescript/lib",
"i18n-ally.localesPaths": ["apps/client/src/locales"],
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
],
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

@ -7,7 +7,7 @@ ARG NX_CLOUD_ACCESS_TOKEN
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable pnpm && corepack prepare pnpm --activate
RUN corepack enable
WORKDIR /app

View File

@ -1,4 +1,15 @@
![Reactive Resume](https://i.imgur.com/FFc4nyZ.jpg)
<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" />
![App Version](https://img.shields.io/github/package-json/version/AmruthPillai/Reactive-Resume?label=version)
[![Docker Pulls](https://img.shields.io/docker/pulls/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/)
</div>
## 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.
@ -24,17 +37,21 @@ Start creating your standout resume with Reactive Resume today!
## Templates
| 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" /> |
| Azurill | Bronzor | Chikorita |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| <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 |
| ----------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- |
| <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" /> |
| Ditto | Gengar | Glalie |
| -------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| <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 |
| ----------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- |
| <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" /> |
| Kakuna | Leafish | Nosepass |
| ---------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| <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
@ -93,7 +110,7 @@ _By the community, for the community._
A passion project by [Amruth Pillai](https://www.amruthpillai.com/)
<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">
</a>
</p>

View File

@ -4,13 +4,6 @@
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"extends": ["plugin:tailwindcss/recommended"],
"settings": {
"tailwindcss": {
"callees": ["cn", "clsx", "cva"],
"config": "tailwind.config.js"
}
},
"rules": {
// eslint
"@typescript-eslint/no-require-imports": "off",
@ -28,10 +21,7 @@
],
// react-hooks
"react-hooks/exhaustive-deps": "off",
// tailwindcss
"tailwindcss/no-custom-classname": "off"
"react-hooks/exhaustive-deps": "off"
}
},
{

View File

@ -39,6 +39,6 @@
<script type="module" src="/src/main.tsx"></script>
<!-- Phosphor Icons -->
<script src="https://unpkg.com/@phosphor-icons/web"></script>
<script defer src="https://cdn.jsdelivr.net/npm/@phosphor-icons/web"></script>
</body>
</html>

View File

@ -1,17 +1,24 @@
import { forwardRef } from "react";
type BrandIconProps = {
slug: string;
};
export const BrandIcon = ({ slug }: BrandIconProps) => {
if (slug === "linkedin") {
export const BrandIcon = forwardRef<HTMLImageElement, BrandIconProps>(({ slug }, ref) => {
if (slug.toLowerCase() === "linkedin") {
return (
<img
alt="linkedin"
ref={ref}
alt="LinkedIn"
className="size-4"
src={`${window.location.origin}/support-logos/linkedin.svg`}
/>
);
}
return <img alt={slug} className="size-4" src={`https://cdn.simpleicons.org/${slug}`} />;
};
return (
<img ref={ref} alt={slug} className="size-4" src={`https://cdn.simpleicons.org/${slug}`} />
);
});
BrandIcon.displayName = "BrandIcon";

View File

@ -14,8 +14,8 @@ export const Picture = ({ className }: PictureProps) => {
return (
<img
src={picture.url}
alt="Profile"
src={picture.url}
className={cn(
"relative z-20 object-cover",
picture.effects.border && "border-primary",

View File

@ -0,0 +1,5 @@
import { HelmetData } from "react-helmet-async";
export const helmetData = new HelmetData({});
export const helmetContext = helmetData.context;

View File

@ -1,10 +1,13 @@
import { isLocalFont } from "@reactive-resume/utils";
import { useEffect, useMemo } from "react";
import { Helmet } from "react-helmet-async";
import { Outlet } from "react-router";
import webfontloader from "webfontloader";
import { useArtboardStore } from "../store/artboard";
export const ArtboardPage = () => {
const name = useArtboardStore((state) => state.resume.basics.name);
const metadata = useArtboardStore((state) => state.resume.metadata);
const fontString = useMemo(() => {
@ -16,6 +19,18 @@ export const ArtboardPage = () => {
}, [metadata.typography.font]);
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({
google: { families: [fontString] },
active: () => {
@ -57,7 +72,14 @@ export const ArtboardPage = () => {
return (
<>
{metadata.css.visible && <style lang="css">{`[data-page] { ${metadata.css.value} }`}</style>}
<Helmet>
<title>{name} | Reactive Resume</title>
{metadata.css.visible && (
<style id="custom-css" lang="css">
{metadata.css.value}
</style>
)}
</Helmet>
<Outlet />
</>

View File

@ -1,8 +1,10 @@
import { SectionKey } from "@reactive-resume/schema";
import { pageSizeMap, Template } from "@reactive-resume/utils";
import type { SectionKey } from "@reactive-resume/schema";
import type { Template } from "@reactive-resume/utils";
import { pageSizeMap } from "@reactive-resume/utils";
import { AnimatePresence, motion } from "framer-motion";
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 { useArtboardStore } from "../store/artboard";

View File

@ -1,5 +1,5 @@
import { SectionKey } from "@reactive-resume/schema";
import { Template } from "@reactive-resume/utils";
import type { SectionKey } from "@reactive-resume/schema";
import type { Template } from "@reactive-resume/utils";
import { useMemo } from "react";
import { Page } from "../components/page";

View File

@ -1,6 +1,8 @@
import { useEffect } from "react";
import { HelmetProvider } from "react-helmet-async";
import { Outlet } from "react-router";
import { helmetContext } from "../constants/helmet";
import { useArtboardStore } from "../store/artboard";
export const Providers = () => {
@ -10,35 +12,28 @@ export const Providers = () => {
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.origin !== window.location.origin) return;
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");
if (resumeData) {
setResume(JSON.parse(resumeData));
return;
}
window.addEventListener("message", handleMessage);
window.addEventListener("message", handleMessage, false);
return () => {
window.removeEventListener("message", handleMessage);
window.removeEventListener("message", handleMessage, false);
};
}, [setResume]);
}, []);
// Only for testing, in production this will be fetched from window.postMessage
// useEffect(() => {
// setResume(sampleResume);
// }, [setResume]);
useEffect(() => {
const resumeData = window.localStorage.getItem("resume");
if (resumeData) setResume(JSON.parse(resumeData));
}, [window.localStorage.getItem("resume")]);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!resume) return null;
return <Outlet />;
return (
<HelmetProvider context={helmetContext}>
<Outlet />
</HelmetProvider>
);
};

View File

@ -6,7 +6,7 @@ import { PreviewLayout } from "../pages/preview";
import { Providers } from "../providers";
export const routes = createRoutesFromChildren(
<Route element={<Providers />} hydrateFallbackElement={<div>Loading...</div>}>
<Route element={<Providers />}>
<Route path="artboard" element={<ArtboardPage />}>
<Route path="builder" element={<BuilderLayout />} />
<Route path="preview" element={<PreviewLayout />} />

View File

@ -1,4 +1,4 @@
import { ResumeData } from "@reactive-resume/schema";
import type { ResumeData } from "@reactive-resume/schema";
import { create } from "zustand";
export type ArtboardStore = {

View File

@ -8,6 +8,10 @@
@apply border-current;
}
body {
overflow: hidden;
}
#root {
@apply antialiased;
}

View File

@ -1,10 +1,8 @@
import {
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Profile,
@ -15,16 +13,16 @@ import {
SectionWithItem,
Skill,
URL,
Volunteer,
} 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 React, { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import { TemplateProps } from "../types/template";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
@ -100,9 +98,9 @@ const Summary = () => {
<div className="absolute left-[-4.5px] top-[8px] hidden size-[8px] rounded-full bg-primary group-[.main]:block" />
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</main>
</section>
@ -133,13 +131,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
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" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -188,7 +186,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: 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 (
<section id={section.id} className="grid">
@ -226,7 +224,10 @@ const Section = <T,>({
<div>{children?.(item as T)}</div>
{summary !== undefined && !isEmptyString(summary) && (
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -552,7 +553,7 @@ export const Azurill = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom space-y-3">
<div className="space-y-3 p-custom">
{isFirstPage && <Header />}
<div className="grid grid-cols-3 gap-x-4">

View File

@ -1,10 +1,8 @@
import {
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Profile,
@ -15,16 +13,16 @@ import {
SectionWithItem,
Skill,
URL,
Volunteer,
} 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 { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import { TemplateProps } from "../types/template";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
@ -91,9 +89,9 @@ const Summary = () => {
</div>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg col-span-4"
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg col-span-4"
/>
</section>
);
@ -124,13 +122,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
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" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -179,7 +177,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: 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 (
<section id={section.id} className="grid grid-cols-5 border-t pt-2.5">
@ -207,7 +205,10 @@ const Section = <T,>({
</div>
{summary !== undefined && !isEmptyString(summary) && (
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -567,7 +568,7 @@ export const Bronzor = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom space-y-4">
<div className="space-y-4 p-custom">
{isFirstPage && <Header />}
<div className="space-y-4">

View File

@ -1,10 +1,8 @@
import {
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Profile,
@ -15,16 +13,16 @@ import {
SectionWithItem,
Skill,
URL,
Volunteer,
} 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 { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import { TemplateProps } from "../types/template";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
@ -91,9 +89,9 @@ const Summary = () => {
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</section>
);
@ -127,14 +125,14 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
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 group-[.sidebar]:text-white" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -184,7 +182,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: 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 (
<section id={section.id} className="grid">
@ -211,7 +209,7 @@ const Section = <T,>({
{summary !== undefined && !isEmptyString(summary) && (
<div
dangerouslySetInnerHTML={{ __html: summary }}
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg group-[.sidebar]:prose-invert"
/>
)}
@ -573,7 +571,7 @@ export const Chikorita = ({ columns, isFirstPage = false }: TemplateProps) => {
<div className="grid min-h-[inherit] grid-cols-3">
<div
className={cn(
"main p-custom group space-y-4",
"main group space-y-4 p-custom",
sidebar.length > 0 ? "col-span-2" : "col-span-3",
)}
>
@ -586,7 +584,7 @@ export const Chikorita = ({ columns, isFirstPage = false }: TemplateProps) => {
<div
className={cn(
"sidebar p-custom group h-full space-y-4 bg-primary text-background",
"sidebar group h-full space-y-4 bg-primary p-custom text-background",
sidebar.length === 0 && "hidden",
)}
>

View File

@ -1,10 +1,8 @@
import {
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Profile,
@ -15,22 +13,22 @@ import {
SectionWithItem,
Skill,
URL,
Volunteer,
} 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 { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import { TemplateProps } from "../types/template";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
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" />
<div className="relative z-10 col-span-2 text-background">
@ -111,9 +109,9 @@ const Summary = () => {
<h4 className="mb-2 text-base font-bold">{section.name}</h4>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</section>
);
@ -144,13 +142,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
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" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -199,7 +197,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: 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 (
<section id={section.id} className="grid">
@ -232,7 +230,10 @@ const Section = <T,>({
</div>
{summary !== undefined && !isEmptyString(summary) && (
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -603,7 +604,7 @@ export const Ditto = ({ columns, isFirstPage = false }: TemplateProps) => {
)}
<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) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
@ -611,7 +612,7 @@ export const Ditto = ({ columns, isFirstPage = false }: TemplateProps) => {
<div
className={cn(
"main p-custom group space-y-4",
"main group space-y-4 p-custom",
sidebar.length > 0 ? "col-span-2" : "col-span-3",
)}
>

View File

@ -1,10 +1,8 @@
import {
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Profile,
@ -15,22 +13,22 @@ import {
SectionWithItem,
Skill,
URL,
Volunteer,
} 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 { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import { TemplateProps } from "../types/template";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
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" />
<div>
@ -88,12 +86,12 @@ const Summary = () => {
if (!section.visible || isEmptyString(section.content)) return null;
return (
<div className="p-custom space-y-4" style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}>
<div className="space-y-4 p-custom" style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}>
<section id={section.id}>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</section>
</div>
@ -125,7 +123,7 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
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 group-[.sidebar]:text-background" />
@ -134,7 +132,7 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -186,7 +184,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: 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 (
<section id={section.id} className="grid">
@ -212,7 +210,10 @@ const Section = <T,>({
</div>
{summary !== undefined && !isEmptyString(summary) && (
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -530,6 +531,11 @@ const mapSectionToComponent = (section: SectionKey) => {
case "education": {
return <Education />;
}
case "summary": {
return <Summary />;
}
case "awards": {
return <Awards />;
}
@ -581,7 +587,7 @@ export const Gengar = ({ columns, isFirstPage = false }: TemplateProps) => {
{isFirstPage && <Header />}
<div
className="p-custom flex-1 space-y-4"
className="flex-1 space-y-4 p-custom"
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
>
{sidebar.map((section) => (
@ -591,9 +597,7 @@ export const Gengar = ({ columns, isFirstPage = false }: TemplateProps) => {
</div>
<div className={cn("main group", sidebar.length > 0 ? "col-span-2" : "col-span-3")}>
{isFirstPage && <Summary />}
<div className="p-custom space-y-4">
<div className="space-y-4 p-custom">
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}

View File

@ -1,10 +1,8 @@
import {
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Profile,
@ -15,16 +13,23 @@ import {
SectionWithItem,
Skill,
URL,
Volunteer,
} 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 { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import { TemplateProps } from "../types/template";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
@ -91,9 +96,9 @@ const Summary = () => {
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</section>
);
@ -130,14 +135,14 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
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 group-[.sidebar]:text-primary" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -187,7 +192,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: 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 (
<section id={section.id} className="grid">
@ -215,7 +220,10 @@ const Section = <T,>({
</div>
{summary !== undefined && !isEmptyString(summary) && (
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -579,7 +587,7 @@ export const Glalie = ({ columns, isFirstPage = false }: TemplateProps) => {
return (
<div className="grid min-h-[inherit] grid-cols-3">
<div
className={cn("sidebar p-custom group space-y-4", sidebar.length === 0 && "hidden")}
className={cn("sidebar group space-y-4 p-custom", sidebar.length === 0 && "hidden")}
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
>
{isFirstPage && <Header />}
@ -591,7 +599,7 @@ export const Glalie = ({ columns, isFirstPage = false }: TemplateProps) => {
<div
className={cn(
"main p-custom group space-y-4",
"main group space-y-4 p-custom",
sidebar.length > 0 ? "col-span-2" : "col-span-3",
)}
>

View File

@ -1,4 +1,4 @@
import { Template } from "@reactive-resume/utils";
import type { Template } from "@reactive-resume/utils";
import { Azurill } from "./azurill";
import { Bronzor } from "./bronzor";

View File

@ -1,10 +1,8 @@
import {
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Project,
@ -14,16 +12,16 @@ import {
SectionWithItem,
Skill,
URL,
Volunteer,
} 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 React, { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import { TemplateProps } from "../types/template";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
@ -110,9 +108,9 @@ const Summary = () => {
</h4>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</section>
);
@ -143,13 +141,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
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" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -198,7 +196,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: 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 (
<section id={section.id} className="grid">
@ -223,7 +221,10 @@ const Section = <T,>({
<div>{children?.(item as T)}</div>
{summary !== undefined && !isEmptyString(summary) && (
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -523,7 +524,7 @@ export const Kakuna = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom space-y-4">
<div className="space-y-4 p-custom">
{isFirstPage && <Header />}
<div className="space-y-4">

View File

@ -1,10 +1,8 @@
import {
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Project,
@ -14,16 +12,16 @@ import {
SectionWithItem,
Skill,
URL,
Volunteer,
} 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 React, { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import { TemplateProps } from "../types/template";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
@ -34,7 +32,7 @@ const Header = () => {
return (
<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) }}
>
<div className="space-y-3">
@ -44,16 +42,16 @@ const Header = () => {
</div>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</div>
<Picture />
</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">
{basics.location && (
<div className="flex items-center gap-x-1.5">
@ -138,13 +136,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
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" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -193,7 +191,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: 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 (
<section id={section.id} className="grid">
@ -218,7 +216,10 @@ const Section = <T,>({
<div>{children?.(item as T)}</div>
{summary !== undefined && !isEmptyString(summary) && (
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -518,7 +519,7 @@ export const Leafish = ({ columns, isFirstPage = false }: TemplateProps) => {
<div>
{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={cn("grid gap-y-4", sidebar.length === 0 && "col-span-2")}>
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>

View File

@ -1,10 +1,8 @@
import {
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Profile,
@ -15,16 +13,16 @@ import {
SectionWithItem,
Skill,
URL,
Volunteer,
} 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 { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import { TemplateProps } from "../types/template";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
@ -107,9 +105,9 @@ const Summary = () => {
</div>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</div>
</section>
@ -128,13 +126,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
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" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -182,7 +180,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: 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 (
<section id={section.id} className={cn("grid", dateKey !== undefined && "gap-y-4")}>
@ -219,7 +217,10 @@ const Section = <T,>({
{url !== undefined && section.separateLinks && <Link url={url} />}
{summary !== undefined && !isEmptyString(summary) && (
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{keywords !== undefined && keywords.length > 0 && (
@ -254,7 +255,10 @@ const Section = <T,>({
{url !== undefined && section.separateLinks && <Link url={url} />}
{summary !== undefined && !isEmptyString(summary) && (
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{keywords !== undefined && keywords.length > 0 && (
@ -571,7 +575,7 @@ export const Nosepass = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom space-y-6">
<div className="space-y-6 p-custom">
<div className="flex items-center justify-between">
<img alt="Europass Logo" className="h-[42px]" src="/assets/europass.png" />

View File

@ -1,10 +1,8 @@
import {
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Project,
@ -14,16 +12,16 @@ import {
SectionWithItem,
Skill,
URL,
Volunteer,
} 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 React, { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import { TemplateProps } from "../types/template";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
@ -111,9 +109,9 @@ const Summary = () => {
<h4 className="font-bold text-primary">{section.name}</h4>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</section>
);
@ -144,13 +142,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
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" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -199,7 +197,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: 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 (
<section id={section.id} className="grid">
@ -225,7 +223,10 @@ const Section = <T,>({
</div>
{summary !== undefined && !isEmptyString(summary) && (
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -563,7 +564,7 @@ export const Onyx = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom space-y-4">
<div className="space-y-4 p-custom">
{isFirstPage && <Header />}
{main.map((section) => (

View File

@ -1,10 +1,8 @@
import {
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Profile,
@ -15,16 +13,16 @@ import {
SectionWithItem,
Skill,
URL,
Volunteer,
} 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 { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import { TemplateProps } from "../types/template";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
@ -112,9 +110,9 @@ const Summary = () => {
<h4 className="mb-2 border-b border-primary text-base font-bold">{section.name}</h4>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</section>
);
@ -128,15 +126,11 @@ const Rating = ({ level }: RatingProps) => (
<i
key={index}
className={cn(
"ph ph-diamond text-primary",
"ph ph-bold ph-diamond text-primary",
level > index && "ph-fill",
level <= index && "ph-bold",
)}
/>
// <div
// key={index}
// className={cn("h-2 w-4 border border-primary", level > index && "bg-primary")}
// />
))}
</div>
);
@ -153,7 +147,7 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
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 group-[.summary]:text-background" />
@ -162,7 +156,7 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -214,7 +208,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: 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 (
<section id={section.id} className="grid">
@ -240,7 +234,10 @@ const Section = <T,>({
</div>
{summary !== undefined && !isEmptyString(summary) && (
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -600,7 +597,7 @@ export const Pikachu = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
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">
{isFirstPage && <Picture className="w-full !max-w-none" />}

View File

@ -1,10 +1,8 @@
import {
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Profile,
@ -15,16 +13,16 @@ import {
SectionWithItem,
Skill,
URL,
Volunteer,
} 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 { Fragment } from "react";
import { BrandIcon } from "../components/brand-icon";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import { TemplateProps } from "../types/template";
import type { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
@ -92,9 +90,9 @@ const Summary = () => {
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</section>
);
@ -125,13 +123,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5 border-r pr-2 last:border-r-0 last:pr-0">
<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" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -180,7 +178,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: 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 (
<section id={section.id} className="grid">
@ -206,7 +204,10 @@ const Section = <T,>({
</div>
{summary !== undefined && !isEmptyString(summary) && (
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
<div
dangerouslySetInnerHTML={{ __html: sanitize(summary) }}
className="wysiwyg"
/>
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -566,7 +567,7 @@ export const Rhyhorn = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom space-y-4">
<div className="space-y-4 p-custom">
{isFirstPage && <Header />}
{main.map((section) => (

View File

@ -1,4 +1,4 @@
import { SectionKey } from "@reactive-resume/schema";
import type { SectionKey } from "@reactive-resume/schema";
export type TemplateProps = {
columns: SectionKey[][];

View File

@ -4,20 +4,10 @@
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"extends": [
"plugin:tailwindcss/recommended",
"plugin:@tanstack/eslint-plugin-query/recommended"
],
"extends": ["plugin:@tanstack/eslint-plugin-query/recommended"],
"parserOptions": {
"projectService": "./apps/client/tsconfig.json"
},
"settings": {
"tailwindcss": {
"callees": ["cn", "clsx", "cva"],
"config": "tailwind.config.js",
"whitelist": ["ph", "ph\\-.*", "si", "si\\-.*"]
}
},
"plugins": ["lingui"],
"rules": {
// eslint

View File

@ -42,6 +42,6 @@
<script type="module" src="/src/main.tsx"></script>
<!-- Phosphor Icons -->
<script src="https://unpkg.com/@phosphor-icons/web"></script>
<script defer src="https://cdn.jsdelivr.net/npm/@phosphor-icons/web"></script>
</body>
</html>

View 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"
}
]
}
}

0
apps/client/public/templates/jpg/azurill.jpg Executable file → Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

View File

@ -1,11 +1,11 @@
import { t } from "@lingui/macro";
import {
CaretDown,
ChatTeardropText,
CircleNotch,
Exam,
MagicWand,
PenNib,
CaretDownIcon,
ChatTeardropTextIcon,
CircleNotchIcon,
ExamIcon,
MagicWandIcon,
PenNibIcon,
} from "@phosphor-icons/react";
import {
Badge,
@ -75,27 +75,31 @@ export const AiActions = ({ value, onChange, className }: Props) => {
variant="primary"
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`}
</Badge>
</div>
<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>
</Button>
<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>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<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>
<CaretDown />
<CaretDownIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>

View File

@ -1,13 +1,23 @@
import { cn } from "@reactive-resume/utils";
import { forwardRef, useEffect } from "react";
import { useDebounceValue } from "usehooks-ts";
type BrandIconProps = {
slug: string;
};
export const BrandIcon = ({ slug }: BrandIconProps) => {
if (slug === "linkedin") {
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`}
@ -15,5 +25,14 @@ export const BrandIcon = ({ slug }: BrandIconProps) => {
);
}
return <i className={cn("si si--color text-[1.25rem]", `si-${slug}`)} />;
};
return (
<img
ref={ref}
alt={debouncedSlug}
className="size-5"
src={`https://cdn.simpleicons.org/${debouncedSlug}`}
/>
);
});
BrandIcon.displayName = "BrandIcon";

View File

@ -1,5 +1,5 @@
import { t } from "@lingui/macro";
import { CaretDown, Check } from "@phosphor-icons/react";
import { CaretDownIcon, CheckIcon } from "@phosphor-icons/react";
import {
Button,
Command,
@ -61,7 +61,7 @@ export const LocaleCombobox = ({ value, onValueChange }: Props) => {
onValueChange(result.original.locale);
}}
>
<Check
<CheckIcon
className={cn(
"mr-2 size-4 opacity-0",
value === original.locale && "opacity-100",
@ -104,7 +104,7 @@ export const LocaleComboboxPopover = ({ value, onValueChange }: Props) => {
<span className="line-clamp-1 text-left font-normal">
{selected?.name} <span className="ml-1 text-xs opacity-50">({selected?.locale})</span>
</span>
<CaretDown
<CaretDownIcon
className={cn(
"ml-2 size-4 shrink-0 rotate-0 opacity-50 transition-transform",
open && "rotate-180",

View File

@ -1,5 +1,6 @@
import { t } from "@lingui/macro";
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 { useState } from "react";
@ -13,8 +14,8 @@ export const LocaleSwitch = () => {
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button size="icon" variant="ghost">
<Translate size={20} />
<Button size="icon" variant="ghost" aria-label={t`Change Language`}>
<TranslateIcon size={20} />
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="p-0">

View File

@ -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 { 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";
type Props = {
size?: number;
className?: string;
};
export const ThemeSwitch = ({ size = 20 }: Props) => {
export const ThemeSwitch = ({ size = 20, className }: Props) => {
const { theme, toggleTheme } = useTheme();
const variants: Variants = useMemo(() => {
@ -20,12 +23,12 @@ export const ThemeSwitch = ({ size = 20 }: Props) => {
}, [size]);
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 }}>
<motion.div animate={theme} variants={variants} className="flex">
<Sun size={size} className="shrink-0" />
<CloudSun size={size} className="shrink-0" />
<Moon size={size} className="shrink-0" />
<SunIcon size={size} className="shrink-0" aria-label={t`Switch to Light Mode`} />
<CloudSunIcon size={size} className="shrink-0" aria-label={t`Use System Theme`} />
<MoonIcon size={size} className="shrink-0" aria-label={t`Switch to Dark Mode`} />
</motion.div>
</div>
</Button>

View File

@ -1,2 +1,3 @@
export const DEFAULT_MODEL = "gpt-3.5-turbo";
export const DEFAULT_MAX_TOKENS = 1024;
export const DEFAULT_AZURE_API_VERSION = "2024-10-21";

View File

@ -1,4 +1,4 @@
import { ReactParallaxTiltProps } from "react-parallax-tilt";
import type { ReactParallaxTiltProps } from "react-parallax-tilt";
export const defaultTiltProps: ReactParallaxTiltProps = {
scale: 1.05,

View File

@ -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 AUTH_PROVIDERS_KEY: QueryKey = ["auth", "providers"];

View File

@ -1,5 +1,5 @@
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";
const TOAST_LIMIT = 1;

View File

@ -1,5 +1,6 @@
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 createAuthRefreshInterceptor from "axios-auth-refresh";
import { redirect } from "react-router";

View File

@ -13,6 +13,7 @@ export const dayjsLocales: Record<string, () => Promise<ILocale>> = {
"af-ZA": () => import("dayjs/locale/af"),
"am-ET": () => import("dayjs/locale/am"),
"ar-SA": () => import("dayjs/locale/ar-sa"),
"az-AZ": () => import("dayjs/locale/az"),
"bg-BG": () => import("dayjs/locale/bg"),
"bn-BD": () => import("dayjs/locale/bn"),
"ca-ES": () => import("dayjs/locale/ca"),
@ -35,17 +36,21 @@ export const dayjsLocales: Record<string, () => Promise<ILocale>> = {
"kn-IN": () => import("dayjs/locale/kn"),
"ko-KR": () => import("dayjs/locale/ko"),
"lt-LT": () => import("dayjs/locale/lt"),
"lv-LV": () => import("dayjs/locale/lv"),
"ml-IN": () => import("dayjs/locale/ml"),
"mr-IN": () => import("dayjs/locale/mr"),
"ms-MY": () => import("dayjs/locale/ms-my"),
"ne-NP": () => import("dayjs/locale/ne"),
"nl-NL": () => import("dayjs/locale/nl"),
"no-NO": () => import("dayjs/locale/en"),
"no-NO": () => import("dayjs/locale/nb"),
"or-IN": () => import("dayjs/locale/en"),
"pl-PL": () => import("dayjs/locale/pl"),
"pt-BR": () => import("dayjs/locale/pt-br"),
"pt-PT": () => import("dayjs/locale/pt"),
"ro-RO": () => import("dayjs/locale/ro"),
"ru-RU": () => import("dayjs/locale/ru"),
"sk-SK": () => import("dayjs/locale/sk"),
"sq-AL": () => import("dayjs/locale/sq"),
"sr-SP": () => import("dayjs/locale/sr"),
"sv-SE": () => import("dayjs/locale/sv"),
"ta-IN": () => import("dayjs/locale/ta"),
@ -53,6 +58,7 @@ export const dayjsLocales: Record<string, () => Promise<ILocale>> = {
"th-TH": () => import("dayjs/locale/th"),
"tr-TR": () => import("dayjs/locale/tr"),
"uk-UA": () => import("dayjs/locale/uk"),
"uz-UZ": () => import("dayjs/locale/uz"),
"vi-VN": () => import("dayjs/locale/vi"),
"zh-CN": () => import("dayjs/locale/zh-cn"),
"zh-TW": () => import("dayjs/locale/zh-tw"),

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More