Compare commits

...

272 Commits

Author SHA1 Message Date
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
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
a5dc15dc08 Merge pull request #2146 from AmruthPillai/feat/custom-css
Implement Custom CSS Feature
2025-01-13 01:07:16 +01:00
eab996f7e7 some more updates 2025-01-13 01:07:01 +01:00
43c5a33773 Implement Custom CSS Feature 2025-01-13 00:31:49 +01:00
7fb0226ddc update to react-router v7 2025-01-12 19:41:18 +01:00
db6e7a7480 fix deploy step 2025-01-12 18:10:58 +01:00
6335ad1571 fixes #2082, fixes #2066 - fallback to cuid2 if filename contains non-latin characters 2025-01-12 18:06:44 +01:00
2d62504895 Merge pull request #2144 from AmruthPillai/chore/update-dependencies
Update dependencies, bump release version and a lot more
2025-01-12 17:43:07 +01:00
e34d0cebe5 fix deploy step in publish-docker-image 2025-01-12 17:42:51 +01:00
0053d696ff Update ESLint configuration and schemas to use Zod library
- Changed ESLint configuration to target TypeScript files and added parser options for better integration.
- Updated various schemas across the application to replace `nestjs-zod/z` imports with `zod` for consistency.
- Refactored password validation in authentication schemas to use `z.string()` instead of `z.password()`.
- Enhanced date handling in user and resume schemas by introducing a new `dateSchema` utility.
- Updated `.ncurc.json` to target minor upgrades for dependencies.
2025-01-12 17:34:45 +01:00
6fb0a72a56 fix dependencies to latest minor, add swc to no-update list 2025-01-12 17:01:20 +01:00
5b3e91e34d update release version 2025-01-12 16:55:59 +01:00
d1a5a41e4d Enhance UI and Configuration:
- Added missing `type="button"` attributes to various toolbar buttons in the rich-input component for better accessibility.
- Updated Gengar template to ensure the summary section is only displayed when populated.
- Changed boolean values in Docker Compose files from unquoted to quoted strings for consistency and to prevent potential parsing issues.
2025-01-12 16:50:07 +01:00
d0a07686a5 Merge pull request #1997 from 2368883029/main
Fix(client): Fix Unintentional Form Submission on Rich-input Toolbar Actions
2025-01-12 16:41:35 +01:00
ffa4747ed6 Merge pull request #2002 from ilatypov/main
Allow the headless browser connect to the app.
2025-01-12 16:40:44 +01:00
b963813910 Merge branch 'main' into main 2025-01-12 16:40:36 +01:00
58950ed0ef Merge pull request #2008 from busches/patch-1
fix(docs): Add missing themes to bug-report
2025-01-12 16:38:15 +01:00
1c7a6c952f Merge pull request #2010 from busches/fix-2007
fix(Gengar): Hide Summary section unless populated
2025-01-12 16:37:01 +01:00
3ae651fece resolve formatting and linting issues 2025-01-12 16:35:35 +01:00
bd52983780 Merge branch 'main' into fix-2007 2025-01-12 16:35:10 +01:00
39daed3502 Merge branch 'infinia-yzl-main' 2025-01-12 16:25:25 +01:00
dda47f51ec Merge branch 'main' of github.com:infinia-yzl/Reactive-Resume into infinia-yzl-main 2025-01-12 16:24:54 +01:00
374b9bcc58 Merge branch 'main' of github.com:AmruthPillai/Reactive-Resume 2025-01-12 16:23:48 +01:00
f0b18019d5 fix allowOverflow definition on scroll-area 2025-01-12 16:23:46 +01:00
5c10f3d866 Merge pull request #2024 from plecrx/patch-1
fix(storage-controller): rm duplicate userID & use correct filename parameter
2025-01-12 16:09:05 +01:00
5d839e5420 Merge pull request #2009 from busches/patch-2
fix(docs): Update CONTRIBUTING.md for Node 20
2025-01-12 16:07:44 +01:00
febbdefc0b Merge pull request #2035 from noreb001/bugfix/CardClippingIssue
bugfix: Fixed clipping issue with cards on dashboard page
2025-01-12 16:03:25 +01:00
6110440682 resolves #2047, fix summary text in chikorita template, when in sidebar 2025-01-12 15:58:28 +01:00
d0a174d7b7 fix localhost translation logic in printer service 2025-01-12 15:52:30 +01:00
6708570c49 Merge pull request #2091 from raymondyangdev/main
fix(templates): Add right border after website in Rhyhorn
2025-01-12 15:38:54 +01:00
bb4bbf4174 Merge pull request #2095 from jbilinski/upstream-0
fix(printer): refine "localhost" matching in printer.service.ts
2025-01-12 15:37:38 +01:00
a3ef6520e7 - refactor(templates): update sidebar display conditionals for consistency
- Modified multiple template files to change the sidebar length conditionals from `!== 0` to `> 0` for improved readability and consistency across components.

