Compare commits

...

285 Commits

Author SHA1 Message Date
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
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
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
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
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
a102f62e28 replace JSON.parse(JSON.stringify({})) with structuredClone({}) 2024-05-10 11:00:15 +02:00
c1a58118c2 - add AuthRefreshProvider to refresh auth tokens every 5 minutes
- pull the latest crowdin language artifacts
2024-05-10 10:52:35 +02:00
b0d26e3230 Merge pull request #1889 from AmruthPillai/l10n
New Translations from Crowdin
2024-05-09 08:06:29 +02:00
7e354b74bd New Crowdin translations by GitHub Action 2024-05-09 00:08:50 +00:00
e20bcb8c14 Merge pull request #1886 from AmruthPillai/l10n
New Translations from Crowdin
2024-05-08 09:21:26 +02:00
506058aacb New Crowdin translations by GitHub Action 2024-05-08 00:07:50 +00:00
a7a0e4e652 Fix: generate PDF name from title instead of id 2024-05-07 13:13:10 -04:00
7b394f1437 pin versions of pnpm and node 2024-05-07 12:33:16 +02:00
e2e2551db4 fix lint-test-build workflow 2024-05-07 12:31:01 +02:00
52e062c0b5 fix eslint issues 2024-05-07 12:02:39 +02:00
e3785030e1 add code chunking to vite.config.ts 2024-05-07 11:45:33 +02:00
e21430a421 Merge pull request #1883 from AmruthPillai/l10n
New Translations from Crowdin
2024-05-07 09:25:38 +02:00
f66704af88 New Crowdin translations by GitHub Action 2024-05-07 00:08:07 +00:00
7a65363296 add test to github action 2024-05-05 17:06:08 +02:00
8180e8c7b8 add SENTRY_AUTH_TOKEN 2024-05-05 16:50:01 +02:00
13b2a5be94 add max-parallel:1 to github action 2024-05-05 16:09:29 +02:00
4dd5367572 attempt to fix lint issues 2024-05-05 15:02:16 +02:00
e87b05a93a release: v4.1.0 2024-05-05 14:55:06 +02:00
68252c35fc update postgres version in templates 2024-05-03 23:03:23 +02:00
ac9b280bd5 revert runs-on 2024-05-03 21:48:28 +02:00
32b8407b1a experimenting with self-hosted action runners 2024-05-03 21:18:25 +02:00
989e8dee5b update dependencies 2024-05-03 20:55:13 +02:00
5ed561812f add privacy policy, bump up version to 4.0.13 2024-05-03 20:53:58 +02:00
cba2eda5d0 fix artifact name 2024-05-03 16:43:20 +02:00
1c7b44b604 migrate actions/upload-artifact to v4 2024-05-03 15:29:59 +02:00
5d61e865a8 changes to Dockerfile 2024-05-03 15:12:38 +02:00
f32a85cec9 add emptyOutDir: true to vite.config.ts 2024-05-03 15:07:07 +02:00
c99ce90cf8 add ARG NX_CLOUD_ACCESS_TOKEN to Dockerfile 2024-05-03 14:52:59 +02:00
6e2b960bdb fix dependencies installation location 2024-05-03 14:31:13 +02:00
862c812ee1 add files to .dockerignore 2024-05-03 14:22:28 +02:00
6424b15b76 update versions of workflows 2024-05-03 14:08:49 +02:00
5e32673358 modify action workflows 2024-05-03 14:04:20 +02:00
470f187c0b fix Dockerfile 2024-05-03 14:02:08 +02:00
8b966946ea remove unnecessary pnpm-lock.yaml files 2024-05-03 13:53:39 +02:00
8579a4c98d Merge pull request #1871 from AmruthPillai/l10n
New Translations from Crowdin
2024-05-03 13:31:56 +02:00
8deff757a9 fix tiptap issues, update dependencies, fix typescript issues with minio client 2024-05-03 12:05:54 +02:00
d227cf64aa New Crowdin translations by GitHub Action 2024-05-03 00:08:45 +00:00
458af1d840 Merge pull request #1870 from AmruthPillai/l10n
New Translations from Crowdin
2024-04-22 19:42:30 +02:00
0024aec60a New Crowdin translations by GitHub Action 2024-04-22 00:08:42 +00:00
ec86536ace Merge pull request #1864 from AmruthPillai/l10n
New Translations from Crowdin
2024-04-18 06:20:17 +02:00
f7e2bfb078 New Crowdin translations by GitHub Action 2024-04-18 00:08:40 +00:00
168be7dfb8 Merge pull request #1861 from AmruthPillai/l10n
New Translations from Crowdin
2024-04-12 09:12:00 +02:00
832d0002e9 New Crowdin translations by GitHub Action 2024-04-12 00:08:14 +00:00
1f9e3aa9d1 update dependencies, update translations 2024-04-10 15:43:56 +02:00
dd97c6d71f Merge pull request #1856 from WangZhiYao/main
feat(server): Support for SMTP-over-TLS
2024-04-09 10:30:17 +02:00
339
1f5dce2233 feat(server): Support for SMTP-over-TLS 2024-04-09 14:42:47 +08:00
fe77b14807 Merge pull request #1855 from AmruthPillai/l10n
New Translations from Crowdin
2024-04-08 11:10:22 +02:00
a36c49fa77 New Crowdin translations by GitHub Action 2024-04-08 00:08:19 +00: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
709bf0a526 Merge pull request #1851 from AmruthPillai/l10n
New Translations from Crowdin
2024-04-05 08:49:19 +02:00
7189c7f203 New Crowdin translations by GitHub Action 2024-04-05 00:08:37 +00:00
cdef456aac update dependencies 2024-04-03 09:58:08 +02:00
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
a578bd1054 Merge pull request #1836 from AmruthPillai/l10n
New Translations from Crowdin
2024-03-31 10:22:04 +02:00
116c038861 New Crowdin translations by GitHub Action 2024-03-31 00:09:18 +00:00
6e6914fe6b Merge pull request #1830 from AmruthPillai/l10n
New Translations from Crowdin
2024-03-27 08:15:01 +01:00
2bf04f2616 New Crowdin translations by GitHub Action 2024-03-27 00:08:21 +00:00
6f2e75f22b feat: separate links option at section level to reduce section item height 2024-03-25 17:27:33 +01:00
f6c2ae7504 remove banner on mobile/tablet devices 2024-03-25 15:08:39 +01:00
890875ad9d - upgrade to browserless v2
- add missing languages
- add donation banner
- update dependencies
- bump version to 4.0.9
2024-03-22 12:11:15 +01:00
11953af700 Merge pull request #1816 from AmruthPillai/dependabot/npm_and_yarn/follow-redirects-1.15.6
Bump follow-redirects from 1.15.5 to 1.15.6
2024-03-19 09:17:57 +01:00
3c774102cf Merge pull request #1809 from Rash-Hit/debouncing-in-profile-icon
debounce in profile icon
2024-03-19 09:17:28 +01:00
fbf92160a3 Merge pull request #1815 from AmruthPillai/l10n
New Translations from Crowdin
2024-03-19 09:16:33 +01:00
a798845865 New Crowdin translations by GitHub Action 2024-03-19 00:08:05 +00:00
7db57e04c0 Bump follow-redirects from 1.15.5 to 1.15.6
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-17 01:21:40 +00:00
b23efa773f Merge pull request #1813 from AmruthPillai/l10n
New Translations from Crowdin
2024-03-14 14:04:42 +01:00
3b41b32f09 New Crowdin translations by GitHub Action 2024-03-14 00:07:49 +00:00
5513b909e7 Merge pull request #1810 from AmruthPillai/l10n
New Translations from Crowdin
2024-03-12 06:46:38 +01:00
c1a50d4125 New Crowdin translations by GitHub Action 2024-03-12 00:07:52 +00:00
7babde2d62 Merge branch 'AmruthPillai:main' into debouncing-in-profile-icon 2024-03-11 16:58:04 +05:30
08a6415ba8 -- code refactored -- 2024-03-11 11:25:45 +00:00
1ee9478200 debounce in profile icon 2024-03-11 10:28:50 +00:00
bbe8fb6655 Merge pull request #1808 from AmruthPillai/1805-bugfont-subset-and-font-variants-not-working
Add CommandList to Combobox component
2024-03-11 09:57:07 +01:00
6405102cab add CommandList to Combobox component 2024-03-11 09:54:10 +01:00
32445a5cd7 Merge pull request #1804 from AmruthPillai/l10n
New Translations from Crowdin
2024-03-11 09:44:55 +01:00
5494d93e1d New Crowdin translations by GitHub Action 2024-03-11 00:08:17 +00:00
ad9647a3f4 revert @nestjs-modules/mailer to 1.10.3 2024-03-10 11:19:51 +01:00
9ee7e3195b bump up version to 4.0.7 2024-03-10 11:14:23 +01:00
53dfd4cb09 fix language selector 2024-03-10 11:12:15 +01:00
7ceb0f6e39 add symbolic links to prettierignore 2024-03-10 10:50:45 +01:00
7496461618 add symbolic links to compose files 2024-03-10 10:36:43 +01:00
b47b7824ff - bump up version to 4.0.6
- update dependencies
- remove test phase from CI script
- lint and format all files
2024-03-10 10:35:23 +01:00
ec77d13ebd add safety check for username in github strategy 2024-03-10 10:27:30 +01:00
c8f7989c1f refactor avoidTooShort function 2024-03-10 10:25:47 +01:00
ec612f0902 Merge pull request #1799 from mradigen/main
fix(github_auth): fix GithubStrategy not searching for user using username
2024-03-10 10:25:28 +01:00
10b2ca8bf2 Merge pull request #1790 from gzsombor/import-fixes
Better error handling and more lenient on the imported values
2024-03-10 10:23:58 +01:00
995b1e627b Merge pull request #1789 from skyworkz/bugfix/compose-setup-pdf-download
fix issue where PDF downloads fail on compose setup
2024-03-10 10:22:50 +01:00
f1d4ebb504 Merge pull request #1788 from abizek/main
Fix popover stacking in right sidebar theme section
2024-03-10 10:21:40 +01:00
6c97c880b3 Merge pull request #1785 from tsp36/feat/chinese_name
modify the name field to a minimum of 2 characters
2024-03-10 10:21:07 +01:00
8fc3c25714 Update libs/dto/src/user/user.ts 2024-03-10 10:21:01 +01:00
19c4d31710 Merge pull request #1771 from iammursal/main
fix(import): Changing 'Filetype' clears the 'File' upload field properly
2024-03-10 10:15:52 +01:00
b17919e909 Merge pull request #1768 from bhumit070/main
fix(gengar template): link of personal project was overflowing outside.
2024-03-10 10:14:51 +01:00
4c1c17c693 Merge pull request #1751 from Samrat-Saha-Sammy/main
fix(#1750): fixed downloaded PDF have issue(s) with Typography options
2024-03-10 10:13:09 +01:00
df99470df8 modifications to rich-input link component 2024-03-10 10:12:09 +01:00
a92528cdb7 Merge pull request #1729 from CorreyL/feature/ctrl-k-for-hyperlink
[Feature] Keyboard Shortcut for Hyperlink
2024-03-10 10:07:33 +01:00
219e6999df Merge branch 'main' into feature/ctrl-k-for-hyperlink 2024-03-10 10:07:26 +01:00
95ee77f65c Merge pull request #1727 from CorreyL/main
[Bugfix] Instantiate Link with inclusive set to false
2024-03-10 10:04:27 +01:00
eac26215a3 update messages.po translations from lingui 2024-03-10 10:01:09 +01:00
783af5070d remove caching from resumes 2024-03-10 10:00:51 +01:00
359c7f1c80 update dependencies 2024-03-10 10:00:33 +01:00
71d3cea100 Fix build 2024-03-09 12:33:49 +01:00
befc5a67fc Format code, better function name 2024-03-09 11:50:56 +01:00
5a2c222d61 Better error handling and more lenient on the imported values 2024-03-09 11:50:56 +01:00
6a3c75c15c Merge branch 'main' into main 2024-03-08 18:01:28 +05:30
b6162d7bb0 fix(github_auth): fix GithubStrategy not searching for user using username 2024-03-06 15:40:37 +05:30
550e15228e Merge pull request #1795 from busches/patch-1
Update minimum node to 18.17.0
2024-03-01 10:58:37 +01:00
518f5b1fb8 Merge pull request #1796 from AmruthPillai/l10n
New Translations from Crowdin
2024-03-01 10:58:19 +01:00
1e56f940d9 New Crowdin translations by GitHub Action 2024-03-01 00:09:05 +00:00
e83e9c61b5 Update minimum node to 18.17.0
The dependency on sharp will fail with anything between 18.0-<18.17.0 per https://sharp.pixelplumbing.com/install#prerequisites
2024-02-28 20:09:29 -06:00
269f4c8b4d fix(builder): fix popover stacking in right sidebar theme section
Fixes #1642
2024-02-28 00:16:18 +05:30
24f0af890a Merge pull request #1781 from AmruthPillai/l10n
New Translations from Crowdin
2024-02-24 07:39:43 +01:00
5ccd98bd0a New Crowdin translations by GitHub Action 2024-02-24 00:07:22 +00:00
21fe2e195c fix issue where PDF downloads fail on compose setup 2024-02-23 10:54:36 +01:00
33168aa535 modify the name field to a minimum of 2 characters 2024-02-21 09:40:57 +08: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
c806dc890a Merge pull request #1773 from AmruthPillai/l10n
New Translations from Crowdin
2024-02-18 05:24:31 +01:00
816023eea6 Merge pull request #1747 from theschles/main
fix(templates): Vertical-align-top instead of vertical-align-middle entry header data
2024-02-18 05:24:08 +01:00
129ac7da38 New Crowdin translations by GitHub Action 2024-02-18 00:08:31 +00:00
1b80f751a3 Merge branch 'main' into main 2024-02-15 15:32:20 -08:00
f30d299949 fix(import): Changing 'Filetype' clears the 'File' upload field properly 2024-02-14 21:14:02 +03:00
5de1bafdc6 Merge pull request #1762 from AmruthPillai/l10n
New Translations from Crowdin
2024-02-14 10:03:28 +01:00
2a8abd3a0b New Crowdin translations by GitHub Action 2024-02-14 00:07:50 +00:00
fdfcd37061 fix(gengar template): link of personal project was overflowing outsite of sidebar 2024-02-13 08:16:58 +05:30
5d146ca86e fix(#1750): fixed downloaded PDF have issue(s) with Typography options
Typography option toggle class were added in DOM #root but when server is printing the resume, a child section of the DOM #root is cloned and inserted for pdf processing which miss out the Typography option toggle class in the parent DOM, resulting in bug, Thus, the same class were added to each cloned DOM node.
2024-02-02 00:52:50 +05:30
e9ec397663 chore(i18n): sync sources from crowdin translations 2024-02-01 10:13:20 +01:00
c4f552f44a Merge pull request #1749 from AmruthPillai/l10n
New Translations from Crowdin
2024-02-01 06:14:40 +01:00
1f274d8ae9 New Crowdin translations by GitHub Action 2024-02-01 00:09:11 +00:00
846050f031 Update pikachu.tsx 2024-01-30 10:10:09 -08:00
d23b35de5e Update glalie.tsx 2024-01-30 10:08:47 -08:00
2e4c660c97 Update gengar.tsx 2024-01-30 10:08:23 -08:00
4d5dc3869e Update ditto.tsx 2024-01-30 10:07:48 -08:00
d4ca61d751 Update chikorita.tsx 2024-01-30 10:05:52 -08:00
5f1da943b8 Update rhyhorn.tsx 2024-01-30 10:05:16 -08:00
0803ad7e2d Update bronzor.tsx 2024-01-30 10:05:13 -08:00
1326895e6b fix: resume entry header items should be top-vertical-aligned, not middle-vertical-aligned 2024-01-30 10:01:20 -08:00
bc17157204 Merge pull request #1744 from AmruthPillai/l10n
New Translations from Crowdin
2024-01-28 09:25:29 +01:00
880b3b5d37 New Crowdin translations by GitHub Action 2024-01-28 00:08:41 +00:00
d9d4085591 Merge pull request #1742 from AmruthPillai/l10n
New Translations from Crowdin
2024-01-26 08:13:43 +01:00
acedc6b116 New Crowdin translations by GitHub Action 2024-01-26 00:08:13 +00:00
4eac1c0024 Merge pull request #1737 from AmruthPillai/l10n
New Translations from Crowdin
2024-01-25 09:35:08 +01:00
e86c390862 New Crowdin translations by GitHub Action 2024-01-25 00:08:41 +00:00
7f877861d1 Merge pull request #1734 from AmruthPillai/l10n
New Translations from Crowdin
2024-01-23 05:19:17 +01:00
6f97c06e3d New Crowdin translations by GitHub Action 2024-01-23 00:08:46 +00:00
af4a96822c Merge pull request #1730 from AmruthPillai/l10n
New Translations from Crowdin
2024-01-22 09:09:25 +01:00
f4bedc668d New Crowdin translations by GitHub Action 2024-01-22 00:08:53 +00:00
8e8b695cbc Add a keyboard shortcut for adding a hyperlink
Use the conventional Ctrl+K keyboard shortcut for adding a hyperlink
2024-01-21 10:38:12 -08:00
37a869fa24 Move core logic of setLink to outer scope
Will allow code changes in a subsequent commit to access the function
and utilize the same core logic
2024-01-21 10:37:15 -08:00
176cac4fbe Instantiate Link with inclusive set to false
Ensures consistent behavior with other common text editors, wherein
after applying a link to text, the following text does not also contain
the link
2024-01-21 09:12:05 -08:00
debfd9167f add eslint ignore 2024-01-21 11:12:35 +01:00
b68b5a7747 add digitalocean attribution 2024-01-21 11:00:24 +01:00
1aaaaeca20 format files 2024-01-21 10:44:22 +01:00
0590367b7f Merge pull request #1725 from AmruthPillai/l10n
New Translations from Crowdin
2024-01-21 09:09:11 +01:00
209947266b New Crowdin translations by GitHub Action 2024-01-21 00:09:20 +00:00
8bca7f5390 release: v4.0.4 2024-01-19 10:04:23 +01:00
643348046f Merge pull request #1714 from calumapplepie/patch-1
Update profile.ts to add default values
2024-01-19 09:58:04 +01:00
9843f39510 Merge pull request #1717 from SergejKasper/fix-linkedIn-import-fails
fix(import): LinkedIn Profile.csv parsing fixes
2024-01-19 09:56:39 +01:00
14362b92c1 Merge pull request #1721 from CorreyL/bugfix/save-pending-keyword-input
[Bugfix] Save Pending Keyword Input
2024-01-19 09:55:42 +01:00
ac322a9bd4 Add pending inputs as a new Keyword
Ensures that any Keywords that were intended to be added are not lost
2024-01-18 19:45:29 -08:00
390f274d06 Dialogs that accept keywords, track pending inputs
In every dialog component that allows the input of Keyword, instantiate
a pendingKeyword State to track if the BadgeIput element has a pending
input
2024-01-18 19:42:24 -08:00
da23b06f71 Capture the current Keyword in the input in state
Keeps a parent State that tracks if a pending Keyword is in the input
field up-to-date
2024-01-18 19:39:53 -08:00
469f1d5cdd Merge pull request #1716 from SergejKasper/fix-po-loader-issue
fix(vite): fix .po file loader issue
2024-01-15 14:14:45 +01:00
45a936c05d fix(import): LinkedIn Profile.csv parsing fixes 2024-01-15 10:43:32 +01:00
84cafba0c2 fix(vite): fix .po file loader issue 2024-01-15 09:23:22 +01:00
e1ec60af92 Merge pull request #1705 from RJohnPaul/patch-1
Update project.json with cleaning and formatting
2024-01-12 08:40:19 +01:00
3cde03e9cb Merge pull request #1711 from AmruthPillai/dependabot/npm_and_yarn/follow-redirects-1.15.4
build(deps): bump follow-redirects from 1.15.3 to 1.15.4
2024-01-12 08:40:02 +01:00
b7c3b84ba2 Merge pull request #1713 from AmruthPillai/l10n
New Translations from Crowdin
2024-01-12 08:39:53 +01:00
b08b86ca9d Update profile.ts to add default values
Add some default values to the LinkedIn data import in case of missing data.
Default values used in place of optional data in order to minimize changes to other parts of the codebase; since optionals require an extra step to handle.  Default values of whitespace might work better,
but I cannot test to be sure.

Closes: #1604
2024-01-11 21:50:37 -05:00
a37edc2caa New Crowdin translations by GitHub Action 2024-01-12 00:08:18 +00:00
f0c778b37a Merge pull request #1710 from AmruthPillai/l10n
New Translations from Crowdin
2024-01-11 07:18:32 +01:00
675a92a17f build(deps): bump follow-redirects from 1.15.3 to 1.15.4
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-11 04:32:26 +00:00
9197729387 New Crowdin translations by GitHub Action 2024-01-11 00:08:30 +00:00
99cb7f512e Merge pull request #1706 from datamoc/main
Minor changes on logo (more regular, smooth lines, png and svg favicon)
2024-01-09 09:54:05 +01:00
0f765af468 Minor changes on logo (more regular, smooth lines, png and svg favicon)
.vs/* added in .gitignore
2024-01-06 18:37:48 +01:00
f5ce9af3e0 Update project.json with cleaning and formatting 2024-01-06 19:23:24 +05:30
8500c30f59 Merge pull request #1704 from AmruthPillai/l10n
New Translations from Crowdin
2024-01-06 08:50:26 +01:00
2f4065b5a3 New Crowdin translations by GitHub Action 2024-01-06 00:08:29 +00:00
fb4ecee897 Merge pull request #1703 from AmruthPillai/l10n
New Translations from Crowdin
2024-01-05 22:31:19 +01:00
735352c3d3 New Crowdin translations by GitHub Action 2024-01-05 00:08:26 +00:00
f89ad7cd1a Merge pull request #1700 from AmruthPillai/l10n
New Translations from Crowdin
2024-01-04 09:35:20 +01:00
bfadfb1a4f New Crowdin translations by GitHub Action 2024-01-04 00:08:36 +00:00
af306746d3 Merge pull request #1696 from eltociear/patch-1
Update CONTRIBUTING.md
2024-01-02 09:06:41 +01:00
d67513b3f4 Merge pull request #1678 from AmruthPillai/l10n
New Translations from Crowdin
2024-01-02 09:06:09 +01:00
0bae5f9422 New Crowdin translations by GitHub Action 2024-01-02 00:08:21 +00:00
791d8e58d9 Update CONTRIBUTING.md
decriptive -> descriptive
2023-12-28 01:07:07 +09:00
1fca2abd93 Merge pull request #1675 from AmruthPillai/l10n
New Translations from Crowdin
2023-12-19 09:09:38 +05:30
de2b7eb1ff New Crowdin translations by GitHub Action 2023-12-19 00:08:30 +00:00
c8840551a1 Merge pull request #1673 from AmruthPillai/l10n
New Translations from Crowdin
2023-12-17 09:06:31 +05:30
302f112b15 New Crowdin translations by GitHub Action 2023-12-17 00:09:08 +00:00
f2d8f99fb3 Merge pull request #1669 from juliandaz11/patch-2
Update change-tone.ts
2023-12-15 11:24:18 +05:30
7ed2b7dc16 Merge pull request #1668 from juliandaz11/patch-1
Update improve-writing.ts
2023-12-15 11:24:06 +05:30
acbed2ed74 Merge pull request #1670 from juliandaz11/patch-3
Update fix-grammar.ts
2023-12-15 11:23:54 +05:30
a095cb8255 Update fix-grammar.ts
returns in the language of the text
2023-12-14 23:16:53 +00:00
5fb6082cce Update change-tone.ts
returns in the language of the text
2023-12-14 23:15:44 +00:00
51408eb03c Update improve-writing.ts
returns in the language of the text
2023-12-14 23:12:59 +00:00
c51b69ade5 Merge pull request #1666 from AmruthPillai/l10n
New Translations from Crowdin
2023-12-14 08:05:40 +05:30
db413378eb New Crowdin translations by GitHub Action 2023-12-14 00:08:32 +00:00
f82b163c7a Merge pull request #1662 from AmruthPillai/l10n
New Translations from Crowdin
2023-12-12 14:37:29 +05:30
1a70a847f7 New Crowdin translations by GitHub Action 2023-12-12 00:08:28 +00:00
b8f3a62bc5 Merge pull request #1650 from AmruthPillai/l10n
New Translations from Crowdin
2023-12-11 13:21:02 +05:30
4f23f5fe01 New Crowdin translations by GitHub Action 2023-12-11 00:08:39 +00:00
b5de1b764a Merge pull request #1649 from AmruthPillai/l10n
New Translations from Crowdin
2023-12-07 11:21:22 +05:30
6444cd3175 New Crowdin translations by GitHub Action 2023-12-07 00:08:22 +00:00
361 changed files with 53413 additions and 38203 deletions

View File

@ -1,22 +1,62 @@
!README.md
.dockerignore
# Compiled Output
dist
tmp
/out-tsc
# Project Dependencies
.git
.gitignore
node_modules
# Docker
compose*.yml
Dockerfile
# IDEs and Editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vs/*
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# Miscellaneous
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
.editorconfig
.eslint*
.git
.github
.gitignore
.husky
# Generated Files
.nx
.prettier*
.vscode
*.env*
*.md
compose*.yml
dist
Dockerfile
node_modules
Thumbs.db
tmp
.swc
fly.toml
stats.html
tools/compose/*
tools/scripts/*
tools/scripts/*
# Environment Variables
*.env*
!.env.example
# Lingui Compiled Messages
apps/client/src/locales/_build/
apps/client/src/locales/*/messages.mjs

View File

@ -4,14 +4,6 @@ NODE_ENV=development
# Ports
PORT=3000
# Client Port & URL (for development)
__DEV__CLIENT_PORT=5173 # Only used in development
__DEV__CLIENT_URL=http://localhost:5173 # Only used in development
# Artboard Port & URL (for development)
__DEV__ARTBOARD_PORT=6173 # Only used in development
__DEV__ARTBOARD_URL=http://localhost:6173 # Only used in development
# URLs
# These URLs must reference a publicly accessible domain or IP address, not a docker container ID (depending on your compose setup)
PUBLIC_URL=http://localhost:3000
@ -38,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
@ -52,20 +46,18 @@ STORAGE_BUCKET=default
STORAGE_ACCESS_KEY=minioadmin
STORAGE_SECRET_KEY=minioadmin
STORAGE_USE_SSL=false
STORAGE_SKIP_BUCKET_CHECK=false
# Redis (for cache & server session management)
REDIS_URL=redis://default:password@localhost:6379
# Sentry (for error reporting, Optional)
# VITE_SENTRY_DSN=
# Nx Cloud (Optional)
# NX_CLOUD_ACCESS_TOKEN=
# Crowdin (Optional)
CROWDIN_PROJECT_ID=
CROWDIN_PERSONAL_TOKEN=
# CROWDIN_PROJECT_ID=
# CROWDIN_PERSONAL_TOKEN=
# Email (Optional)
# DISABLE_EMAIL_AUTH=true
# VITE_DISABLE_SIGNUPS=false
# Feature Flags (Optional)
# DISABLE_SIGNUPS=false
# DISABLE_EMAIL_AUTH=false
# GitHub (OAuth, Optional)
# GITHUB_CLIENT_ID=

View File

@ -8,6 +8,10 @@
"extends": ["plugin:prettier/recommended"],
"plugins": ["simple-import-sort", "unused-imports"],
"rules": {
// eslint
"no-console": "error",
"no-return-await": "off",
// simple-import-sort
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
@ -38,15 +42,51 @@
}
]
}
],
// prettier
"prettier/prettier": [
"warn",
{
"endOfLine": "auto"
}
]
}
},
{
"files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nx/typescript"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"ecmaVersion": "latest",
"project": ["tsconfig.*?.json"]
},
"extends": [
"plugin:@nx/typescript",
"plugin:@typescript-eslint/strict-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked",
"plugin:unicorn/recommended"
],
"plugins": ["@typescript-eslint", "unicorn"],
"rules": {
// typescript-eslint
"@typescript-eslint/no-unused-vars": "off"
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/return-await": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-misused-promises": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/no-redundant-type-constituents": "off",
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
// unicorn
"unicorn/no-null": "off",
"unicorn/prevent-abbreviations": "off",
"unicorn/prefer-string-replace-all": "off",
"unicorn/prefer-structured-clone": "off"
}
},
{

View File

@ -21,7 +21,7 @@ body:
label: Product Variant
description: What variant of Reactive Resume are you using?
options:
- Cloud (http://rxresu.me)
- Cloud (https://rxresu.me)
- Self-Hosted
validations:
required: true

View File

@ -1,45 +1,48 @@
name: Lint, Test and Build
name: Lint, Test & Build
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: true
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
permissions:
actions: read
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NX_BRANCH: ${{ github.event.number || github.ref_name }}
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
jobs:
main:
name: Nx Cloud - Main Job
uses: nrwl/ci/.github/workflows/nx-cloud-main.yml@v0.14.0
with:
main-branch-name: main
number-of-agents: 3
init-commands: |
pnpm exec prisma generate
pnpm exec nx-cloud start-ci-run --stop-agents-after="build" --agent-count=3
parallel-commands: |
pnpm exec nx-cloud record -- pnpm exec nx format:check
parallel-commands-on-agents: |
pnpm exec nx affected --target=lint --parallel=3
pnpm exec nx affected --target=test --parallel=3 --ci --code-coverage
pnpm exec nx affected --target=build --parallel=3
runs-on: ubuntu-latest
agents:
name: Nx Cloud - Agents
uses: nrwl/ci/.github/workflows/nx-cloud-agents.yml@v0.14.0
with:
number-of-agents: 3
steps:
- name: Checkout Repository
uses: actions/checkout@v4.2.2
with:
fetch-depth: 2
- name: Setup pnpm
uses: pnpm/action-setup@v4.0.0
- name: Setup Node.js
uses: actions/setup-node@v4.1.0
with:
cache: "pnpm"
node-version: 22
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm run lint
- name: Format
run: pnpm run format:check
- name: Test
run: pnpm run test
- name: Build
run: pnpm run build

View File

@ -7,7 +7,7 @@ on:
- "*"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: true
env:
@ -21,15 +21,14 @@ jobs:
version: ${{ steps.version.outputs.version }}
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
# - linux/arm64 # Uncomment this if you want to build for ARM64, disabled by default due to slower build times
steps:
- name: Checkout Repository
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.4
- name: Extract version from package.json
id: version
@ -38,38 +37,42 @@ jobs:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
- name: Login to Docker Hub
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Login to GitHub Container Registery
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ github.token }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.3.0
- name: Extract Docker Metadata
id: meta
uses: docker/metadata-action@v5.0.0
uses: docker/metadata-action@v5.5.1
with:
tags: type=semver,pattern={{version}},prefix=v,value=${{ steps.version.outputs.version }}
images: |
${{ env.IMAGE }}
ghcr.io/${{ env.IMAGE }}
- name: Prepare a unique name for Artifacts
id: artifact_name
run: |
name=$(echo -n "${{ matrix.platform }}" | sed -e 's/[ \t:\/\\"<>|*?]/-/g' -e 's/--*/-/g')
echo "name=$name" >> "$GITHUB_OUTPUT"
- name: Build and Push by Digest
uses: docker/build-push-action@v5.0.0
uses: docker/build-push-action@v5.3.0
id: build
with:
context: .
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
@ -83,9 +86,9 @@ jobs:
touch "/tmp/digests/${digest#sha256:}"
- name: Upload Digest
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4.3.3
with:
name: digests
name: digests-${{ steps.artifact_name.outputs.name }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
@ -98,33 +101,34 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4.1.4
- name: Download Digest
uses: actions/download-artifact@v3.0.0
uses: actions/download-artifact@v4.1.7
with:
name: digests
path: /tmp/digests
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
pattern: digests-*
merge-multiple: true
- name: Login to Docker Hub
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Login to GitHub Container Registery
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ github.token }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.3.0
- name: Extract Docker Metadata
id: meta
uses: docker/metadata-action@v5.0.0
uses: docker/metadata-action@v5.5.1
with:
tags: type=semver,pattern={{version}},prefix=v,value=${{ needs.build.outputs.version }}
images: |
@ -142,8 +146,18 @@ jobs:
docker buildx imagetools inspect ${{ env.IMAGE }}:${{ steps.meta.outputs.version }}
- name: Update Repository Description
uses: peter-evans/dockerhub-description@v3
uses: peter-evans/dockerhub-description@v4.0.0
with:
repository: ${{ github.repository }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- 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:

4
.gitignore vendored
View File

@ -16,6 +16,7 @@ node_modules
*.sublime-workspace
# IDE - VSCode
.vs/*
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
@ -41,7 +42,6 @@ Thumbs.db
.swc
fly.toml
stats.html
libs/prisma
# Environment Variables
*.env*
@ -49,4 +49,4 @@ libs/prisma
# Lingui Compiled Messages
apps/client/src/locales/_build/
apps/client/src/locales/*/messages.mjs
apps/client/src/locales/*/messages.*js

7
.ncurc.json Normal file
View File

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

View File

@ -2,4 +2,6 @@
/coverage
/.nx/cache
stats.html
pnpm-lock.yaml
pnpm-lock.yaml
compose-dev.yml
compose.yml

View File

@ -1,3 +1,4 @@
{
"printWidth": 100
"printWidth": 100,
"endOfLine": "auto"
}

View File

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

View File

@ -10,5 +10,6 @@
"tools/compose/*"
]
},
"i18n-ally.localesPaths": ["apps/client/src/locales"]
"i18n-ally.localesPaths": ["apps/client/src/locales"],
"vitest.disableWorkspaceWarning": true
}

View File

@ -14,6 +14,6 @@ When it comes to **security**, you now have the option to protect your account w
From a **design** perspective, the motivation behind this is to ensure that Reactive Resume is taken more seriously and not perceived as just another subpar side-project, which is often associated with free software. My goal is to demonstrate that this is not the case, and that **free and open source software can be just as good**, if not better, than paid alternatives.
From a **self-hosting perspective**, it has never been simpler. Instead of running two separate services on your Docker (one for the client and one for the server) and struggling to establish communication between them, now you only need to pull a single image. Additionally, there are a few dependent services available on Docker (such as Postgres, Redis, Minio etc.) that you can also pull and have them all working together seamlessly.
From a **self-hosting perspective**, it has never been simpler. Instead of running two separate services on your Docker (one for the client and one for the server) and struggling to establish communication between them, now you only need to pull a single image. Additionally, there are a few dependent services available on Docker (such as Postgres, Minio etc.) that you can also pull and have them all working together seamlessly.
I'm excited for you to try out the app, as I've spent months building it to perfection. If you enjoy the experience of building your resume using the app, please consider supporting by [becoming a GitHub Sponsor](https://github.com/sponsors/AmruthPillai).

View File

@ -2,7 +2,7 @@
## Getting the project set up locally
There are a number of Docker Compose examples that are suitable for a wide variety of deployment strategies depending on your use-case. All of the examples can be found in the `tools/compose` folder.
There are a number of Docker Compose examples that are suitable for a wide variety of deployment strategies depending on your use-case. All the examples can be found in the `tools/compose` folder.
To run the development environment of the application locally on your computer, please follow these steps:
@ -57,15 +57,13 @@ You can also visit `http://localhost:3000/api/health`, the health check endpoint
"info": {
"database": { "status": "up" },
"storage": { "status": "up" },
"browser": { "status": "up", "version": "Chrome/119.0.6045.9" },
"redis": { "status": "up" }
"browser": { "status": "up", "version": "Chrome/119.0.6045.9" }
},
"error": {},
"details": {
"database": { "status": "up" },
"storage": { "status": "up" },
"browser": { "status": "up", "version": "Chrome/119.0.6045.9" },
"redis": { "status": "up" }
"browser": { "status": "up", "version": "Chrome/119.0.6045.9" }
}
}
```
@ -82,6 +80,6 @@ Once you are happy with the changes you've made locally, commit it to your repos
git commit -m "fix(homepage): fix typo on homepage in the faq section"
```
It helps to be as decriptive as possible in commit messages so that users can be aware of the changes made by you.
It helps to be as descriptive as possible in commit messages so that users can be aware of the changes made by you.
Finally, create a pull request to merge the changes on your forked repository to the original repository hosted on AmruthPillai/Reactive-Resume. I can take a look at the changes you've made when I have the time and have it merged onto the app.

View File

@ -1,39 +1,45 @@
ARG NX_CLOUD_ACCESS_TOKEN
# --- Base Image ---
FROM node:lts-bullseye-slim AS base
ARG NX_CLOUD_ACCESS_TOKEN
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
ARG NX_CLOUD_ACCESS_TOKEN
RUN corepack enable
RUN corepack enable pnpm && corepack prepare pnpm --activate
WORKDIR /app
# --- Build Image ---
FROM base AS build
ENV NX_CLOUD_ACCESS_TOKEN=$NX_CLOUD_ACCESS_TOKEN
ARG NX_CLOUD_ACCESS_TOKEN
COPY .npmrc package.json pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
COPY ./tools/prisma /app/tools/prisma
RUN pnpm install --frozen-lockfile
COPY . .
ENV NX_CLOUD_ACCESS_TOKEN=$NX_CLOUD_ACCESS_TOKEN
RUN pnpm run build
# --- Release Image ---
FROM base AS release
ARG NX_CLOUD_ACCESS_TOKEN
RUN apt update && apt install -y dumb-init --no-install-recommends
RUN apt update && apt install -y dumb-init --no-install-recommends && rm -rf /var/lib/apt/lists/*
COPY --chown=node:node --from=build /app/.npmrc /app/package.json /app/pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
RUN pnpm install --prod --frozen-lockfile
COPY --chown=node:node --from=build /app/dist ./dist
COPY --chown=node:node --from=build /app/tools/prisma ./tools/prisma
RUN pnpm run prisma:generate
ENV TZ=UTC
ENV PORT=3000
ENV NODE_ENV=production
EXPOSE 3000

View File

@ -40,11 +40,11 @@ Start creating your standout resume with Reactive Resume today!
- **Free, forever** and open-source
- No telemetry, user tracking or advertising
- You can self-host the application in less then 30 seconds
- You can self-host the application in less than 30 seconds
- **Available in multiple languages** ([help add/improve your language here](https://translate.rxresu.me/))
- Use your email address (or a throw-away address, no problem) to create an account
- You can also sign in with your GitHub or Google account, and even set up two-factor authentication for extra security
- Create as many resumes as you like under a single account, optimising each resume for every job application based on its description for a higher ATS score
- Create as many resumes as you like under a single account, optimising each resume for every job application based on its description for a higher ATS score
- **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
@ -69,11 +69,9 @@ Start creating your standout resume with Reactive Resume today!
- NestJS, for the backend
- Postgres (primary database)
- Prisma ORM, which frees you to switch to any other relational database with a few minor changes in the code
- Redis (for caching, session storage and resume statistics)
- 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)
@ -93,3 +91,9 @@ Reactive Resume is packaged and distributed using the [MIT License](/LICENSE.md)
_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">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" width="200px">
</a>
</p>

View File

@ -11,4 +11,4 @@
## Reporting a Vulnerability
Please raise an issue on GitHub to report any security vulnerabilities in the app. If the vulnerability is potentially lethal, send me an email about it on hello@amruthpillai.com.
Please raise an issue on GitHub to report any security vulnerabilities in the app. If the vulnerability is potentially lethal, email me about it on hello@amruthpillai.com.

View File

@ -12,6 +12,21 @@
}
},
"rules": {
// eslint
"@typescript-eslint/no-require-imports": "off",
// react
"react/no-unescaped-entities": "off",
"react/jsx-sort-props": [
"error",
{
"reservedFirst": true,
"callbacksLast": true,
"shorthandFirst": true,
"noSortAlphabetically": true
}
],
// react-hooks
"react-hooks/exhaustive-deps": "off",

View File

@ -1,9 +1,9 @@
const { join } = require("path");
const path = require("node:path");
module.exports = {
plugins: {
tailwindcss: {
config: join(__dirname, "tailwind.config.js"),
config: path.join(__dirname, "tailwind.config.js"),
},
autoprefixer: {},
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" fill="none"><rect width="256" height="256" rx="80" style="fill:#000;stroke:none;stroke-width:.396357"/><g fill="#09090b" fill-rule="evenodd" clip-rule="evenodd"><path d="m282.857 257.192-37.64 50.15 39.21.004 17.95-23.88 17.95 23.876h39.21l-75.11-100.256-39.21-.003zm23.775-31.764 13.696-18.345h39.21l-33.234 44.435zM119.537 135.21v129.485h36.626V230.29h19.993c17.99-.01 40.841-3.946 52.762-21.828 4.686-7.152 7.03-15.6 7.03-25.342 0-9.865-2.344-18.374-7.03-25.527-4.687-7.276-11.346-12.825-19.978-16.648-8.51-3.823-18.37-5.735-30.21-5.735zm90.963 95.183s-14.972 6.285-34.681 6.047l21.162 28.255h39.21zm-54.337-28.405h20.348c7.646 0 13.319-1.665 17.018-4.995 3.823-3.33 5.735-7.954 5.735-13.874 0-6.042-1.912-10.729-5.735-14.058-3.7-3.33-9.372-4.995-17.018-4.995h-20.348z" style="fill:#fafafa;fill-opacity:1;stroke-width:.9375" transform="matrix(.91667 0 0 .91667 -91.576 -74.838)"/></g></svg>

After

Width:  |  Height:  |  Size: 958 B

View File

@ -1,8 +1 @@
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M173.611 166.311L132.877 219.804H173.524L193.973 191.813L213.183 219.804H256L215.673 165.707L215.15 165.046L207.461 155.332L195.329 140.004L195.258 139.915L193.813 138.089L193.923 138.001L176.286 112.861H134.061L173.611 166.311ZM199.89 133.554L214.959 112.861H254.619L219.874 158.8L199.89 133.554Z"
fill="#09090B" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M0 36.1959V174.314H39.0678V137.614H60.3938L60.4323 137.671C60.8436 137.653 61.2518 137.634 61.6569 137.614C75.0665 136.968 85.1471 135.549 96.3849 131.385C96.7596 131.246 97.1355 131.104 97.5128 130.959L97.4591 130.881C105.816 126.86 112.331 121.344 117.006 114.331C122.005 106.702 124.504 97.6915 124.504 87.2997C124.504 76.7764 122.005 67.7 117.006 60.0706C112.007 52.3097 104.904 46.3903 95.6964 42.3125C86.62 38.2347 75.7678 36.1959 63.1399 36.1959H0ZM102.156 137.725L64.8705 144.175L85.4361 174.314H127.266L102.156 137.725ZM39.0678 107.426H60.7721C68.9277 107.426 74.9786 105.65 78.9248 102.098C83.0026 98.5465 85.0415 93.6137 85.0415 87.2997C85.0415 80.8542 83.0026 75.8556 78.9248 72.304C74.9786 68.7523 68.9277 66.9765 60.7721 66.9765H39.0678V107.426Z"
fill="#09090B" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#09090b" fill-rule="evenodd" d="m16.332 15.592-3.764 5.015h3.921l1.795-2.388 1.795 2.388H24L16.489 10.58h-3.921Zm2.377-3.177 1.37-1.834H24l-3.323 4.443zM0 3.393v12.949h3.663v-3.44h1.999c1.799-.002 4.084-.395 5.276-2.183.469-.716.703-1.56.703-2.535 0-.986-.234-1.837-.703-2.552-.469-.728-1.135-1.283-1.998-1.665-.85-.382-1.837-.574-3.02-.574Zm9.096 9.519s-1.497.628-3.468.604l2.116 2.826h3.921zm-5.433-2.84h2.034c.765 0 1.332-.167 1.702-.5.382-.333.574-.796.574-1.388 0-.604-.192-1.073-.574-1.405-.37-.333-.937-.5-1.702-.5H3.663Z" clip-rule="evenodd" style="stroke-width:.09375"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 672 B

View File

@ -1,8 +1 @@
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M173.611 166.311L132.877 219.804H173.524L193.973 191.813L213.183 219.804H256L215.673 165.707L215.15 165.046L207.461 155.332L195.329 140.004L195.258 139.915L193.813 138.089L193.923 138.001L176.286 112.861H134.061L173.611 166.311ZM199.89 133.554L214.959 112.861H254.619L219.874 158.8L199.89 133.554Z"
fill="#FAFAFA" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M0 36.1959V174.314H39.0678V137.614H60.3938L60.4323 137.671C60.8436 137.653 61.2517 137.634 61.6567 137.614C75.0665 136.968 85.1471 135.549 96.385 131.385C96.7596 131.246 97.1355 131.104 97.5128 130.959L97.4591 130.881C105.816 126.86 112.331 121.344 117.006 114.331C122.005 106.702 124.504 97.6915 124.504 87.2997C124.504 76.7764 122.005 67.7 117.006 60.0706C112.007 52.3097 104.904 46.3903 95.6964 42.3125C86.62 38.2347 75.7679 36.1959 63.1399 36.1959H0ZM102.156 137.725L64.8705 144.175L85.4361 174.314H127.266L102.156 137.725ZM39.0678 107.426H60.7721C68.9277 107.426 74.9786 105.65 78.9248 102.098C83.0026 98.5465 85.0415 93.6137 85.0415 87.2997C85.0415 80.8542 83.0026 75.8556 78.9248 72.304C74.9786 68.7523 68.9277 66.9765 60.7721 66.9765H39.0678V107.426Z"
fill="#FAFAFA" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#09090b" fill-rule="evenodd" d="m16.332 15.592-3.764 5.015h3.921l1.795-2.388 1.795 2.388H24L16.489 10.58h-3.921Zm2.377-3.177 1.37-1.834H24l-3.323 4.443zM0 3.393v12.949h3.663v-3.44h1.999c1.799-.002 4.084-.395 5.276-2.183.469-.716.703-1.56.703-2.535 0-.986-.234-1.837-.703-2.552-.469-.728-1.135-1.283-1.998-1.665-.85-.382-1.837-.574-3.02-.574Zm9.096 9.519s-1.497.628-3.468.604l2.116 2.826h3.921zm-5.433-2.84h2.034c.765 0 1.332-.167 1.702-.5.382-.333.574-.796.574-1.388 0-.604-.192-1.073-.574-1.405-.37-.333-.937-.5-1.702-.5H3.663Z" clip-rule="evenodd" style="stroke-width:.09375;fill:#fafafa;fill-opacity:1"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 700 B

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

@ -4,7 +4,8 @@ import { RouterProvider } from "react-router-dom";
import { router } from "./router";
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const root = ReactDOM.createRoot(document.querySelector("#root")!);
root.render(
<StrictMode>

View File

@ -39,26 +39,19 @@ export const ArtboardPage = () => {
`${metadata.typography.lineHeight}`,
);
document.documentElement.style.setProperty("--color-text", `${metadata.theme.text}`);
document.documentElement.style.setProperty("--color-primary", `${metadata.theme.primary}`);
document.documentElement.style.setProperty(
"--color-background",
`${metadata.theme.background}`,
);
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]);
// Typography Options
useEffect(() => {
if (metadata.typography.hideIcons) {
document.querySelector("#root")!.classList.add("hide-icons");
} else {
document.querySelector("#root")!.classList.remove("hide-icons");
}
// eslint-disable-next-line unicorn/prefer-spread
const elements = Array.from(document.querySelectorAll(`[data-page]`));
if (metadata.typography.underlineLinks) {
document.querySelector("#root")!.classList.add("underline-links");
} else {
document.querySelector("#root")!.classList.remove("underline-links");
for (const el of elements) {
el.classList.toggle("hide-icons", metadata.typography.hideIcons);
el.classList.toggle("underline-links", metadata.typography.underlineLinks);
}
}, [metadata]);

View File

@ -38,11 +38,11 @@ export const BuilderLayout = () => {
return (
<TransformWrapper
ref={transformRef}
centerOnInit
maxScale={2}
minScale={0.4}
initialScale={0.8}
ref={transformRef}
limitToBounds={false}
>
<TransformComponent
@ -56,8 +56,8 @@ export const BuilderLayout = () => {
<AnimatePresence>
{layout.map((columns, pageIndex) => (
<motion.div
layout
key={pageIndex}
layout
initial={{ opacity: 0, x: -200, y: 0 }}
animate={{ opacity: 1, x: 0, transition: { delay: pageIndex * 0.3 } }}
exit={{ opacity: 0, x: -200 }}

View File

@ -20,7 +20,10 @@ export const Providers = () => {
};
const resumeData = window.localStorage.getItem("resume");
if (resumeData) return setResume(JSON.parse(resumeData));
if (resumeData) {
setResume(JSON.parse(resumeData));
return;
}
window.addEventListener("message", handleMessage);
@ -34,6 +37,7 @@ export const Providers = () => {
// setResume(sampleResume);
// }, [setResume]);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!resume) return null;
return <Outlet />;

View File

@ -8,5 +8,7 @@ export type ArtboardStore = {
export const useArtboardStore = create<ArtboardStore>()((set) => ({
resume: null as unknown as ResumeData,
setResume: (resume) => set({ resume }),
setResume: (resume) => {
set({ resume });
},
}));

View File

@ -12,14 +12,14 @@
@apply antialiased;
}
#root.hide-icons .ph {
[data-page].hide-icons .ph {
@apply hidden;
}
#root.underline-links a {
[data-page].underline-links a {
@apply underline underline-offset-2;
}
.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;
@apply prose max-w-none prose-foreground prose-headings:mt-0 prose-headings:mb-2 prose-p:mt-0 prose-p:mb-2 prose-ul:mt-0 prose-ul:mb-2 prose-li:mt-0 prose-li:mb-2 prose-ol:mt-0 prose-ol:mb-2 prose-img:mt-0 prose-img:mb-2 prose-hr:mt-0 prose-hr:mb-2 prose-p:leading-normal prose-li:leading-normal prose-a:break-all;
}

View File

@ -64,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,18 +90,18 @@ const Summary = () => {
</div>
<div className="mb-2 hidden items-center gap-x-2 text-center font-bold text-primary group-[.sidebar]:flex">
<div className="h-1.5 w-1.5 rounded-full border border-primary" />
<div className="size-1.5 rounded-full border border-primary" />
<h4>{section.name}</h4>
<div className="h-1.5 w-1.5 rounded-full border border-primary" />
<div className="size-1.5 rounded-full border border-primary" />
</div>
<main className={cn("relative space-y-2", "border-l border-primary pl-4")}>
<div className="absolute left-[-4.5px] top-[8px] hidden h-[8px] w-[8px] rounded-full bg-primary group-[.main]:block" />
<div className="absolute left-[-4.5px] top-[8px] hidden size-[8px] rounded-full bg-primary group-[.main]:block" />
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
style={{ columns: section.columns }}
dangerouslySetInnerHTML={{ __html: section.content }}
/>
</main>
</section>
@ -117,28 +123,51 @@ 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"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label || url.label || url.href}
{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 +187,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || !section.items.length) return null;
if (!section.visible || section.items.length === 0) return null;
return (
<section id={section.id} className="grid">
@ -167,9 +196,9 @@ const Section = <T,>({
</div>
<div className="mx-auto mb-2 hidden items-center gap-x-2 text-center font-bold text-primary group-[.sidebar]:flex">
<div className="h-1.5 w-1.5 rounded-full border border-primary" />
<div className="size-1.5 rounded-full border border-primary" />
<h4>{section.name}</h4>
<div className="h-1.5 w-1.5 rounded-full border border-primary" />
<div className="size-1.5 rounded-full border border-primary" />
</div>
<div
@ -196,7 +225,7 @@ const Section = <T,>({
<div>{children?.(item as T)}</div>
{summary !== undefined && !isEmptyString(summary) && (
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -205,9 +234,9 @@ 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 h-[8px] w-[8px] rounded-full bg-primary group-[.main]:block" />
<div className="absolute left-[-4.5px] top-px hidden size-[8px] rounded-full bg-primary group-[.main]:block" />
</div>
);
})}
@ -241,7 +270,7 @@ const Profiles = () => {
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
@ -255,7 +284,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 +306,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 +330,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 +346,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 +386,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 +407,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 +445,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 +468,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 +494,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>
@ -449,36 +513,50 @@ const Custom = ({ id }: { id: string }) => {
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles":
case "profiles": {
return <Profiles />;
case "summary":
}
case "summary": {
return <Summary />;
case "experience":
}
case "experience": {
return <Experience />;
case "education":
}
case "education": {
return <Education />;
case "awards":
}
case "awards": {
return <Awards />;
case "certifications":
}
case "certifications": {
return <Certifications />;
case "skills":
}
case "skills": {
return <Skills />;
case "interests":
}
case "interests": {
return <Interests />;
case "publications":
}
case "publications": {
return <Publications />;
case "volunteer":
}
case "volunteer": {
return <Volunteer />;
case "languages":
}
case "languages": {
return <Languages />;
case "projects":
}
case "projects": {
return <Projects />;
case "references":
}
case "references": {
return <References />;
default:
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};

View File

@ -64,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 +90,9 @@ const Summary = () => {
</div>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg col-span-4"
style={{ columns: section.columns }}
dangerouslySetInnerHTML={{ __html: section.content }}
/>
</section>
);
@ -99,7 +105,7 @@ const Rating = ({ level }: RatingProps) => (
{Array.from({ length: 5 }).map((_, index) => (
<div
key={index}
className={cn("h-2 w-2 rounded-full border border-primary", level > index && "bg-primary")}
className={cn("size-2 rounded-full border border-primary", level > index && "bg-primary")}
/>
))}
</div>
@ -108,28 +114,51 @@ 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"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label || url.label || url.href}
{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 +178,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || !section.items.length) return null;
if (!section.visible || section.items.length === 0) return null;
return (
<section id={section.id} className="grid grid-cols-5 border-t pt-2.5">
@ -173,11 +202,11 @@ 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 className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -218,7 +247,7 @@ const Profiles = () => {
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
@ -231,9 +260,14 @@ const Experience = () => {
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between">
<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>
@ -253,9 +287,14 @@ const Education = () => {
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between">
<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>
@ -276,10 +315,14 @@ const Awards = () => {
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between">
<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">
@ -297,10 +340,10 @@ const Certifications = () => {
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between">
<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">
@ -343,9 +386,14 @@ const Publications = () => {
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between">
<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>
@ -364,9 +412,14 @@ const Volunteer = () => {
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between">
<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>
@ -401,9 +454,14 @@ const Projects = () => {
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div className="flex items-center justify-between">
<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 +481,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>
)}
@ -442,9 +505,14 @@ const Custom = ({ id }: { id: string }) => {
keywordsKey="keywords"
>
{(item) => (
<div className="flex items-center justify-between">
<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>
@ -460,36 +528,50 @@ const Custom = ({ id }: { id: string }) => {
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles":
case "profiles": {
return <Profiles />;
case "summary":
}
case "summary": {
return <Summary />;
case "experience":
}
case "experience": {
return <Experience />;
case "education":
}
case "education": {
return <Education />;
case "awards":
}
case "awards": {
return <Awards />;
case "certifications":
}
case "certifications": {
return <Certifications />;
case "skills":
}
case "skills": {
return <Skills />;
case "interests":
}
case "interests": {
return <Interests />;
case "publications":
}
case "publications": {
return <Publications />;
case "volunteer":
}
case "volunteer": {
return <Volunteer />;
case "languages":
}
case "languages": {
return <Languages />;
case "projects":
}
case "projects": {
return <Projects />;
case "references":
}
case "references": {
return <References />;
default:
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};

View File

@ -65,7 +65,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 +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"
style={{ columns: section.columns }}
dangerouslySetInnerHTML={{ __html: section.content }}
/>
</section>
);
@ -100,7 +106,7 @@ const Rating = ({ level }: RatingProps) => (
<div
key={index}
className={cn(
"h-2 w-2 rounded-full border border-primary group-[.sidebar]:border-background",
"size-2 rounded-full border border-primary group-[.sidebar]:border-background",
level > index && "bg-primary group-[.sidebar]:bg-background",
)}
/>
@ -111,28 +117,51 @@ 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"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label || url.label || url.href}
{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;
@ -152,7 +181,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || !section.items.length) return null;
if (!section.visible || section.items.length === 0) return null;
return (
<section id={section.id} className="grid">
@ -174,11 +203,11 @@ 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 className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -200,9 +229,14 @@ const Experience = () => {
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -222,9 +256,14 @@ const Education = () => {
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -264,7 +303,7 @@ const Profiles = () => {
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
@ -277,10 +316,14 @@ const Awards = () => {
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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">
@ -298,13 +341,10 @@ const Certifications = () => {
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -344,9 +384,14 @@ const Publications = () => {
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -365,9 +410,14 @@ const Volunteer = () => {
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -402,9 +452,14 @@ const Projects = () => {
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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 +479,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,9 +503,14 @@ const Custom = ({ id }: { id: string }) => {
keywordsKey="keywords"
>
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -461,36 +526,50 @@ const Custom = ({ id }: { id: string }) => {
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles":
case "profiles": {
return <Profiles />;
case "summary":
}
case "summary": {
return <Summary />;
case "experience":
}
case "experience": {
return <Experience />;
case "education":
}
case "education": {
return <Education />;
case "awards":
}
case "awards": {
return <Awards />;
case "certifications":
}
case "certifications": {
return <Certifications />;
case "skills":
}
case "skills": {
return <Skills />;
case "interests":
}
case "interests": {
return <Interests />;
case "publications":
}
case "publications": {
return <Publications />;
case "volunteer":
}
case "volunteer": {
return <Volunteer />;
case "languages":
}
case "languages": {
return <Languages />;
case "projects":
}
case "projects": {
return <Projects />;
case "references":
}
case "references": {
return <References />;
default:
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};
@ -498,7 +577,7 @@ export const Chikorita = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="grid h-full grid-cols-3">
<div className="grid min-h-[inherit] grid-cols-3">
<div className="main p-custom group col-span-2 space-y-4">
{isFirstPage && <Header />}

View File

@ -38,7 +38,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 && (
<>
@ -46,7 +46,7 @@ const Header = () => {
<i className="ph ph-bold ph-map-pin text-primary" />
<div>{basics.location}</div>
</div>
<div className="bg-text h-1 w-1 rounded-full last:hidden" />
<div className="bg-text size-1 rounded-full last:hidden" />
</>
)}
@ -58,7 +58,7 @@ const Header = () => {
{basics.phone}
</a>
</div>
<div className="bg-text h-1 w-1 rounded-full last:hidden" />
<div className="bg-text size-1 rounded-full last:hidden" />
</>
)}
{basics.email && (
@ -69,22 +69,28 @@ const Header = () => {
{basics.email}
</a>
</div>
<div className="bg-text h-1 w-1 rounded-full last:hidden" />
<div className="bg-text size-1 rounded-full last:hidden" />
</>
)}
{isUrl(basics.url.href) && (
<>
<Link url={basics.url} />
<div className="bg-text h-1 w-1 rounded-full last:hidden" />
<div className="bg-text size-1 rounded-full last:hidden" />
</>
)}
{basics.customFields.map((item) => (
<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 h-1 w-1 rounded-full last:hidden" />
<div className="bg-text size-1 rounded-full last:hidden" />
</Fragment>
))}
</div>
@ -104,9 +110,9 @@ const Summary = () => {
<h4 className="mb-2 text-base font-bold">{section.name}</h4>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
style={{ columns: section.columns }}
dangerouslySetInnerHTML={{ __html: section.content }}
/>
</section>
);
@ -128,28 +134,51 @@ 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"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label || url.label || url.href}
{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 +198,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || !section.items.length) return null;
if (!section.visible || section.items.length === 0) return null;
return (
<section id={section.id} className="grid">
@ -195,14 +224,14 @@ 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-[4px] border-primary group-[.sidebar]:hidden" />
<div className="absolute inset-y-0 -left-px border-l-4 border-primary group-[.sidebar]:hidden" />
</div>
{summary !== undefined && !isEmptyString(summary) && (
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -245,7 +274,7 @@ const Profiles = () => {
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
@ -258,9 +287,14 @@ const Experience = () => {
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -280,9 +314,14 @@ const Education = () => {
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -303,10 +342,14 @@ const Awards = () => {
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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">
@ -324,10 +367,10 @@ const Certifications = () => {
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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">
@ -370,9 +413,14 @@ const Publications = () => {
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -391,9 +439,14 @@ const Volunteer = () => {
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -428,9 +481,14 @@ const Projects = () => {
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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 +508,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>
)}
@ -469,9 +532,14 @@ const Custom = ({ id }: { id: string }) => {
keywordsKey="keywords"
>
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -487,36 +555,50 @@ const Custom = ({ id }: { id: string }) => {
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles":
case "profiles": {
return <Profiles />;
case "summary":
}
case "summary": {
return <Summary />;
case "experience":
}
case "experience": {
return <Experience />;
case "education":
}
case "education": {
return <Education />;
case "awards":
}
case "awards": {
return <Awards />;
case "certifications":
}
case "certifications": {
return <Certifications />;
case "skills":
}
case "skills": {
return <Skills />;
case "interests":
}
case "interests": {
return <Interests />;
case "publications":
}
case "publications": {
return <Publications />;
case "volunteer":
}
case "volunteer": {
return <Volunteer />;
case "languages":
}
case "languages": {
return <Languages />;
case "projects":
}
case "projects": {
return <Projects />;
case "references":
}
case "references": {
return <References />;
default:
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};

View File

@ -65,7 +65,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>
))}
@ -82,9 +88,9 @@ const Summary = () => {
return (
<section id={section.id}>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
style={{ columns: section.columns }}
dangerouslySetInnerHTML={{ __html: section.content }}
/>
</section>
);
@ -106,28 +112,57 @@ 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"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label || url.label || url.href}
{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 +182,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || !section.items.length) return null;
if (!section.visible || section.items.length === 0) return null;
return (
<section id={section.id} className="grid">
@ -169,11 +204,11 @@ 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 className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -214,7 +249,7 @@ const Profiles = () => {
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
@ -227,9 +262,14 @@ const Experience = () => {
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -249,9 +289,14 @@ const Education = () => {
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -272,10 +317,14 @@ const Awards = () => {
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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">
@ -293,10 +342,10 @@ const Certifications = () => {
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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">
@ -339,9 +388,14 @@ const Publications = () => {
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -360,9 +414,14 @@ const Volunteer = () => {
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -397,9 +456,14 @@ const Projects = () => {
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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 +483,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>
)}
@ -438,9 +507,14 @@ const Custom = ({ id }: { id: string }) => {
keywordsKey="keywords"
>
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -456,34 +530,47 @@ const Custom = ({ id }: { id: string }) => {
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles":
case "profiles": {
return <Profiles />;
case "experience":
}
case "experience": {
return <Experience />;
case "education":
}
case "education": {
return <Education />;
case "awards":
}
case "awards": {
return <Awards />;
case "certifications":
}
case "certifications": {
return <Certifications />;
case "skills":
}
case "skills": {
return <Skills />;
case "interests":
}
case "interests": {
return <Interests />;
case "publications":
}
case "publications": {
return <Publications />;
case "volunteer":
}
case "volunteer": {
return <Volunteer />;
case "languages":
}
case "languages": {
return <Languages />;
case "projects":
}
case "projects": {
return <Projects />;
case "references":
}
case "references": {
return <References />;
default:
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};

View File

@ -65,7 +65,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 +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"
style={{ columns: section.columns }}
dangerouslySetInnerHTML={{ __html: section.content }}
/>
</section>
);
@ -114,28 +120,53 @@ 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"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label || url.label || url.href}
{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 +186,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || !section.items.length) return null;
if (!section.visible || section.items.length === 0) return null;
return (
<section id={section.id} className="grid">
@ -179,11 +210,11 @@ 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 className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -205,9 +236,14 @@ const Experience = () => {
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -227,9 +263,14 @@ const Education = () => {
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -269,7 +310,7 @@ const Profiles = () => {
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
@ -282,10 +323,14 @@ const Awards = () => {
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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">
@ -303,10 +348,10 @@ const Certifications = () => {
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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">
@ -349,9 +394,14 @@ const Publications = () => {
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -370,9 +420,14 @@ const Volunteer = () => {
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -407,9 +462,14 @@ const Projects = () => {
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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 +489,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>
)}
@ -448,9 +513,14 @@ const Custom = ({ id }: { id: string }) => {
keywordsKey="keywords"
>
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -466,36 +536,50 @@ const Custom = ({ id }: { id: string }) => {
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles":
case "profiles": {
return <Profiles />;
case "summary":
}
case "summary": {
return <Summary />;
case "experience":
}
case "experience": {
return <Experience />;
case "education":
}
case "education": {
return <Education />;
case "awards":
}
case "awards": {
return <Awards />;
case "certifications":
}
case "certifications": {
return <Certifications />;
case "skills":
}
case "skills": {
return <Skills />;
case "interests":
}
case "interests": {
return <Interests />;
case "publications":
}
case "publications": {
return <Publications />;
case "volunteer":
}
case "volunteer": {
return <Volunteer />;
case "languages":
}
case "languages": {
return <Languages />;
case "projects":
}
case "projects": {
return <Projects />;
case "references":
}
case "references": {
return <References />;
default:
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};

View File

@ -15,31 +15,44 @@ import { Rhyhorn } from "./rhyhorn";
export const getTemplate = (template: Template) => {
switch (template) {
case "azurill":
case "azurill": {
return Azurill;
case "bronzor":
}
case "bronzor": {
return Bronzor;
case "chikorita":
}
case "chikorita": {
return Chikorita;
case "ditto":
}
case "ditto": {
return Ditto;
case "gengar":
}
case "gengar": {
return Gengar;
case "glalie":
}
case "glalie": {
return Glalie;
case "kakuna":
}
case "kakuna": {
return Kakuna;
case "leafish":
}
case "leafish": {
return Leafish;
case "nosepass":
}
case "nosepass": {
return Nosepass;
case "onyx":
}
case "onyx": {
return Onyx;
case "pikachu":
}
case "pikachu": {
return Pikachu;
case "rhyhorn":
}
case "rhyhorn": {
return Rhyhorn;
default:
}
default: {
return Onyx;
}
}
};

View File

@ -65,7 +65,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>
@ -110,9 +116,9 @@ const Summary = () => {
</h4>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
style={{ columns: section.columns }}
dangerouslySetInnerHTML={{ __html: section.content }}
/>
</section>
);
@ -134,28 +140,51 @@ 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"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label || url.label || url.href}
{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 +204,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || !section.items.length) return null;
if (!section.visible || section.items.length === 0) return null;
return (
<section id={section.id} className="grid">
@ -200,7 +229,7 @@ const Section = <T,>({
<div>{children?.(item as T)}</div>
{summary !== undefined && !isEmptyString(summary) && (
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -209,7 +238,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 +254,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 +276,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 +300,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 +316,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 +356,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 +377,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 +415,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 +437,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 +463,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>
@ -417,34 +481,47 @@ const Custom = ({ id }: { id: string }) => {
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "summary":
case "summary": {
return <Summary />;
case "experience":
}
case "experience": {
return <Experience />;
case "education":
}
case "education": {
return <Education />;
case "awards":
}
case "awards": {
return <Awards />;
case "certifications":
}
case "certifications": {
return <Certifications />;
case "skills":
}
case "skills": {
return <Skills />;
case "interests":
}
case "interests": {
return <Interests />;
case "publications":
}
case "publications": {
return <Publications />;
case "volunteer":
}
case "volunteer": {
return <Volunteer />;
case "languages":
}
case "languages": {
return <Languages />;
case "projects":
}
case "projects": {
return <Projects />;
case "references":
}
case "references": {
return <References />;
default:
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};

View File

@ -44,9 +44,9 @@ const Header = () => {
</div>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
style={{ columns: section.columns }}
dangerouslySetInnerHTML={{ __html: section.content }}
/>
</div>
@ -81,7 +81,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>
@ -131,28 +137,51 @@ 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"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label || url.label || url.href}
{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 +201,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || !section.items.length) return null;
if (!section.visible || section.items.length === 0) return null;
return (
<section id={section.id} className="grid">
@ -197,7 +226,7 @@ const Section = <T,>({
<div>{children?.(item as T)}</div>
{summary !== undefined && !isEmptyString(summary) && (
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -206,7 +235,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 +251,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 +273,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 +297,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 +313,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 +353,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 +374,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 +412,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 +434,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 +460,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>
@ -414,32 +478,44 @@ const Custom = ({ id }: { id: string }) => {
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "experience":
case "experience": {
return <Experience />;
case "education":
}
case "education": {
return <Education />;
case "awards":
}
case "awards": {
return <Awards />;
case "certifications":
}
case "certifications": {
return <Certifications />;
case "skills":
}
case "skills": {
return <Skills />;
case "interests":
}
case "interests": {
return <Interests />;
case "publications":
}
case "publications": {
return <Publications />;
case "volunteer":
}
case "volunteer": {
return <Volunteer />;
case "languages":
}
case "languages": {
return <Languages />;
case "projects":
}
case "projects": {
return <Projects />;
case "references":
}
case "references": {
return <References />;
default:
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};

View File

@ -70,8 +70,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>
@ -94,13 +102,13 @@ const Summary = () => {
<div className="col-span-3">
<div className="relative">
<hr className="mt-3 border-primary pb-3" />
<div className="absolute bottom-3 right-0 h-3 w-3 bg-primary" />
<div className="absolute bottom-3 right-0 size-3 bg-primary" />
</div>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
style={{ columns: section.columns }}
dangerouslySetInnerHTML={{ __html: section.content }}
/>
</div>
</section>
@ -110,28 +118,51 @@ 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"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label || url.label || url.href}
{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 +181,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || !section.items.length) return null;
if (!section.visible || section.items.length === 0) return null;
return (
<section id={section.id} className={cn("grid", dateKey !== undefined && "gap-y-4")}>
@ -162,7 +193,7 @@ const Section = <T,>({
<div className="col-span-3">
<div className="relative">
<hr className="mt-3 border-primary" />
<div className="absolute bottom-0 right-0 h-3 w-3 bg-primary" />
<div className="absolute bottom-0 right-0 size-3 bg-primary" />
</div>
</div>
</div>
@ -184,10 +215,10 @@ 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 className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
)}
{keywords !== undefined && keywords.length > 0 && (
@ -219,10 +250,10 @@ 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 className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
)}
{keywords !== undefined && keywords.length > 0 && (
@ -263,7 +294,7 @@ const Profiles = () => {
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
@ -277,7 +308,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 +329,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 +352,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 +367,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 +406,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 +426,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 +468,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 +488,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 +514,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>
@ -464,36 +530,50 @@ const Custom = ({ id }: { id: string }) => {
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles":
case "profiles": {
return <Profiles />;
case "summary":
}
case "summary": {
return <Summary />;
case "experience":
}
case "experience": {
return <Experience />;
case "education":
}
case "education": {
return <Education />;
case "awards":
}
case "awards": {
return <Awards />;
case "certifications":
}
case "certifications": {
return <Certifications />;
case "skills":
}
case "skills": {
return <Skills />;
case "interests":
}
case "interests": {
return <Interests />;
case "publications":
}
case "publications": {
return <Publications />;
case "volunteer":
}
case "volunteer": {
return <Volunteer />;
case "languages":
}
case "languages": {
return <Languages />;
case "projects":
}
case "projects": {
return <Projects />;
case "references":
}
case "references": {
return <References />;
default:
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};
@ -505,7 +585,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

@ -66,7 +66,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>
@ -113,9 +119,9 @@ const Summary = () => {
<h4 className="font-bold text-primary">{section.name}</h4>
<div
dangerouslySetInnerHTML={{ __html: section.content }}
className="wysiwyg"
style={{ columns: section.columns }}
dangerouslySetInnerHTML={{ __html: section.content }}
/>
</section>
);
@ -128,7 +134,7 @@ const Rating = ({ level }: RatingProps) => (
{Array.from({ length: 5 }).map((_, index) => (
<div
key={index}
className={cn("h-3 w-3 rounded border-2 border-primary", level > index && "bg-primary")}
className={cn("size-3 rounded border-2 border-primary", level > index && "bg-primary")}
/>
))}
</div>
@ -137,28 +143,51 @@ 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"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label || url.label || url.href}
{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 +207,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || !section.items.length) return null;
if (!section.visible || section.items.length === 0) return null;
return (
<section id={section.id} className="grid">
@ -200,11 +229,11 @@ 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 className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -226,9 +255,14 @@ const Experience = () => {
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between">
<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>
@ -248,9 +282,14 @@ const Education = () => {
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between">
<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>
@ -271,10 +310,14 @@ const Awards = () => {
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between">
<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">
@ -292,10 +335,10 @@ const Certifications = () => {
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between">
<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">
@ -338,9 +381,14 @@ const Publications = () => {
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between">
<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>
@ -359,9 +407,14 @@ const Volunteer = () => {
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between">
<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>
@ -396,9 +449,14 @@ const Projects = () => {
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div className="flex items-center justify-between">
<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 +476,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>
)}
@ -437,9 +500,14 @@ const Custom = ({ id }: { id: string }) => {
keywordsKey="keywords"
>
{(item) => (
<div className="flex items-center justify-between">
<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>
@ -455,34 +523,47 @@ const Custom = ({ id }: { id: string }) => {
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "summary":
case "summary": {
return <Summary />;
case "experience":
}
case "experience": {
return <Experience />;
case "education":
}
case "education": {
return <Education />;
case "awards":
}
case "awards": {
return <Awards />;
case "certifications":
}
case "certifications": {
return <Certifications />;
case "skills":
}
case "skills": {
return <Skills />;
case "interests":
}
case "interests": {
return <Interests />;
case "publications":
}
case "publications": {
return <Publications />;
case "volunteer":
}
case "volunteer": {
return <Volunteer />;
case "languages":
}
case "languages": {
return <Languages />;
case "projects":
}
case "projects": {
return <Projects />;
case "references":
}
case "references": {
return <References />;
default:
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};

View File

@ -49,7 +49,7 @@ const Header = () => {
<i className="ph ph-bold ph-map-pin" />
<div>{basics.location}</div>
</div>
<div className="h-1 w-1 rounded-full bg-background last:hidden" />
<div className="size-1 rounded-full bg-background last:hidden" />
</>
)}
{basics.phone && (
@ -60,7 +60,7 @@ const Header = () => {
{basics.phone}
</a>
</div>
<div className="h-1 w-1 rounded-full bg-background last:hidden" />
<div className="size-1 rounded-full bg-background last:hidden" />
</>
)}
{basics.email && (
@ -71,22 +71,28 @@ const Header = () => {
{basics.email}
</a>
</div>
<div className="h-1 w-1 rounded-full bg-background last:hidden" />
<div className="size-1 rounded-full bg-background last:hidden" />
</>
)}
{isUrl(basics.url.href) && (
<>
<Link url={basics.url} />
<div className="h-1 w-1 rounded-full bg-background last:hidden" />
<div className="size-1 rounded-full bg-background last:hidden" />
</>
)}
{basics.customFields.map((item) => (
<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="h-1 w-1 rounded-full bg-background last:hidden" />
<div className="size-1 rounded-full bg-background last:hidden" />
</Fragment>
))}
</div>
@ -105,9 +111,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"
style={{ columns: section.columns }}
dangerouslySetInnerHTML={{ __html: section.content }}
/>
</section>
);
@ -137,28 +143,57 @@ 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"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label || url.label || url.href}
{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 +213,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || !section.items.length) return null;
if (!section.visible || section.items.length === 0) return null;
return (
<section id={section.id} className="grid">
@ -200,11 +235,11 @@ 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 className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -245,7 +280,7 @@ const Profiles = () => {
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
@ -258,9 +293,14 @@ const Experience = () => {
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -280,9 +320,14 @@ const Education = () => {
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -303,10 +348,14 @@ const Awards = () => {
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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">
@ -324,10 +373,10 @@ const Certifications = () => {
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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">
@ -370,9 +419,14 @@ const Publications = () => {
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -391,9 +445,14 @@ const Volunteer = () => {
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -428,9 +487,14 @@ const Projects = () => {
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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 +514,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>
)}
@ -469,9 +538,14 @@ const Custom = ({ id }: { id: string }) => {
keywordsKey="keywords"
>
{(item) => (
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
<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>
@ -487,36 +561,50 @@ const Custom = ({ id }: { id: string }) => {
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles":
case "profiles": {
return <Profiles />;
case "summary":
}
case "summary": {
return <Summary />;
case "experience":
}
case "experience": {
return <Experience />;
case "education":
}
case "education": {
return <Education />;
case "awards":
}
case "awards": {
return <Awards />;
case "certifications":
}
case "certifications": {
return <Certifications />;
case "skills":
}
case "skills": {
return <Skills />;
case "interests":
}
case "interests": {
return <Interests />;
case "publications":
}
case "publications": {
return <Publications />;
case "volunteer":
}
case "volunteer": {
return <Volunteer />;
case "languages":
}
case "languages": {
return <Languages />;
case "projects":
}
case "projects": {
return <Projects />;
case "references":
}
case "references": {
return <References />;
default:
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};

View File

@ -66,7 +66,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 +91,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"
style={{ columns: section.columns }}
dangerouslySetInnerHTML={{ __html: section.content }}
/>
</section>
);
@ -100,7 +106,7 @@ const Rating = ({ level }: RatingProps) => (
{Array.from({ length: 5 }).map((_, index) => (
<div
key={index}
className={cn("h-2 w-2 rounded-full border border-primary", level > index && "bg-primary")}
className={cn("size-2 rounded-full border border-primary", level > index && "bg-primary")}
/>
))}
</div>
@ -109,28 +115,51 @@ 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"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label || url.label || url.href}
{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 +179,7 @@ const Section = <T,>({
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || !section.items.length) return null;
if (!section.visible || section.items.length === 0) return null;
return (
<section id={section.id} className="grid">
@ -172,11 +201,11 @@ 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 className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
)}
{level !== undefined && level > 0 && <Rating level={level} />}
@ -217,7 +246,7 @@ const Profiles = () => {
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
{!item.icon && <p className="text-sm">{item.network}</p>}
</div>
)}
</Section>
@ -230,9 +259,14 @@ const Experience = () => {
return (
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between">
<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>
@ -252,9 +286,14 @@ const Education = () => {
return (
<Section<Education> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between">
<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>
@ -275,10 +314,14 @@ const Awards = () => {
return (
<Section<Award> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between">
<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">
@ -296,10 +339,10 @@ const Certifications = () => {
return (
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between">
<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">
@ -342,9 +385,14 @@ const Publications = () => {
return (
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between">
<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>
@ -363,9 +411,14 @@ const Volunteer = () => {
return (
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div className="flex items-center justify-between">
<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>
@ -400,9 +453,14 @@ const Projects = () => {
return (
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
{(item) => (
<div className="flex items-center justify-between">
<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 +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>
)}
@ -441,9 +504,14 @@ const Custom = ({ id }: { id: string }) => {
keywordsKey="keywords"
>
{(item) => (
<div className="flex items-center justify-between">
<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>
@ -459,36 +527,50 @@ const Custom = ({ id }: { id: string }) => {
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles":
case "profiles": {
return <Profiles />;
case "summary":
}
case "summary": {
return <Summary />;
case "experience":
}
case "experience": {
return <Experience />;
case "education":
}
case "education": {
return <Education />;
case "awards":
}
case "awards": {
return <Awards />;
case "certifications":
}
case "certifications": {
return <Certifications />;
case "skills":
}
case "skills": {
return <Skills />;
case "interests":
}
case "interests": {
return <Interests />;
case "publications":
}
case "publications": {
return <Publications />;
case "volunteer":
}
case "volunteer": {
return <Volunteer />;
case "languages":
}
case "languages": {
return <Languages />;
case "projects":
}
case "projects": {
return <Projects />;
case "references":
}
case "references": {
return <References />;
default:
}
default: {
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
}
};

View File

@ -4,8 +4,3 @@ export type TemplateProps = {
columns: SectionKey[][];
isFirstPage?: boolean;
};
export type BaseProps = {
children?: React.ReactNode;
className?: string;
};

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

@ -2,7 +2,7 @@
import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
import react from "@vitejs/plugin-react-swc";
import { defineConfig, searchForWorkspaceRoot, splitVendorChunkPlugin } from "vite";
import { defineConfig, searchForWorkspaceRoot } from "vite";
export default defineConfig({
base: "/artboard/",
@ -11,15 +11,16 @@ export default defineConfig({
build: {
sourcemap: true,
emptyOutDir: true,
},
server: {
host: true,
port: +(process.env.__DEV__ARTBOARD_PORT ?? 6173),
port: 6173,
fs: { allow: [searchForWorkspaceRoot(process.cwd())] },
},
plugins: [react(), nxViteTsPaths(), splitVendorChunkPlugin()],
plugins: [react(), nxViteTsPaths()],
resolve: {
alias: {

View File

@ -11,11 +11,27 @@
"settings": {
"tailwindcss": {
"callees": ["cn", "clsx", "cva"],
"config": "tailwind.config.js"
"config": "tailwind.config.js",
"whitelist": ["ph", "ph-"]
}
},
"plugins": ["lingui"],
"rules": {
// eslint
"@typescript-eslint/no-require-imports": "off",
// react
"react/no-unescaped-entities": "off",
"react/jsx-sort-props": [
"error",
{
"reservedFirst": true,
"callbacksLast": true,
"shorthandFirst": true,
"noSortAlphabetically": true
}
],
// react-hooks
"react-hooks/exhaustive-deps": "off",

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="text-sm antialiased bg-background text-foreground print:bg-white print:m-0">
<div id="root"></div>
<!-- Scripts -->
<script type="module" src="/src/main.tsx"></script>
<!-- Phosphor Icons -->
<script src="https://unpkg.com/@phosphor-icons/web"></script>
</body>
</html>

View File

@ -1,10 +1,10 @@
const { join } = require("path");
const path = require("node:path");
module.exports = {
plugins: {
"postcss-import": {},
"tailwindcss/nesting": {},
tailwindcss: { config: join(__dirname, "tailwind.config.js") },
tailwindcss: { config: path.join(__dirname, "tailwind.config.js") },
autoprefixer: {},
},
};

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

@ -54,7 +54,7 @@ export const AiActions = ({ value, onChange, className }: Props) => {
toast({
variant: "error",
title: t`Oops, the server returned an error.`,
description: (error as Error)?.message,
description: (error as Error).message,
});
} finally {
setLoading(false);

View File

@ -27,10 +27,7 @@ export const Copyright = ({ className }: Props) => (
<span>{t`By the community, for the community.`}</span>
<span>
<Trans>
A passion project by{" "}
<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.amruthpillai.com/">
Amruth Pillai
</a>
A passion project by <a href="https://www.amruthpillai.com/">Amruth Pillai</a>
</Trans>
</span>

View File

@ -12,12 +12,14 @@ export const Icon = ({ size = 32, className }: Props) => {
let src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
switch (isDarkMode) {
case false:
case false: {
src = "/icon/dark.svg";
break;
case true:
}
case true: {
src = "/icon/light.svg";
break;
}
}
return (

View File

@ -7,6 +7,7 @@ import {
CommandGroup,
CommandInput,
CommandItem,
CommandList,
Popover,
PopoverContent,
PopoverTrigger,
@ -37,39 +38,43 @@ export const LocaleCombobox = ({ value, onValueChange }: Props) => {
<Command shouldFilter={false}>
<CommandInput
value={search}
onValueChange={setSearch}
placeholder={t`Search for a language`}
onValueChange={setSearch}
/>
<CommandEmpty>{t`No results found`}</CommandEmpty>
<CommandGroup>
<ScrollArea orientation="vertical">
<div className="max-h-60">
{options.map(({ original }) => (
<CommandItem
key={original.locale}
value={original.locale.trim().toLowerCase()}
onSelect={async (selectedValue) => {
const result = options.find(
({ original }) => original.locale.trim().toLowerCase() === selectedValue,
);
<CommandList>
<CommandEmpty>{t`No results found`}</CommandEmpty>
<CommandGroup>
<ScrollArea orientation="vertical">
<div className="max-h-60">
{options.map(({ original }) => (
<CommandItem
key={original.locale}
disabled={false}
value={original.locale.trim()}
onSelect={(selectedValue) => {
const result = options.find(
({ original }) => original.locale.trim() === selectedValue,
);
if (!result) return null;
if (!result) return null;
onValueChange(result.original.locale);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4 opacity-0",
value === original.locale && "opacity-100",
)}
/>
{original.name} <span className="ml-1 text-xs opacity-50">({original.locale})</span>
</CommandItem>
))}
</div>
</ScrollArea>
</CommandGroup>
onValueChange(result.original.locale);
}}
>
<Check
className={cn(
"mr-2 size-4 opacity-0",
value === original.locale && "opacity-100",
)}
/>
{original.name}{" "}
<span className="ml-1 text-xs opacity-50">({original.locale})</span>
</CommandItem>
))}
</div>
</ScrollArea>
</CommandGroup>
</CommandList>
</Command>
);
};
@ -101,7 +106,7 @@ export const LocaleComboboxPopover = ({ value, onValueChange }: Props) => {
</span>
<CaretDown
className={cn(
"ml-2 h-4 w-4 shrink-0 rotate-0 opacity-50 transition-transform",
"ml-2 size-4 shrink-0 rotate-0 opacity-50 transition-transform",
open && "rotate-180",
)}
/>

View File

@ -12,12 +12,14 @@ export const Logo = ({ size = 32, className }: Props) => {
let src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
switch (isDarkMode) {
case false:
case false: {
src = "/logo/light.svg";
break;
case true:
}
case true: {
src = "/logo/dark.svg";
break;
}
}
return (

View File

@ -12,9 +12,18 @@ export const UserAvatar = ({ size = 36, className }: Props) => {
if (!user) return null;
let picture: React.ReactNode = null;
let picture: React.ReactNode;
if (!user.picture) {
if (user.picture) {
picture = (
<img
alt={user.name}
src={user.picture}
className="rounded-full"
style={{ width: size, height: size }}
/>
);
} else {
const initials = getInitials(user.name);
picture = (
@ -25,15 +34,6 @@ export const UserAvatar = ({ size = 36, className }: Props) => {
{initials}
</div>
);
} else {
picture = (
<img
alt={user.name}
src={user.picture}
className="rounded-full"
style={{ width: size, height: size }}
/>
);
}
return <div className={className}>{picture}</div>;

View File

@ -24,7 +24,11 @@ export const UserOptions = ({ children }: Props) => {
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start" className="w-48">
<DropdownMenuItem onClick={() => navigate("/dashboard/settings")}>
<DropdownMenuItem
onClick={() => {
navigate("/dashboard/settings");
}}
>
{t`Settings`}
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
<KeyboardShortcut>S</KeyboardShortcut>

View File

@ -0,0 +1,2 @@
export const DEFAULT_MODEL = "gpt-3.5-turbo";
export const DEFAULT_MAX_TOKENS = 1024;

View File

@ -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",
@ -40,9 +40,9 @@ type Action =
toastId?: ToasterToast["id"];
};
interface State {
type State = {
toasts: ToasterToast[];
}
};
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
@ -64,17 +64,19 @@ const addToRemoveQueue = (toastId: string) => {
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
case "ADD_TOAST": {
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
}
case "UPDATE_TOAST":
case "UPDATE_TOAST": {
return {
...state,
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
};
}
case "DISMISS_TOAST": {
const { toastId } = action;
@ -82,9 +84,9 @@ export const reducer = (state: State, action: Action): State => {
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
for (const toast of state.toasts) {
addToRemoveQueue(toast.id);
});
}
}
return {
@ -99,7 +101,7 @@ export const reducer = (state: State, action: Action): State => {
),
};
}
case "REMOVE_TOAST":
case "REMOVE_TOAST": {
if (action.toastId === undefined) {
return {
...state,
@ -110,18 +112,19 @@ export const reducer = (state: State, action: Action): State => {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
}
};
const listeners: Array<(state: State) => void> = [];
const listeners: ((state: State) => void)[] = [];
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
for (const listener of listeners) {
listener(memoryState);
});
}
}
type Toast = Omit<ToasterToast, "id">;
@ -129,12 +132,15 @@ type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = createId();
const update = (props: ToasterToast) =>
const update = (props: ToasterToast) => {
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
};
const dismiss = () => {
dispatch({ type: "DISMISS_TOAST", toastId: id });
};
dispatch({
type: "ADD_TOAST",
@ -170,7 +176,9 @@ function useToast() {
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
dismiss: (toastId?: string) => {
dispatch({ type: "DISMISS_TOAST", toastId });
},
};
}

View File

@ -4,18 +4,13 @@ import _axios from "axios";
import createAuthRefreshInterceptor from "axios-auth-refresh";
import { redirect } from "react-router-dom";
import { refreshToken } from "@/client/services/auth";
import { USER_KEY } from "../constants/query-keys";
import { toast } from "../hooks/use-toast";
import { refresh } from "../services/auth/refresh";
import { translateError } from "../services/errors/translate-error";
import { queryClient } from "./query-client";
export type ServerError = {
statusCode: number;
message: string;
error: string;
};
export const axios = _axios.create({ baseURL: "/api", withCredentials: true });
// Intercept responses to transform ISO dates to JS date objects
@ -36,7 +31,7 @@ axios.interceptors.response.use(
});
}
return Promise.reject(error);
return Promise.reject(new Error(message));
},
);
@ -45,26 +40,12 @@ axios.interceptors.response.use(
const axiosForRefresh = _axios.create({ baseURL: "/api", withCredentials: true });
// Interceptor to handle expired access token errors
const handleAuthError = async () => {
try {
await refresh(axiosForRefresh);
return Promise.resolve();
} catch (error) {
return Promise.reject(error);
}
};
const handleAuthError = () => refreshToken(axiosForRefresh);
// Interceptor to handle expired refresh token errors
const handleRefreshError = async () => {
try {
queryClient.invalidateQueries({ queryKey: USER_KEY });
redirect("/auth/login");
return Promise.resolve();
} catch (error) {
return Promise.reject(error);
}
await queryClient.invalidateQueries({ queryKey: USER_KEY });
redirect("/auth/login");
};
// Intercept responses to check for 401 and 403 errors, refresh token and retry the request

View File

@ -13,10 +13,12 @@ export async function dynamicActivate(locale: string) {
i18n.loadAndActivate({ locale, messages });
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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