- Added Code of Conduct
2025-01-12 15:36:01 +01:00
0fec5ce86d Merge pull request #2103 from 0x346e3730/feat/full-width-main-if-no-sidebar
feat(templates): make main full-width if sidebar is empty
2025-01-12 15:31:42 +01:00
26e34b6b83 chore: update Prettier configuration and add Tailwind CSS plugin; refactor CSS classes for consistency
- Updated .prettierrc to include Tailwind CSS plugin and functions.
- Added prettier-plugin-tailwindcss to package.json and pnpm-lock.yaml.
- Refactored CSS classes in main.css and various TSX files for improved consistency and readability.
- Adjusted spacing in several components to enhance layout and maintain uniformity.
2025-01-12 15:29:23 +01:00
007243f2c3 Merge pull request #2114 from Karanrajsinh/main
Fix #1660 [Mobile Frontend Issue] :  Section-Dialog Overflow In Mobile Device Along With Some Changes In Spacing And Font Size Of Sidebar
2025-01-12 15:28:18 +01:00
9d0a0bba86 Merge branch 'main' into main 2025-01-12 15:17:55 +01:00
89a44cc33a - normalize username and email fields to lowercase, resolves #1740
- add autoComplete attributes to auth flow for easier sign in/sign up
2025-01-12 15:13:11 +01:00
92856b6f06 Merge pull request #1978 from googleknight/fix/profile-picture-adjustment-crash
fix(edit profile picture): fix the page crash happening when clicking on circular border radius
2025-01-12 13:46:26 +01:00
c968188080 Merge pull request #2133 from smuwonge/main
docs: remove period for consistency in feature list
2025-01-12 13:35:06 +01:00
db36bc9770 Merge pull request #2143 from AmruthPillai/fix/linkedin-logo-not-appearing
Release v4.3.2
2025-01-12 13:31:31 +01:00
35df043f66 fix: handle edge case to display LinkedIn logo when used in Profiles 2025-01-12 13:24:57 +01:00
3c4a26bc06 Update publish-docker-image.yml 2025-01-11 19:03:28 +01:00
62046c49b5 Merge pull request #2140 from AmruthPillai/release/v4.3.1
release: v4.3.1
2025-01-11 16:15:17 +01:00
3b73dcf29d fix issues brought up by coderabbit 2025-01-11 16:13:04 +01:00
9b20c46348 revert ci workflow to non-distributed 2025-01-11 16:09:07 +01:00
996ef650db update CI 2025-01-11 16:01:19 +01:00
1c36ac1d68 update CI workflow 2025-01-11 15:59:36 +01:00
198c269790 remove console.log statements 2025-01-11 15:55:06 +01:00
58ef309b68 release: v4.3.1 2025-01-11 15:38:26 +01:00
54bace451c Merge pull request #2138 from AmruthPillai/dependabot/npm_and_yarn/npm_and_yarn-45c02561f4
Bump the npm_and_yarn group across 1 directory with 10 updates
2025-01-11 14:03:54 +01:00
193b15edc1 Merge branch 'main' into dependabot/npm_and_yarn/npm_and_yarn-45c02561f4 2025-01-11 13:22:39 +01:00
e4ec678512 pin versions of chromium 2025-01-11 13:17:14 +01:00
29c18c1e89 Bump the npm_and_yarn group across 1 directory with 10 updates
Bumps the npm_and_yarn group with 10 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `5.4.2` | `5.4.6` |
| [braces](https://github.com/micromatch/braces) | `3.0.2` | `3.0.3` |
| [dset](https://github.com/lukeed/dset) | `3.1.3` | `3.1.4` |
| [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) | `4.3.5` | `4.5.1` |
| [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) | `2.0.6` | `2.0.7` |
| [nanoid](https://github.com/ai/nanoid) | `3.3.7` | `3.3.8` |
| [pug](https://github.com/pugjs/pug) | `3.0.2` | `3.0.3` |
| [rollup](https://github.com/rollup/rollup) | `4.21.0` | `4.30.1` |
| [webpack](https://github.com/webpack/webpack) | `5.90.3` | `5.97.1` |
| [ws](https://github.com/websockets/ws) | `7.5.9` | `7.5.10` |



Updates `vite` from 5.4.2 to 5.4.6
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.6/packages/vite)

Updates `braces` from 3.0.2 to 3.0.3
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

Updates `dset` from 3.1.3 to 3.1.4
- [Release notes](https://github.com/lukeed/dset/releases)
- [Commits](https://github.com/lukeed/dset/compare/v3.1.3...v3.1.4)

Updates `fast-xml-parser` from 4.3.5 to 4.5.1
- [Release notes](https://github.com/NaturalIntelligence/fast-xml-parser/releases)
- [Changelog](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v4.3.5...v4.5.1)

Updates `http-proxy-middleware` from 2.0.6 to 2.0.7
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.7/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.6...v2.0.7)

Updates `nanoid` from 3.3.7 to 3.3.8
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

Updates `pug` from 3.0.2 to 3.0.3
- [Release notes](https://github.com/pugjs/pug/releases)
- [Commits](https://github.com/pugjs/pug/compare/pug@3.0.2...pug@3.0.3)

Updates `rollup` from 4.21.0 to 4.30.1
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.21.0...v4.30.1)

Updates `webpack` from 5.90.3 to 5.97.1
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.90.3...v5.97.1)

Updates `ws` from 7.5.9 to 7.5.10
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.5.9...7.5.10)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: braces
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: dset
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: fast-xml-parser
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: http-proxy-middleware
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: nanoid
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: pug
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: rollup
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: webpack
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: ws
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-11 11:37:06 +00:00
13d90e8138 update translations 2025-01-11 12:28:16 +01:00
02ce1ad48c fix lint issues 2025-01-11 12:07:30 +01:00
8a401de5c9 fix account deletion 2025-01-09 15:46:50 +01:00
1c5c4d0117 fix: allow the headless browser connect to the app 2025-01-02 20:04:17 -05:00
d6fee1e3a6 docs: remove period for consistency in feature list 2025-01-02 14:31:52 -08:00
d84aeee968 fix(builderpage): resolve section-dialog height overflow for mobile and adjust sidebar font and spacing styles 2024-12-01 10:17:01 +05:30
c87242142d feat(templates): make main full-width if sidebar is empty 2024-11-14 09:39:57 +04:00
2c84976e28 Merge pull request #2 from jbilinski/jbilinski-patch-1
fix: refine "localhost" matching in printer.service.ts
2024-11-07 10:18:43 -08:00
31ed9f41a0 fix: refine "localhost" matching in printer.service.ts
changed localhost matching for local dev environments from a url string match to more specific regex that matches host property.
This was causing the "about:blank", no output error in (edge) case of printing a pdf from any url with string "localhost"
2024-11-07 10:16:35 -08:00
700b98fcb7 fix(templates): Add right border in header after website field with custom fields in Rhyhorn 2024-11-05 17:35:14 +13:00
e29f973889 Merge pull request #2073 from bocklucas/ft-add-ollama
Adding Ollama Support
2024-10-13 02:08:15 +02:00
6e25780b25 Adding Ollama Support 2024-10-12 18:44:23 -05:00
Ben
3f01a9e58e fixed clipping issue with cards on dashboard page 2024-09-06 12:43:27 +00:00
1bed63a4af Merge pull request #2030 from rinczefi-user/rinczefi-patch
set endOfLine to auto to support the codebase also on windows machines
2024-09-04 16:53:52 +02:00
a530fce78c set endOfLine to auto to support the codebase also on windows machines 2024-09-04 14:55:11 +03:00
87a395683d fix eslint issues 2024-09-02 12:50:57 +02:00
e3f3b8b464 bump version to 4.2.0 2024-09-02 12:31:56 +02:00
b9de35f0d9 fix(storage-controller): rm duplicate userID & use correct filename parameter 2024-08-23 16:45:50 +02:00
5827576ffb chore(package/react-zoom-pan-pinch): Bump react-zoom-pan-pinch version to 3.6.1 from 3.4.4 2024-08-14 17:01:05 +08:00
acc9becf1a feat(toggle-pan): Add toggle to switch between scroll to pan and scroll to zoom
- Modify wheel behaviour to pan by default for a more natural behaviour with trackpads (https://github.com/BetterTyped/react-zoom-pan-pinch/pull/447)
- Bump `react-zoom-pan-pinch` version to `3.6.1` from `3.4.4`
2024-08-14 16:25:10 +08:00
c2837838ee fix(Gengar): Hide Summary section unless populated 2024-08-04 14:36:58 -05:00
304fd93ece Update CONTRIBUTING.md for Node 20
package.json and GitHub Actions both run with Node 20
2024-08-04 14:20:41 -05:00
491bbcadcc Add missing themes to bug-report
A few themes were missing when I filled out a bug report.
2024-08-04 14:18:47 -05:00
df63f4b8a7 Merge pull request #1999 from StijnGroenen/feature/custom-field-url
feat(templates): added url support for custom fields
2024-07-22 12:04:36 +02:00
29fc7c419b feat(templates): added url support for custom fields 2024-07-21 21:17:18 +02:00
5bc0230a5a Fix: Can't insert image in a category other than summary 2024-07-21 00:54:00 -04:00
c9a2c27b2d fix(edit profile picture): fix the crash happening when clicking on circular border radius 2024-06-27 14:59:13 +05:30
767573ce6d disable resume previews to lower load on server, fixes #1954 2024-06-13 11:40:17 +02:00
713ce91683 bump version to 4.1.7 2024-06-10 08:04:56 +02:00
8478a42cf0 Merge pull request #1943 from AmruthPillai/1900-bug-inputting-openai-api-key-throws-weird-error
#1900 - bug inputting openai api key throws weird error
2024-06-10 08:04:01 +02:00
5e33f7e8f4 update dependencies, fix lint issues 2024-06-10 08:00:05 +02:00
2292c25158 i18n: update translations 2024-06-10 07:51:09 +02:00
13a7412498 Merge branch 'main' into 1900-bug-inputting-openai-api-key-throws-weird-error 2024-06-10 07:50:03 +02:00
a44846b632 feat: Add file validation for reactive-resume-json type in ImportDialog 2024-06-10 07:49:16 +02:00
454aa20aa9 Merge pull request #1929 from HUAHUAI23/fix/client
fix(client): Fix issue with unhandled errors during resume import
2024-06-10 07:46:47 +02:00
e3b4105424 Merge pull request #1909 from DemaPy/patch-1
fix(client): fix Type Error in Picture Configuration Options
2024-06-10 07:46:35 +02:00
f0ede57786 Merge pull request #1941 from AmruthPillai/l10n
New Translations from Crowdin
2024-06-10 07:46:24 +02:00
ab10d026d6 Refactor OpenAI API key regex to accept project API keys, fixes #1900 2024-06-10 07:46:06 +02:00
a2dbd7ce2b New Crowdin translations by GitHub Action 2024-06-10 00:09:11 +00:00
eca51decc2 Merge pull request #1939 from abizek/fix/skip-crowdin-action-on-forks
Fix(actions): skip sync crowdin action on forks
2024-06-05 18:14:32 +02:00
06455da40a fix(actions): skip sync crowdin action on forks
Signed-off-by: abizek <abishekilango@protonmail.com>
2024-06-05 13:06:05 +05:30
700b74a0b7 Merge pull request #1928 from AmruthPillai/l10n
New Translations from Crowdin
2024-06-04 09:58:42 +02:00
ae64515e93 Merge pull request #1937 from abizek/fix/double-issuer-name
fix(template): double issuer name in chikorita certificates
2024-06-04 09:57:57 +02:00
0992726d7e fix(template): chikorita certificate layout
Signed-off-by: abizek <abishekilango@protonmail.com>
2024-06-04 11:39:04 +05:30
38a442c40d fix(template): double issuer name in chikorita certificates
Signed-off-by: abizek <abishekilango@protonmail.com>
2024-06-04 10:55:41 +05:30
14bcfb219a New Crowdin translations by GitHub Action 2024-06-04 00:08:28 +00:00
d0fd1e3ff5 fix(client): Fix issue with unhandled errors during resume import 2024-05-30 12:39:25 +00:00
3f29ca3add Merge pull request #1922 from AmruthPillai/1592-feature-the-possibility-to-disable-new-sign-ups
🚀 [Feature] Disable new signups through server controlled feature flags
2024-05-29 10:42:51 +02:00
bf498b5972 chore(i18n): update translation files 2024-05-29 10:39:53 +02:00
2bc42493af chore(release): bump version to 4.1.6, update pnpm version 2024-05-29 10:31:41 +02:00
d18ef2e1a5 feat(feature-flags): fixes #1592, introduces new flags DISABLE_SIGNUPS and DISABLE_EMAIL_AUTH, renamed STORAGE_SKIP_BUCKET_CHECK 2024-05-29 10:30:38 +02:00
2409a5786a Update options.tsx
Fix for issue log:
https://github.com/AmruthPillai/Reactive-Resume/issues/1902
2024-05-22 12:58:27 +02:00
1191bbca67 Merge pull request #1906 from AmruthPillai/l10n
New Translations from Crowdin
2024-05-22 08:09:51 +02:00
f5804ab458 New Crowdin translations by GitHub Action 2024-05-22 00:08:19 +00:00
748b509b1d update dependencies, fix import sample resume issue, update pnpm version 2024-05-21 08:10:12 +02:00
696f6f71b5 bump version to 4.1.4 2024-05-20 17:13:50 +02:00
09ebcdf40f remove sentry integration, fix linting issues 2024-05-20 17:13:17 +02:00
0ee0b6b2e9 Merge pull request #1823 from jzaehrin/feat/separate-links
feat: separate links option to reduce section item height
2024-05-20 17:02:56 +02:00
fe550ccc36 update pnpm version 2024-05-20 17:02:48 +02:00
6dcbe78730 Merge branch 'main' into feat/separate-links 2024-05-20 17:01:16 +02:00
73b29f78ab Fix linting, update translations, remove vite-plugin-chunk-split 2024-05-20 16:59:38 +02:00
0124db049b Merge pull request #1780 from dhavalsavalia/custom-icons-in-header
fix(CustomField): add a way to have icon in custom fields
2024-05-20 16:44:37 +02:00
a033c3eff6 Merge branch 'main' into custom-icons-in-header 2024-05-20 16:44:11 +02:00
97286739f2 Merge pull request #1842 from abizek/fix/custom-text-and-background-color
Fix(artboard): Text and background color
2024-05-20 16:42:36 +02:00
a74a8ed044 Merge pull request #1844 from abizek/fix/right-sidebar-page-section-scroll
Fix(builder): right sidebar scroll to page section
2024-05-20 16:41:25 +02:00
376f72a22f Merge pull request #1845 from abizek/fix/nosepass-template-europass-logo
Fix(templates): serve europass logo from public assets
2024-05-20 16:40:55 +02:00
77dee57324 Merge pull request #1885 from nickconway/fix/pdf-name
Fix: generate PDF name from title instead of id
2024-05-20 16:37:43 +02:00
5d0c92e90d Merge pull request #1891 from AashishSinghal/main
Bug Fix #1847 - Fix Sidebar Height in Chikorita template
2024-05-20 16:36:57 +02:00
868e6de7d9 Merge pull request #1843 from abizek/fix/tfa-length-error-message
Fix(tfa): error message for tfa code length
2024-05-20 16:36:23 +02:00
e24f8850d2 merge branch main
Signed-off-by: abizek <abishekilango@protonmail.com>
2024-05-20 13:12:53 +05:30
f30a76229b Revert "disable crowdin sync cron"
This reverts commit b1af5d9339.
2024-05-20 13:08:50 +05:30
02c6318f60 Merge main into fix/custom-text-and-background-color 2024-05-20 13:06:02 +05:30
2f87dde48d Merge branch 'main' into fix/nosepass-template-europass-logo 2024-05-20 13:03:31 +05:30
d570b21635 Merge branch 'main' into fix/right-sidebar-page-section-scroll 2024-05-20 13:03:27 +05:30
b1af5d9339 disable crowdin sync cron 2024-05-20 12:47:21 +05:30
a598a7a7f0 Merge pull request #1899 from ToshY/issue/1897
fix(docker): add environment variable for puppeteer to launch with `ignoreHTTPSerrors` flag
2024-05-15 20:24:29 +02:00
f60fc63ee3 fix(docker): add environment variable for puppeteer to launch with ignore http errors flag 2024-05-15 19:52:22 +02:00
94eb549d25 Bug Fix #1847 - Fix Sidebar is not stretching to the full height in Chikorita template 2024-05-12 08:25:10 +05:30
7a8b5d09c6 add ReleaseBot Discord Webhook to Publish Docker Image action 2024-05-10 11:46:25 +02:00
c15d9f7645 revert structuredClone as it is not supported on proxy objects 2024-05-10 11:27:13 +02:00
a7a0e4e652 Fix: generate PDF name from title instead of id 2024-05-07 13:13:10 -04:00
589e782d71 Merge branch 'main' into fix/tfa-length-error-message 2024-04-06 11:16:43 +05:30
31f396c01a Merge branch 'main' into fix/nosepass-template-europass-logo 2024-04-06 11:16:28 +05:30
6d20eda423 Merge branch 'main' into fix/right-sidebar-page-section-scroll 2024-04-06 11:16:15 +05:30
53b5bdc0b5 Merge branch 'main' into fix/custom-text-and-background-color 2024-04-06 11:16:02 +05:30
8c879841d7 fix(tfa): lazily translate error message 2024-04-02 17:05:29 +05:30
937e6b053d fix(templates): serve europass logo from public assets
fixes #1825
2024-04-02 14:05:00 +05:30
ac9109c2b6 fix(builder): right sidebar scroll to page section 2024-04-02 12:28:17 +05:30
fcc68750cf fix(tfa): error message for tfa code length 2024-04-02 12:22:30 +05:30
afe52da92d fix(artboard): apply custom text and background color 2024-04-01 19:31:28 +05:30
6f2e75f22b feat: separate links option at section level to reduce section item height 2024-03-25 17:27:33 +01:00
2dce78200b feat(Profiles): hide network name when icon is present 2024-02-18 13:29:33 -05:00
b92b4c9936 fix(CustomField): add a way to have icon in custom fields 2024-02-18 12:50:33 -05:00
387 changed files with 44186 additions and 27828 deletions

View File

@ -30,6 +30,8 @@ REFRESH_TOKEN_SECRET=refresh_token_secret
CHROME_PORT=8080
CHROME_TOKEN=chrome_token
CHROME_URL=ws://localhost:8080
# Launch puppeteer with flag to ignore https errors
# CHROME_IGNORE_HTTPS_ERRORS=true
# Mail Server (for e-mails)
# For testing, you can use https://ethereal.email/create
@ -44,11 +46,7 @@ STORAGE_BUCKET=default
STORAGE_ACCESS_KEY=minioadmin
STORAGE_SECRET_KEY=minioadmin
STORAGE_USE_SSL=false
# Sentry (for error reporting, Optional)
# SENTRY_AUTH_TOKEN=
# SERVER_SENTRY_DSN=
# VITE_CLIENT_SENTRY_DSN=
STORAGE_SKIP_BUCKET_CHECK=false
# Nx Cloud (Optional)
# NX_CLOUD_ACCESS_TOKEN=
@ -57,10 +55,9 @@ STORAGE_USE_SSL=false
# CROWDIN_PROJECT_ID=
# CROWDIN_PERSONAL_TOKEN=
# Flags (Optional)
# DISABLE_EMAIL_AUTH=true
# VITE_DISABLE_SIGNUPS=false
# SKIP_STORAGE_BUCKET_CHECK=false
# Feature Flags (Optional)
# DISABLE_SIGNUPS=false
# DISABLE_EMAIL_AUTH=false
# GitHub (OAuth, Optional)
# GITHUB_CLIENT_ID=
@ -71,3 +68,14 @@ STORAGE_USE_SSL=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

@ -9,6 +9,7 @@
"plugins": ["simple-import-sort", "unused-imports"],
"rules": {
// eslint
"no-console": "error",
"no-return-await": "off",
// simple-import-sort
@ -41,6 +42,14 @@
}
]
}
],
// prettier
"prettier/prettier": [
"warn",
{
"endOfLine": "auto"
}
]
}
},
@ -69,6 +78,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"],
@ -76,7 +86,8 @@
// unicorn
"unicorn/no-null": "off",
"unicorn/prevent-abbreviations": "off",
"unicorn/prefer-string-replace-all": "off"
"unicorn/prefer-string-replace-all": "off",
"unicorn/prefer-structured-clone": "off"
}
},
{

View File

@ -76,7 +76,10 @@ body:
- Bronzor
- Chikorita
- Ditto
- Gengar
- Glalie
- Kakuna
- Leafish
- Nosepass
- Onyx
- Pikachu

View File

@ -19,36 +19,30 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.2.2
with:
fetch-depth: 2
- name: Setup pnpm
uses: pnpm/action-setup@v3.0.0
with:
version: 9.1.0
uses: pnpm/action-setup@v4.0.0
- name: Setup Node.js
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v4.1.0
with:
cache: "pnpm"
node-version: 20.12.2
node-version: 22
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm lint
run: pnpm run lint
- name: Format
run: pnpm format:check
run: pnpm run format
- name: Test
run: pnpm test
run: pnpm run test
- name: Build
run: pnpm build
env:
NODE_ENV: production
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
run: pnpm run build

View File

@ -21,7 +21,6 @@ jobs:
version: ${{ steps.version.outputs.version }}
strategy:
max-parallel: 1
matrix:
platform:
- linux/amd64
@ -53,10 +52,6 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.3.0
with:
driver: cloud
version: "lab:latest"
endpoint: "amruthpillai/default"
- name: Extract Docker Metadata
id: meta
@ -82,7 +77,6 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
build-args: |
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Export Digest
@ -131,10 +125,6 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.3.0
with:
driver: cloud
version: "lab:latest"
endpoint: "amruthpillai/default"
- name: Extract Docker Metadata
id: meta
@ -161,3 +151,17 @@ jobs:
repository: ${{ github.repository }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- 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()
with:
username: ReleaseBot
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }}
title: "Release `${{ steps.meta.outputs.version }}`"
description: "A new version of Reactive Resume just dropped! 🚀"
url: "https://github.com/AmruthPillai/Reactive-Resume"

View File

@ -7,6 +7,7 @@ on:
jobs:
sync:
if: github.repository == 'AmruthPillai/Reactive-Resume'
runs-on: ubuntu-latest
steps:

3
.gitignore vendored
View File

@ -40,6 +40,7 @@ Thumbs.db
# Generated Files
.nx
.swc
.turbo
fly.toml
stats.html
@ -49,4 +50,4 @@ stats.html
# Lingui Compiled Messages
apps/client/src/locales/_build/
apps/client/src/locales/*/messages.mjs
apps/client/src/locales/*/messages.*js

View File

@ -1,7 +1,16 @@
{
"$schema": "https://raw.githubusercontent.com/raineorshine/npm-check-updates/main/src/types/RunOptions.json",
"upgrade": true,
"target": "minor",
"install": "always",
"packageManager": "pnpm",
"reject": ["eslint", "vite-plugin-chunk-split", "@reactive-resume/*"]
"reject": [
"nx",
"eslint",
"@nx/*",
"@swc/*",
"@swc-node/*",
"@reactive-resume/*",
"eslint-plugin-unused-imports"
]
}

View File

@ -1,3 +1,6 @@
{
"printWidth": 100
"printWidth": 100,
"endOfLine": "auto",
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindFunctions": ["cn", "cva"]
}

View File

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

12
.vscode/settings.json vendored
View File

@ -1,15 +1,9 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"css.validate": false,
"i18n-ally.localesPaths": ["apps/client/src/locales"],
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
],
"yaml.schemas": {
"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
"typescript.tsdk": "node_modules/typescript/lib"
}

132
CODE_OF_CONDUCT.md Normal file
View 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

View File

@ -9,7 +9,7 @@ To run the development environment of the application locally on your computer,
#### Requirements
- Docker (with Docker Compose)
- Node.js 18 or higher (with pnpm)
- Node.js 20 or higher (with pnpm)
### 1. Fork and Clone the Repository

View File

@ -1,21 +1,18 @@
ARG SENTRY_AUTH_TOKEN
ARG NX_CLOUD_ACCESS_TOKEN
# --- Base Image ---
FROM node:lts-bullseye-slim AS base
ARG SENTRY_AUTH_TOKEN
ARG NX_CLOUD_ACCESS_TOKEN
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable pnpm && corepack prepare pnpm@9.0.6 --activate
RUN corepack enable
WORKDIR /app
# --- Build Image ---
FROM base AS build
ARG SENTRY_AUTH_TOKEN
ARG NX_CLOUD_ACCESS_TOKEN
COPY .npmrc package.json pnpm-lock.yaml ./
@ -24,7 +21,6 @@ RUN pnpm install --frozen-lockfile
COPY . .
ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
ENV NX_CLOUD_ACCESS_TOKEN=$NX_CLOUD_ACCESS_TOKEN
RUN pnpm run build

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
@ -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
- 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
- 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
- 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
@ -72,7 +89,6 @@ Start creating your standout resume with Reactive Resume today!
- Minio (for object storage: to store avatars, resume PDFs and previews)
- Browserless (for headless chrome, to print PDFs and generate previews)
- SMTP Server (to send password recovery emails)
- Sentry (for error tracing and performance monitoring)
- GitHub/Google OAuth (for quickly authenticating users)
- LinguiJS and Crowdin (for translation management and localization)
@ -94,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

@ -12,6 +12,9 @@
}
},
"rules": {
// eslint
"@typescript-eslint/no-require-imports": "off",
// react
"react/no-unescaped-entities": "off",
"react/jsx-sort-props": [

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 src="https://cdn.jsdelivr.net/npm/@phosphor-icons/web"></script>
</body>
</html>

View 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";

View File

@ -20,7 +20,7 @@ export const Page = ({ mode = "preview", pageNumber, children }: Props) => {
return (
<div
data-page={pageNumber}
className={cn("relative bg-white", mode === "builder" && "shadow-2xl")}
className={cn("relative bg-background text-foreground", mode === "builder" && "shadow-2xl")}
style={{
fontFamily,
width: `${pageSizeMap[page.format].width * MM_TO_PX}px`,

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,6 +1,6 @@
import { StrictMode } from "react";
import * as ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import { RouterProvider } from "react-router";
import { router } from "./router";

View File

@ -1,10 +1,12 @@
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 { 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(() => {
@ -39,7 +41,7 @@ export const ArtboardPage = () => {
`${metadata.typography.lineHeight}`,
);
document.documentElement.style.setProperty("--color-text", metadata.theme.text);
document.documentElement.style.setProperty("--color-foreground", metadata.theme.text);
document.documentElement.style.setProperty("--color-primary", metadata.theme.primary);
document.documentElement.style.setProperty("--color-background", metadata.theme.background);
}, [metadata]);
@ -55,5 +57,18 @@ export const ArtboardPage = () => {
}
}, [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 />
</>
);
};

View File

@ -1,17 +1,22 @@
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 } from "react";
import { ReactZoomPanPinchRef, TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
import { useEffect, useMemo, useRef, useState } from "react";
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";
import { getTemplate } from "../templates";
export const BuilderLayout = () => {
const [wheelPanning, setWheelPanning] = useState(true);
const transformRef = useRef<ReactZoomPanPinchRef>(null);
const format = useArtboardStore((state) => state.resume.metadata.page.format);
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 = useMemo(() => getTemplate(template), [template]);
@ -27,6 +32,9 @@ export const BuilderLayout = () => {
transformRef.current?.resetTransform(0);
setTimeout(() => transformRef.current?.centerView(0.8, 0), 10);
}
if (event.data.type === "TOGGLE_PAN_MODE") {
setWheelPanning(event.data.panMode);
}
};
window.addEventListener("message", handleMessage);
@ -44,6 +52,8 @@ export const BuilderLayout = () => {
minScale={0.4}
initialScale={0.8}
limitToBounds={false}
wheel={{ wheelDisabled: wheelPanning }}
panning={{ wheelPanning: wheelPanning }}
>
<TransformComponent
wrapperClass="!w-screen !h-screen"

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 { 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";
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

@ -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 { BuilderLayout } from "../pages/builder";

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;
}
@ -21,5 +25,5 @@
}
.wysiwyg {
@apply prose max-w-none text-current 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;
}

View File

@ -1,10 +1,8 @@
import {
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Profile,
@ -15,15 +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);
@ -64,7 +63,13 @@ const Header = () => {
{basics.customFields.map((item) => (
<div key={item.id} className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
))}
</div>
@ -93,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>
@ -117,16 +122,17 @@ const Rating = ({ level }: RatingProps) => (
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, label, className }: LinkProps) => {
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{icon ?? <i className="ph ph-bold ph-link text-primary" />}
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
<a
href={url.href}
target="_blank"
@ -135,10 +141,32 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
@ -158,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">
@ -196,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} />}
@ -205,7 +236,7 @@ const Section = <T,>({
<p className="text-sm">{keywords.join(", ")}</p>
)}
{url !== undefined && <Link url={url} />}
{url !== undefined && section.separateLinks && <Link url={url} />}
<div className="absolute left-[-4.5px] top-px hidden size-[8px] rounded-full bg-primary group-[.main]:block" />
</div>
@ -218,30 +249,17 @@ const Section = <T,>({
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link
url={item.url}
label={item.username}
icon={
<img
className="ph"
width={fontSize}
height={fontSize}
alt={item.network}
src={`https://cdn.simpleicons.org/${item.icon}`}
/>
}
/>
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
@ -255,7 +273,12 @@ const Experience = () => {
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.company}</div>
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
<div>{item.location}</div>
<div className="font-bold">{item.date}</div>
@ -272,7 +295,12 @@ const Education = () => {
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.institution}</div>
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
<div>{item.studyType}</div>
@ -291,7 +319,7 @@ const Awards = () => {
{(item) => (
<div>
<div className="font-bold">{item.title}</div>
<div>{item.awarder}</div>
<LinkedEntity name={item.awarder} url={item.url} separateLinks={section.separateLinks} />
<div className="font-bold">{item.date}</div>
</div>
)}
@ -307,7 +335,7 @@ const Certifications = () => {
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.issuer}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
<div className="font-bold">{item.date}</div>
</div>
)}
@ -347,7 +375,12 @@ const Publications = () => {
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
<div className="font-bold">{item.date}</div>
</div>
@ -363,7 +396,12 @@ const Volunteer = () => {
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.organization}</div>
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
<div>{item.location}</div>
<div className="font-bold">{item.date}</div>
@ -396,7 +434,12 @@ const Projects = () => {
{(item) => (
<div>
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
<div className="font-bold">{item.date}</div>
@ -414,7 +457,12 @@ const References = () => {
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
@ -435,7 +483,12 @@ const Custom = ({ id }: { id: string }) => {
{(item) => (
<div>
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
<div className="font-bold">{item.date}</div>
@ -510,7 +563,9 @@ export const Azurill = ({ columns, isFirstPage = false }: TemplateProps) => {
))}
</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) => (
<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,15 +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);
@ -64,7 +63,13 @@ const Header = () => {
{basics.customFields.map((item) => (
<div key={item.id} className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
))}
</div>
@ -84,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>
);
@ -108,16 +113,17 @@ const Rating = ({ level }: RatingProps) => (
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, label, className }: LinkProps) => {
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{icon ?? <i className="ph ph-bold ph-link text-primary" />}
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
<a
href={url.href}
target="_blank"
@ -126,10 +132,32 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
@ -149,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">
@ -173,11 +201,14 @@ const Section = <T,>({
<div key={item.id} className={cn("space-y-2", className)}>
<div>
{children?.(item as T)}
{url !== undefined && <Link url={url} />}
{url !== undefined && section.separateLinks && <Link url={url} />}
</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} />}
@ -195,30 +226,17 @@ const Section = <T,>({
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link
url={item.url}
label={item.username}
icon={
<img
className="ph"
width={fontSize}
height={fontSize}
alt={item.network}
src={`https://cdn.simpleicons.org/${item.icon}`}
/>
}
/>
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
@ -233,7 +251,12 @@ const Experience = () => {
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.company}</div>
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
@ -255,7 +278,12 @@ const Education = () => {
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.institution}</div>
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
</div>
@ -279,7 +307,11 @@ const Awards = () => {
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.title}</div>
<div>{item.awarder}</div>
<LinkedEntity
name={item.awarder}
url={item.url}
separateLinks={section.separateLinks}
/>
</div>
<div className="shrink-0 text-right">
@ -300,7 +332,7 @@ const Certifications = () => {
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<div>{item.issuer}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
</div>
<div className="shrink-0 text-right">
@ -345,7 +377,12 @@ const Publications = () => {
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
</div>
@ -366,7 +403,12 @@ const Volunteer = () => {
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.organization}</div>
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
@ -403,7 +445,12 @@ const Projects = () => {
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
@ -423,7 +470,12 @@ const References = () => {
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
@ -444,7 +496,12 @@ const Custom = ({ id }: { id: string }) => {
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>

View File

@ -1,10 +1,8 @@
import {
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Profile,
@ -15,15 +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);
@ -65,7 +64,13 @@ const Header = () => {
{basics.customFields.map((item) => (
<div key={item.id} className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
))}
</div>
@ -84,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>
);
@ -111,16 +116,18 @@ const Rating = ({ level }: RatingProps) => (
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, label, className }: LinkProps) => {
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{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
href={url.href}
target="_blank"
@ -129,10 +136,33 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight &&
(icon ?? <i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-white" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
@ -152,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">
@ -174,11 +204,14 @@ const Section = <T,>({
<div key={item.id} className={cn("space-y-2", className)}>
<div>
{children?.(item as T)}
{url !== undefined && <Link url={url} />}
{url !== undefined && section.separateLinks && <Link url={url} />}
</div>
{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} />}
@ -202,7 +235,12 @@ const Experience = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.company}</div>
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
@ -224,7 +262,12 @@ const Education = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.institution}</div>
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
</div>
@ -241,30 +284,17 @@ const Education = () => {
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link
url={item.url}
label={item.username}
icon={
<img
className="ph"
width={fontSize}
height={fontSize}
alt={item.network}
src={`https://cdn.simpleicons.org/${item.icon}`}
/>
}
/>
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
@ -280,7 +310,11 @@ const Awards = () => {
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.title}</div>
<div>{item.awarder}</div>
<LinkedEntity
name={item.awarder}
url={item.url}
separateLinks={section.separateLinks}
/>
</div>
<div className="shrink-0 text-right">
@ -301,10 +335,7 @@ const Certifications = () => {
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<div>{item.issuer}</div>
</div>
<div className="shrink-0 text-right">
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
<div className="font-bold">{item.date}</div>
</div>
</div>
@ -346,7 +377,12 @@ const Publications = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
</div>
@ -367,7 +403,12 @@ const Volunteer = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.organization}</div>
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
@ -404,7 +445,12 @@ const Projects = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
@ -424,7 +470,12 @@ const References = () => {
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
@ -445,7 +496,12 @@ const Custom = ({ id }: { id: string }) => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
@ -512,8 +568,13 @@ export const Chikorita = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="grid h-full grid-cols-3">
<div className="main p-custom group col-span-2 space-y-4">
<div className="grid min-h-[inherit] grid-cols-3">
<div
className={cn(
"main p-custom group space-y-4",
sidebar.length > 0 ? "col-span-2" : "col-span-3",
)}
>
{isFirstPage && <Header />}
{main.map((section) => (
@ -521,7 +582,12 @@ export const Chikorita = ({ columns, isFirstPage = false }: TemplateProps) => {
))}
</div>
<div className="sidebar p-custom group h-full space-y-4 bg-primary text-background">
<div
className={cn(
"sidebar p-custom group h-full space-y-4 bg-primary text-background",
sidebar.length === 0 && "hidden",
)}
>
{sidebar.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,15 +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);
@ -38,7 +37,7 @@ const Header = () => {
<p>{basics.headline}</p>
</div>
<div className="text-text col-span-2 col-start-2 mt-10">
<div className="col-span-2 col-start-2 mt-10 text-foreground">
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm">
{basics.location && (
<>
@ -82,7 +81,13 @@ const Header = () => {
<Fragment key={item.id}>
<div className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
<div className="bg-text size-1 rounded-full last:hidden" />
</Fragment>
@ -104,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>
);
@ -128,16 +133,17 @@ const Rating = ({ level }: RatingProps) => (
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, label, className }: LinkProps) => {
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{icon ?? <i className="ph ph-bold ph-link text-primary" />}
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
<a
href={url.href}
target="_blank"
@ -146,10 +152,32 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
@ -169,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">
@ -195,14 +223,17 @@ const Section = <T,>({
<div className="relative -ml-4 group-[.sidebar]:ml-0">
<div className="pl-4 group-[.sidebar]:pl-0">
{children?.(item as T)}
{url !== undefined && <Link url={url} />}
{url !== undefined && section.separateLinks && <Link url={url} />}
</div>
<div className="absolute inset-y-0 -left-px border-l-4 border-primary group-[.sidebar]:hidden" />
</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} />}
@ -222,30 +253,17 @@ const Section = <T,>({
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link
url={item.url}
label={item.username}
icon={
<img
className="ph"
width={fontSize}
height={fontSize}
alt={item.network}
src={`https://cdn.simpleicons.org/${item.icon}`}
/>
}
/>
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
@ -260,7 +278,12 @@ const Experience = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.company}</div>
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
@ -282,7 +305,12 @@ const Education = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.institution}</div>
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
</div>
@ -306,7 +334,11 @@ const Awards = () => {
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.title}</div>
<div>{item.awarder}</div>
<LinkedEntity
name={item.awarder}
url={item.url}
separateLinks={section.separateLinks}
/>
</div>
<div className="shrink-0 text-right">
@ -327,7 +359,7 @@ const Certifications = () => {
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<div>{item.issuer}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
</div>
<div className="shrink-0 text-right">
@ -372,7 +404,12 @@ const Publications = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
</div>
@ -393,7 +430,12 @@ const Volunteer = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.organization}</div>
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
@ -430,7 +472,12 @@ const Projects = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
@ -450,7 +497,12 @@ const References = () => {
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
@ -471,7 +523,12 @@ const Custom = ({ id }: { id: string }) => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
@ -553,7 +610,12 @@ export const Ditto = ({ columns, isFirstPage = false }: TemplateProps) => {
))}
</div>
<div className="main p-custom group col-span-2 space-y-4">
<div
className={cn(
"main p-custom group space-y-4",
sidebar.length > 0 ? "col-span-2" : "col-span-3",
)}
>
{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,15 +13,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 { 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);
@ -65,7 +64,13 @@ const Header = () => {
<Fragment key={item.id}>
<div className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`)} />
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
</Fragment>
))}
@ -76,17 +81,20 @@ const Header = () => {
const 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;
return (
<section id={section.id}>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
style={{ columns: section.columns }}
/>
</section>
<div className="p-custom space-y-4" style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}>
<section id={section.id}>
<div
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</section>
</div>
);
};
@ -106,16 +114,20 @@ const Rating = ({ level }: RatingProps) => (
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, label, className }: LinkProps) => {
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{icon ?? <i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-background" />}
{!iconOnRight &&
(icon ?? (
<i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-background" />
))}
<a
href={url.href}
target="_blank"
@ -124,10 +136,35 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight &&
(icon ?? (
<i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-background" />
))}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary group-[.sidebar]:text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
@ -147,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">
@ -169,11 +206,14 @@ const Section = <T,>({
<div key={item.id} className={cn("space-y-2", className)}>
<div>
{children?.(item as T)}
{url !== undefined && <Link url={url} />}
{url !== undefined && section.separateLinks && <Link url={url} />}
</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} />}
@ -191,30 +231,17 @@ const Section = <T,>({
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link
url={item.url}
label={item.username}
icon={
<img
className="ph"
width={fontSize}
height={fontSize}
alt={item.network}
src={`https://cdn.simpleicons.org/${item.icon}`}
/>
}
/>
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
@ -229,7 +256,12 @@ const Experience = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.company}</div>
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
@ -251,7 +283,12 @@ const Education = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.institution}</div>
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
</div>
@ -275,7 +312,11 @@ const Awards = () => {
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.title}</div>
<div>{item.awarder}</div>
<LinkedEntity
name={item.awarder}
url={item.url}
separateLinks={section.separateLinks}
/>
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
@ -296,7 +337,7 @@ const Certifications = () => {
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<div>{item.issuer}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
@ -341,7 +382,12 @@ const Publications = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
</div>
@ -362,7 +408,12 @@ const Volunteer = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.organization}</div>
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
@ -399,7 +450,12 @@ const Projects = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
@ -419,7 +475,12 @@ const References = () => {
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
@ -440,7 +501,12 @@ const Custom = ({ id }: { id: string }) => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
@ -465,6 +531,11 @@ const mapSectionToComponent = (section: SectionKey) => {
case "education": {
return <Education />;
}
case "summary": {
return <Summary />;
}
case "awards": {
return <Awards />;
}
@ -525,16 +596,7 @@ export const Gengar = ({ columns, isFirstPage = false }: TemplateProps) => {
</div>
</div>
<div className="main group col-span-2">
{isFirstPage && (
<div
className="p-custom space-y-4"
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
>
<Summary />
</div>
)}
<div className={cn("main group", sidebar.length > 0 ? "col-span-2" : "col-span-3")}>
<div className="p-custom space-y-4">
{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,15 +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);
@ -65,7 +71,13 @@ const Header = () => {
{basics.customFields.map((item) => (
<div key={item.id} className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon} text-primary`)} />
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
))}
</div>
@ -84,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>
);
@ -114,16 +126,18 @@ const Rating = ({ level }: RatingProps) => {
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, label, className }: LinkProps) => {
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{icon ?? <i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-primary" />}
{!iconOnRight &&
(icon ?? <i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-primary" />)}
<a
href={url.href}
target="_blank"
@ -132,10 +146,33 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight &&
(icon ?? <i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-primary" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary group-[.sidebar]:text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
@ -155,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">
@ -179,11 +216,14 @@ const Section = <T,>({
<div key={item.id} className={cn("space-y-2", className)}>
<div>
{children?.(item as T)}
{url !== undefined && <Link url={url} />}
{url !== undefined && section.separateLinks && <Link url={url} />}
</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} />}
@ -207,7 +247,12 @@ const Experience = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.company}</div>
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
@ -229,7 +274,12 @@ const Education = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.institution}</div>
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
</div>
@ -246,30 +296,17 @@ const Education = () => {
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link
url={item.url}
label={item.username}
icon={
<img
className="ph"
width={fontSize}
height={fontSize}
alt={item.network}
src={`https://cdn.simpleicons.org/${item.icon}`}
/>
}
/>
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
@ -285,7 +322,11 @@ const Awards = () => {
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.title}</div>
<div>{item.awarder}</div>
<LinkedEntity
name={item.awarder}
url={item.url}
separateLinks={section.separateLinks}
/>
</div>
<div className="shrink-0 text-right">
@ -306,7 +347,7 @@ const Certifications = () => {
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<div>{item.issuer}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
</div>
<div className="shrink-0 text-right">
@ -351,7 +392,12 @@ const Publications = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
</div>
@ -372,7 +418,12 @@ const Volunteer = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.organization}</div>
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
@ -409,7 +460,12 @@ const Projects = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
@ -429,7 +485,12 @@ const References = () => {
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
@ -450,7 +511,12 @@ const Custom = ({ id }: { id: string }) => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
@ -521,7 +587,7 @@ export const Glalie = ({ columns, isFirstPage = false }: TemplateProps) => {
return (
<div className="grid min-h-[inherit] grid-cols-3">
<div
className="sidebar p-custom group space-y-4"
className={cn("sidebar p-custom group space-y-4", sidebar.length === 0 && "hidden")}
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
>
{isFirstPage && <Header />}
@ -531,7 +597,12 @@ export const Glalie = ({ columns, isFirstPage = false }: TemplateProps) => {
))}
</div>
<div className="main p-custom group col-span-2 space-y-4">
<div
className={cn(
"main p-custom group space-y-4",
sidebar.length > 0 ? "col-span-2" : "col-span-3",
)}
>
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}

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,20 +12,20 @@ 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);
const profiles = useArtboardStore((state) => state.resume.sections.profiles);
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
return (
<div className="flex flex-col items-center justify-center space-y-2 pb-2 text-center">
@ -61,11 +59,19 @@ const Header = () => {
</a>
</div>
)}
<Link url={basics.url} />
{basics.customFields.map((item) => (
<div key={item.id} className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
))}
</div>
@ -80,15 +86,7 @@ const Header = () => {
url={item.url}
label={item.username}
className="text-sm"
icon={
<img
className="ph"
width={fontSize}
height={fontSize}
alt={item.network}
src={`https://cdn.simpleicons.org/${item.icon}`}
/>
}
icon={<BrandIcon slug={item.icon} />}
/>
</div>
))}
@ -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>
);
@ -134,16 +132,17 @@ const Rating = ({ level }: RatingProps) => (
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, label, className }: LinkProps) => {
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{icon ?? <i className="ph ph-bold ph-link text-primary" />}
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
<a
href={url.href}
target="_blank"
@ -152,10 +151,32 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
@ -175,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">
@ -200,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} />}
@ -209,7 +233,7 @@ const Section = <T,>({
<p className="text-sm">{keywords.join(", ")}</p>
)}
{url !== undefined && <Link url={url} />}
{url !== undefined && section.separateLinks && <Link url={url} />}
</div>
);
})}
@ -225,7 +249,12 @@ const Experience = () => {
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.company}</div>
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
<div>{item.location}</div>
<div className="font-bold">{item.date}</div>
@ -242,7 +271,12 @@ const Education = () => {
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.institution}</div>
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
<div>{item.studyType}</div>
@ -261,7 +295,7 @@ const Awards = () => {
{(item) => (
<div>
<div className="font-bold">{item.title}</div>
<div>{item.awarder}</div>
<LinkedEntity name={item.awarder} url={item.url} separateLinks={section.separateLinks} />
<div className="font-bold">{item.date}</div>
</div>
)}
@ -277,7 +311,7 @@ const Certifications = () => {
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.issuer}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
<div className="font-bold">{item.date}</div>
</div>
)}
@ -317,7 +351,12 @@ const Publications = () => {
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
<div className="font-bold">{item.date}</div>
</div>
@ -333,7 +372,12 @@ const Volunteer = () => {
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.organization}</div>
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
<div>{item.location}</div>
<div className="font-bold">{item.date}</div>
@ -366,7 +410,12 @@ const Projects = () => {
{(item) => (
<div>
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
<div className="font-bold">{item.date}</div>
</div>
@ -383,7 +432,12 @@ const References = () => {
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
@ -404,7 +458,12 @@ const Custom = ({ id }: { id: string }) => {
{(item) => (
<div>
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
<div>{item.location}</div>
<div className="font-bold">{item.date}</div>

View File

@ -1,10 +1,8 @@
import {
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Project,
@ -14,22 +12,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 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);
const section = useArtboardStore((state) => state.resume.sections.summary);
const profiles = useArtboardStore((state) => state.resume.sections.profiles);
const primaryColor = useArtboardStore((state) => state.resume.metadata.theme.primary);
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
return (
<div>
@ -44,9 +42,9 @@ const Header = () => {
</div>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
style={{ columns: section.columns }}
className="wysiwyg"
/>
</div>
@ -81,7 +79,13 @@ const Header = () => {
{basics.customFields.map((item) => (
<div key={item.id} className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
))}
</div>
@ -96,15 +100,7 @@ const Header = () => {
url={item.url}
label={item.username}
className="text-sm"
icon={
<img
className="ph"
width={fontSize}
height={fontSize}
alt={item.network}
src={`https://cdn.simpleicons.org/${item.icon}`}
/>
}
icon={<BrandIcon slug={item.icon} />}
/>
</div>
))}
@ -131,16 +127,17 @@ const Rating = ({ level }: RatingProps) => (
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, label, className }: LinkProps) => {
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{icon ?? <i className="ph ph-bold ph-link text-primary" />}
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
<a
href={url.href}
target="_blank"
@ -149,10 +146,32 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
@ -172,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">
@ -197,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} />}
@ -206,7 +228,7 @@ const Section = <T,>({
<p className="text-sm">{keywords.join(", ")}</p>
)}
{url !== undefined && <Link url={url} />}
{url !== undefined && section.separateLinks && <Link url={url} />}
</div>
);
})}
@ -222,7 +244,12 @@ const Experience = () => {
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.company}</div>
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
<div>{item.location}</div>
<div className="font-bold">{item.date}</div>
@ -239,7 +266,12 @@ const Education = () => {
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.institution}</div>
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
<div>{item.studyType}</div>
@ -258,7 +290,7 @@ const Awards = () => {
{(item) => (
<div>
<div className="font-bold">{item.title}</div>
<div>{item.awarder}</div>
<LinkedEntity name={item.awarder} url={item.url} separateLinks={section.separateLinks} />
<div className="font-bold">{item.date}</div>
</div>
)}
@ -274,7 +306,7 @@ const Certifications = () => {
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.issuer}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
<div className="font-bold">{item.date}</div>
</div>
)}
@ -314,7 +346,12 @@ const Publications = () => {
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
<div className="font-bold">{item.date}</div>
</div>
@ -330,7 +367,12 @@ const Volunteer = () => {
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.organization}</div>
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
<div>{item.location}</div>
<div className="font-bold">{item.date}</div>
@ -363,7 +405,12 @@ const Projects = () => {
{(item) => (
<div>
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
<div className="font-bold">{item.date}</div>
</div>
@ -380,7 +427,12 @@ const References = () => {
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
@ -401,7 +453,12 @@ const Custom = ({ id }: { id: string }) => {
{(item) => (
<div>
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
<div>{item.location}</div>
<div className="font-bold">{item.date}</div>
@ -463,13 +520,13 @@ export const Leafish = ({ columns, isFirstPage = false }: TemplateProps) => {
{isFirstPage && <Header />}
<div className="p-custom grid grid-cols-2 items-start space-x-6">
<div className="grid gap-y-4">
<div className={cn("grid gap-y-4", sidebar.length === 0 && "col-span-2")}>
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
<div className="grid gap-y-4">
<div className={cn("grid gap-y-4", sidebar.length === 0 && "hidden")}>
{sidebar.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,15 +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);
@ -70,8 +69,16 @@ const Header = () => {
{basics.customFields.map((item) => (
<div key={item.id} className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
<span className="text-primary">{item.name}</span>
<span>{item.value}</span>
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<>
<span className="text-primary">{item.name}</span>
<span>{item.value}</span>
</>
)}
</div>
))}
</div>
@ -98,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>
@ -110,16 +117,17 @@ const Summary = () => {
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, label, className }: LinkProps) => {
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{icon ?? <i className="ph ph-bold ph-link text-primary" />}
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
<a
href={url.href}
target="_blank"
@ -128,10 +136,32 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
@ -150,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")}>
@ -184,10 +214,13 @@ const Section = <T,>({
<div className="col-span-3 space-y-1">
{children?.(item as T)}
{url !== undefined && <Link url={url} />}
{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 && (
@ -219,10 +252,13 @@ const Section = <T,>({
<div key={item.id}>
{children?.(item as T)}
{url !== undefined && <Link url={url} />}
{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 && (
@ -240,30 +276,17 @@ const Section = <T,>({
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link
url={item.url}
label={item.username}
icon={
<img
className="ph"
width={fontSize}
height={fontSize}
alt={item.network}
src={`https://cdn.simpleicons.org/${item.icon}`}
/>
}
/>
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
@ -277,7 +300,12 @@ const Experience = () => {
<Section<Experience> section={section} urlKey="url" dateKey="date" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.company}</div>
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
<div>{item.location}</div>
</div>
@ -293,7 +321,12 @@ const Education = () => {
<Section<Education> section={section} urlKey="url" dateKey="date" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.institution}</div>
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.studyType}</div>
<div>{item.score}</div>
@ -311,7 +344,7 @@ const Awards = () => {
{(item) => (
<div>
<div className="font-bold">{item.title}</div>
<div>{item.awarder}</div>
<LinkedEntity name={item.awarder} url={item.url} separateLinks={section.separateLinks} />
</div>
)}
</Section>
@ -326,7 +359,7 @@ const Certifications = () => {
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.issuer}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
</div>
)}
</Section>
@ -365,7 +398,12 @@ const Publications = () => {
<Section<Publication> section={section} urlKey="url" dateKey="date" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
</div>
)}
@ -380,7 +418,12 @@ const Volunteer = () => {
<Section<Volunteer> section={section} urlKey="url" dateKey="date" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.organization}</div>
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
<div>{item.location}</div>
</div>
@ -417,7 +460,12 @@ const Projects = () => {
>
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
@ -432,7 +480,12 @@ const References = () => {
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
@ -453,7 +506,12 @@ const Custom = ({ id }: { id: string }) => {
>
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
<div>{item.location}</div>
</div>
@ -519,7 +577,7 @@ export const Nosepass = ({ columns, isFirstPage = false }: TemplateProps) => {
return (
<div className="p-custom space-y-6">
<div className="flex items-center justify-between">
<img alt="Europass Logo" className="h-[42px]" src="https://i.imgur.com/eRK005p.png" />
<img alt="Europass Logo" className="h-[42px]" src="/assets/europass.png" />
<p className="font-medium text-primary">Curriculum Vitae</p>

View File

@ -1,10 +1,8 @@
import {
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Project,
@ -14,20 +12,20 @@ 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);
const profiles = useArtboardStore((state) => state.resume.sections.profiles);
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
return (
<div className="flex items-center justify-between space-x-4 border-b border-primary pb-5">
@ -66,7 +64,13 @@ const Header = () => {
{basics.customFields.map((item) => (
<div key={item.id} className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
))}
</div>
@ -85,15 +89,7 @@ const Header = () => {
url={item.url}
label={item.username}
className="text-sm"
icon={
<img
className="ph"
width={fontSize}
height={fontSize}
alt={item.network}
src={`https://cdn.simpleicons.org/${item.icon}`}
/>
}
icon={<BrandIcon slug={item.icon} />}
/>
</div>
))}
@ -113,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>
);
@ -137,16 +133,17 @@ const Rating = ({ level }: RatingProps) => (
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, label, className }: LinkProps) => {
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{icon ?? <i className="ph ph-bold ph-link text-primary" />}
{!iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
<a
href={url.href}
target="_blank"
@ -155,10 +152,32 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
@ -178,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">
@ -200,11 +219,14 @@ const Section = <T,>({
<div key={item.id} className={cn("space-y-2", className)}>
<div>
{children?.(item as T)}
{url !== undefined && <Link url={url} />}
{url !== undefined && section.separateLinks && <Link url={url} />}
</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} />}
@ -228,7 +250,12 @@ const Experience = () => {
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.company}</div>
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
@ -250,7 +277,12 @@ const Education = () => {
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.institution}</div>
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
</div>
@ -274,7 +306,11 @@ const Awards = () => {
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.title}</div>
<div>{item.awarder}</div>
<LinkedEntity
name={item.awarder}
url={item.url}
separateLinks={section.separateLinks}
/>
</div>
<div className="shrink-0 text-right">
@ -295,7 +331,7 @@ const Certifications = () => {
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<div>{item.issuer}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
</div>
<div className="shrink-0 text-right">
@ -340,7 +376,12 @@ const Publications = () => {
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
</div>
@ -361,7 +402,12 @@ const Volunteer = () => {
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.organization}</div>
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
@ -398,7 +444,12 @@ const Projects = () => {
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
@ -418,7 +469,12 @@ const References = () => {
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
@ -439,7 +495,12 @@ const Custom = ({ id }: { id: string }) => {
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>

View File

@ -1,10 +1,8 @@
import {
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Profile,
@ -15,15 +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);
@ -84,7 +83,13 @@ const Header = () => {
<Fragment key={item.id}>
<div className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`)} />
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
<div className="size-1 rounded-full bg-background last:hidden" />
</Fragment>
@ -105,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>
);
@ -137,16 +142,20 @@ const Rating = ({ level }: RatingProps) => (
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, label, className }: LinkProps) => {
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{icon ?? <i className="ph ph-bold ph-link text-primary group-[.summary]:text-background" />}
{!iconOnRight &&
(icon ?? (
<i className="ph ph-bold ph-link text-primary group-[.summary]:text-background" />
))}
<a
href={url.href}
target="_blank"
@ -155,10 +164,35 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight &&
(icon ?? (
<i className="ph ph-bold ph-link text-primary group-[.summary]:text-background" />
))}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary group-[.summary]:text-background" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
@ -178,7 +212,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">
@ -200,11 +234,14 @@ const Section = <T,>({
<div key={item.id} className={cn("space-y-2", className)}>
<div>
{children?.(item as T)}
{url !== undefined && <Link url={url} />}
{url !== undefined && section.separateLinks && <Link url={url} />}
</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} />}
@ -222,30 +259,17 @@ const Section = <T,>({
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link
url={item.url}
label={item.username}
icon={
<img
className="ph"
width={fontSize}
height={fontSize}
alt={item.network}
src={`https://cdn.simpleicons.org/${item.icon}`}
/>
}
/>
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
@ -260,7 +284,12 @@ const Experience = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.company}</div>
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
@ -282,7 +311,12 @@ const Education = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.institution}</div>
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
</div>
@ -306,7 +340,11 @@ const Awards = () => {
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.title}</div>
<div>{item.awarder}</div>
<LinkedEntity
name={item.awarder}
url={item.url}
separateLinks={section.separateLinks}
/>
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
@ -327,7 +365,7 @@ const Certifications = () => {
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<div>{item.issuer}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
</div>
<div className="shrink-0 text-right group-[.sidebar]:text-left">
@ -372,7 +410,12 @@ const Publications = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
</div>
@ -393,7 +436,12 @@ const Volunteer = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.organization}</div>
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
@ -430,7 +478,12 @@ const Projects = () => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
@ -450,7 +503,12 @@ const References = () => {
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
@ -471,7 +529,12 @@ const Custom = ({ id }: { id: string }) => {
{(item) => (
<div className="flex items-start justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
@ -547,7 +610,7 @@ export const Pikachu = ({ columns, isFirstPage = false }: TemplateProps) => {
))}
</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 />}
{main.map((section) => (

View File

@ -1,10 +1,8 @@
import {
import type {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Profile,
@ -15,15 +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);
@ -66,7 +65,13 @@ const Header = () => {
className="flex items-center gap-x-1.5 border-r pr-2 last:border-r-0 last:pr-0"
>
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
{isUrl(item.value) ? (
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
{item.name || item.value}
</a>
) : (
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
)}
</div>
))}
</div>
@ -85,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>
);
@ -109,16 +114,17 @@ const Rating = ({ level }: RatingProps) => (
type LinkProps = {
url: URL;
icon?: React.ReactNode;
iconOnRight?: boolean;
label?: string;
className?: string;
};
const Link = ({ url, icon, label, className }: LinkProps) => {
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{icon ?? <i className="ph ph-bold ph-link text-primary" />}
<div className="flex items-center gap-x-1.5 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"
@ -127,10 +133,32 @@ const Link = ({ url, icon, label, className }: LinkProps) => {
>
{label ?? (url.label || url.href)}
</a>
{iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
</div>
);
};
type LinkedEntityProps = {
name: string;
url: URL;
separateLinks: boolean;
className?: string;
};
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
return !separateLinks && isUrl(url.href) ? (
<Link
url={url}
label={name}
icon={<i className="ph ph-bold ph-globe text-primary" />}
iconOnRight={true}
className={className}
/>
) : (
<div className={className}>{name}</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
@ -150,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">
@ -172,11 +200,14 @@ const Section = <T,>({
<div key={item.id} className={cn("space-y-2", className)}>
<div>
{children?.(item as T)}
{url !== undefined && <Link url={url} />}
{url !== undefined && section.separateLinks && <Link url={url} />}
</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} />}
@ -194,30 +225,17 @@ const Section = <T,>({
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link
url={item.url}
label={item.username}
icon={
<img
className="ph"
width={fontSize}
height={fontSize}
alt={item.network}
src={`https://cdn.simpleicons.org/${item.icon}`}
/>
}
/>
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
@ -232,7 +250,12 @@ const Experience = () => {
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.company}</div>
<LinkedEntity
name={item.company}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
@ -254,7 +277,12 @@ const Education = () => {
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.institution}</div>
<LinkedEntity
name={item.institution}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.area}</div>
<div>{item.score}</div>
</div>
@ -278,7 +306,11 @@ const Awards = () => {
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.title}</div>
<div>{item.awarder}</div>
<LinkedEntity
name={item.awarder}
url={item.url}
separateLinks={section.separateLinks}
/>
</div>
<div className="shrink-0 text-right">
@ -299,7 +331,7 @@ const Certifications = () => {
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<div>{item.issuer}</div>
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
</div>
<div className="shrink-0 text-right">
@ -344,7 +376,12 @@ const Publications = () => {
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.publisher}</div>
</div>
@ -365,7 +402,12 @@ const Volunteer = () => {
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.organization}</div>
<LinkedEntity
name={item.organization}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.position}</div>
</div>
@ -402,7 +444,12 @@ const Projects = () => {
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
@ -422,7 +469,12 @@ const References = () => {
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>
)}
@ -443,7 +495,12 @@ const Custom = ({ id }: { id: string }) => {
{(item) => (
<div className="flex items-start justify-between">
<div className="text-left">
<div className="font-bold">{item.name}</div>
<LinkedEntity
name={item.name}
url={item.url}
separateLinks={section.separateLinks}
className="font-bold"
/>
<div>{item.description}</div>
</div>

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

@ -11,7 +11,7 @@ module.exports = {
theme: {
extend: {
colors: {
text: "var(--color-text)",
foreground: "var(--color-foreground)",
primary: "var(--color-primary)",
background: "var(--color-background)",
},
@ -23,6 +23,28 @@ module.exports = {
loose: "calc(var(--line-height) + 0.5)",
},
spacing: { custom: "var(--margin)" },
typography: () => ({
foreground: {
css: {
"--tw-prose-body": "var(--color-foreground)",
"--tw-prose-headings": "var(--color-foreground)",
"--tw-prose-lead": "var(--color-foreground)",
"--tw-prose-links": "var(--color-foreground)",
"--tw-prose-bold": "var(--color-foreground)",
"--tw-prose-counters": "var(--color-foreground)",
"--tw-prose-bullets": "var(--color-foreground)",
"--tw-prose-hr": "var(--color-foreground)",
"--tw-prose-quotes": "var(--color-foreground)",
"--tw-prose-quote-borders": "var(--color-foreground)",
"--tw-prose-captions": "var(--color-foreground)",
"--tw-prose-code": "var(--color-foreground)",
"--tw-prose-pre-code": "var(--color-foreground)",
"--tw-prose-pre-bg": "var(--color-background)",
"--tw-prose-th-borders": "var(--color-foreground)",
"--tw-prose-td-borders": "var(--color-foreground)",
},
},
}),
},
},
plugins: [require("@tailwindcss/typography")],

View File

@ -3,7 +3,6 @@
import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
import react from "@vitejs/plugin-react-swc";
import { defineConfig, searchForWorkspaceRoot } from "vite";
import { chunkSplitPlugin } from "vite-plugin-chunk-split";
export default defineConfig({
base: "/artboard/",
@ -21,13 +20,7 @@ export default defineConfig({
fs: { allow: [searchForWorkspaceRoot(process.cwd())] },
},
plugins: [
react(),
nxViteTsPaths(),
chunkSplitPlugin({
strategy: "unbundle",
}),
],
plugins: [react(), nxViteTsPaths()],
resolve: {
alias: {

View File

@ -3,19 +3,26 @@
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"files": ["*.ts", "*.tsx"],
"extends": [
"plugin:tailwindcss/recommended",
"plugin:@tanstack/eslint-plugin-query/recommended"
],
"parserOptions": {
"projectService": "./apps/client/tsconfig.json"
},
"settings": {
"tailwindcss": {
"callees": ["cn", "clsx", "cva"],
"config": "tailwind.config.js"
"config": "tailwind.config.js",
"whitelist": ["ph", "ph\\-.*", "si", "si\\-.*"]
}
},
"plugins": ["lingui"],
"rules": {
// eslint
"@typescript-eslint/no-require-imports": "off",
// react
"react/no-unescaped-entities": "off",
"react/jsx-sort-props": [
@ -35,8 +42,59 @@
"lingui/no-unlocalized-strings": [
2,
{
"ignoreFunction": ["cn"],
"ignoreAttribute": ["alt"]
"ignore": [
// 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,

View File

@ -35,10 +35,13 @@
<!-- Styles -->
<link rel="stylesheet" href="/src/styles/main.css" />
</head>
<body class="bg-background text-foreground text-sm antialiased 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>
<!-- Scripts -->
<script type="module" src="/src/main.tsx"></script>
<!-- Phosphor Icons -->
<script src="https://cdn.jsdelivr.net/npm/@phosphor-icons/web"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -9,7 +9,7 @@
} else {
document.documentElement.classList.remove("dark");
}
} catch (_) {
} catch {
// pass
}
})();

View 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;
}
}

View 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));
}

View 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
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

@ -289,7 +289,7 @@
]
],
"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
},
"page": {

View File

@ -314,7 +314,7 @@
]
],
"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
},
"page": {

View File

@ -314,7 +314,7 @@
]
],
"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
},
"page": {

View File

@ -315,7 +315,7 @@
]
],
"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
},
"page": {

View File

@ -289,7 +289,7 @@
[[], []]
],
"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
},
"page": {

View File

@ -288,7 +288,7 @@
]
],
"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
},
"page": {

View File

@ -287,7 +287,7 @@
]
],
"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
},
"page": {

View File

@ -289,7 +289,7 @@
]
],
"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
},
"page": {

View File

@ -306,7 +306,7 @@
[["projects", "certifications", "skills", "languages", "references"], []]
],
"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
},
"page": {

View File

@ -287,7 +287,7 @@
]
],
"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
},
"page": {

View File

@ -315,7 +315,7 @@
]
],
"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
},
"page": {

View File

@ -288,7 +288,7 @@
]
],
"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
},
"page": {

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

@ -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";

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

@ -7,7 +7,7 @@ import {
DropdownMenuTrigger,
KeyboardShortcut,
} from "@reactive-resume/ui";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import { useLogout } from "../services/auth";
@ -26,7 +26,7 @@ export const UserOptions = ({ children }: Props) => {
<DropdownMenuContent side="top" align="start" className="w-48">
<DropdownMenuItem
onClick={() => {
navigate("/dashboard/settings");
void navigate("/dashboard/settings");
}}
>
{t`Settings`}

View File

@ -0,0 +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;
@ -13,7 +13,7 @@ type ToasterToast = ToastProps & {
action?: ToastActionElement;
};
const actionTypes = {
export const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",

View File

@ -1,8 +1,9 @@
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-dom";
import { redirect } from "react-router";
import { refreshToken } from "@/client/services/auth";

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"),

View File

@ -17,7 +17,8 @@ export async function dynamicActivate(locale: string) {
if (dayjsLocales[locale]) {
dayjs.locale(await dayjsLocales[locale]());
}
} catch (error) {
console.error(error);
} catch {
// eslint-disable-next-line lingui/no-unlocalized-strings
throw new Error(`Failed to load messages for locale: ${locale}`);
}
}

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