Compare commits

...

168 Commits

Author SHA1 Message Date
d342c0a9af fix axios type issue 2023-01-03 17:17:35 +01:00
63084eebb4 feat(dependencies): ⬆️ update dependencies, fix date display issue, add more profile icons 2023-01-03 17:06:30 +01:00
3b4ea00db8 Merge pull request #1162 from AmruthPillai/dependabot/gradle/app/org.jetbrains.kotlin.android-1.8.0
Bump org.jetbrains.kotlin.android from 1.7.22 to 1.8.0 in /app
2023-01-03 16:51:17 +01:00
c8f7bffe7e Merge pull request #1160 from coolswood/main
fix: Gengar two cols bug
2023-01-03 16:51:09 +01:00
3ff56f89d9 Merge pull request #1158 from AmruthPillai/i18n_main
New Crowdin updates
2023-01-03 16:50:48 +01:00
7fb9f27837 Bump org.jetbrains.kotlin.android from 1.7.22 to 1.8.0 in /app
Bumps [org.jetbrains.kotlin.android](https://github.com/JetBrains/kotlin) from 1.7.22 to 1.8.0.
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v1.7.22...v1.8.0)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlin.android
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-02 02:04:34 +00:00
c9685d4ce7 fix: Gengar two cols bug 2022-12-31 10:01:54 +02:00
4dc987e27d New translations landing.json (Persian) 2022-12-30 19:09:41 +01:00
f7af06ae9a New translations builder.json (Persian) 2022-12-30 18:12:15 +01:00
a5c337faa3 Merge pull request #1151 from AmruthPillai/dependabot/github_actions/digitalocean/action-doctl-2.3.0
Bump digitalocean/action-doctl from 2.2.0 to 2.3.0
2022-12-30 11:48:34 +01:00
fc4704f0a6 Merge pull request #1150 from AmruthPillai/dependabot/docker/server/playwright-v1.29.1-focal
Bump playwright from v1.29.0-focal to v1.29.1-focal in /server
2022-12-30 11:48:25 +01:00
d968334ada Merge pull request #1155 from coolswood/main
fix: overflow-y-auto
2022-12-29 21:37:29 +01:00
fea6d23178 fix: overflow-y-auto 2022-12-29 19:37:47 +02:00
3fefc95572 Bump digitalocean/action-doctl from 2.2.0 to 2.3.0
Bumps [digitalocean/action-doctl](https://github.com/digitalocean/action-doctl) from 2.2.0 to 2.3.0.
- [Release notes](https://github.com/digitalocean/action-doctl/releases)
- [Commits](https://github.com/digitalocean/action-doctl/compare/v2.2.0...v2.3.0)

---
updated-dependencies:
- dependency-name: digitalocean/action-doctl
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-26 02:04:49 +00:00
b07e7d1213 Bump playwright from v1.29.0-focal to v1.29.1-focal in /server
Bumps playwright from v1.29.0-focal to v1.29.1-focal.

---
updated-dependencies:
- dependency-name: playwright
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-26 02:04:31 +00:00
d47b8bfb03 Merge pull request #1139 from AmruthPillai/dependabot/github_actions/actions/checkout-3.2.0
Bump actions/checkout from 3.1.0 to 3.2.0
2022-12-19 10:22:44 +01:00
5bf7fbdae1 Merge pull request #1140 from AmruthPillai/dependabot/docker/server/playwright-v1.29.0-focal
Bump playwright from v1.28.1-focal to v1.29.0-focal in /server
2022-12-19 10:22:34 +01:00
fca766b382 Bump playwright from v1.28.1-focal to v1.29.0-focal in /server
Bumps playwright from v1.28.1-focal to v1.29.0-focal.

---
updated-dependencies:
- dependency-name: playwright
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-19 02:06:27 +00:00
feadfb1b67 Bump actions/checkout from 3.1.0 to 3.2.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.1.0 to 3.2.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.1.0...v3.2.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-19 02:06:19 +00:00
e69000f221 Merge pull request #1137 from AmruthPillai/feat/add-remark-gfm-math-plugins
feat(client):  add github flavored syntax and math equations to markdown support
2022-12-17 10:27:19 +01:00
6b4a54465a feat(client): add github flavored syntax and math equations to markdown support 2022-12-17 10:19:25 +01:00
878659999f Merge pull request #1135 from AmruthPillai/i18n_main
New Crowdin updates
2022-12-17 09:53:13 +01:00
1868c47e30 New translations common.json (Dutch) 2022-12-17 09:48:12 +01:00
51442efc23 New translations common.json (German) 2022-12-17 09:48:07 +01:00
556e962ec5 refactor(client): 📝 add link to subreddit 2022-12-16 22:56:23 +01:00
b5ce67f863 Update README.md 2022-12-16 18:06:46 +01:00
c3ce89dc3a Update README.md 2022-12-16 18:04:22 +01:00
e87930c758 bump version to 3.6.16 2022-12-16 17:59:58 +01:00
815a693e58 Merge pull request #1134 from AmruthPillai/feature/configure-gitpod
Configure Gitpod for easier development and self-hosting
2022-12-16 17:58:50 +01:00
8287fcae96 add gitpod to README.md 2022-12-16 17:58:38 +01:00
cd7fe6c404 Remove .env.gitpod 2022-12-16 15:58:22 +00:00
d47d5dd819 build and run project finally, in gitpod 2022-12-16 16:52:37 +01:00
1919d79e43 fix url host parsing 2022-12-16 15:50:43 +00:00
ab08cd9e34 add sync await/done 2022-12-16 16:46:26 +01:00
2522bdd0a2 add onOpen: ignore to postgres port 2022-12-16 16:42:02 +01:00
f9b6aefffe create .env generation script 2022-12-16 16:39:29 +01:00
2ba6658a0b add CORS to server, input appUrl 2022-12-16 15:04:08 +00:00
dbc46f27a3 add postgres port 2022-12-16 14:12:43 +00:00
f21e1caed1 trying this now 2022-12-16 15:09:43 +01:00
4ffe2a6330 add 2022-12-16 15:03:22 +01:00
1bc0438872 remove 2022-12-16 15:03:06 +01:00
57fb9fdaea add dockerfile support to gitpod.yml 2022-12-16 14:01:17 +00:00
58ce641f18 add postgres to gitpod 2022-12-16 13:56:08 +00:00
5f4e7802e4 Merge branch 'main' into feature/configure-gitpod 2022-12-16 13:44:46 +00:00
42d3109ae1 Merge pull request #1128 from AmruthPillai/dependabot/gradle/app/org.jetbrains.kotlin.android-1.7.22
Bump org.jetbrains.kotlin.android from 1.7.21 to 1.7.22 in /app
2022-12-16 14:43:57 +01:00
f7ca7b97fa Merge pull request #1127 from AmruthPillai/dependabot/docker/server/playwright-v1.28.1-focal
Bump playwright from v1.28.0-focal to v1.28.1-focal in /server
2022-12-16 14:43:51 +01:00
f5d8a54134 Merge pull request #1130 from Jack-Kingdom/main
Fix: wait for networkidle event to ensure image load
2022-12-16 14:43:43 +01:00
eaec14dc62 initial gitpod configuration 2022-12-16 13:42:03 +00:00
c93b3264cd Bump playwright from v1.28.0-focal to v1.28.1-focal in /server
Bumps playwright from v1.28.0-focal to v1.28.1-focal.

---
updated-dependencies:
- dependency-name: playwright
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-16 13:34:56 +00:00
bf41aa9c6c feat(sentry): remove sentry integration 2022-12-16 14:34:15 +01:00
8af6bfd5ae Fix: wait for networkidle event to ensure image load 2022-12-10 02:55:38 +08:00
ab08c10874 Bump org.jetbrains.kotlin.android from 1.7.21 to 1.7.22 in /app
Bumps [org.jetbrains.kotlin.android](https://github.com/JetBrains/kotlin) from 1.7.21 to 1.7.22.
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v1.7.21...v1.7.22)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlin.android
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-05 02:08:23 +00:00
9af9a0284e set sentry user ID, if available and applicable 2022-12-03 00:15:08 +01:00
716a05032d one more attempt to fix sentry auth issue 2022-12-02 23:54:25 +01:00
43e43e7d76 one more attempt to fix sentry auth issue 2022-12-02 23:44:51 +01:00
c91af3668d attempt to fix sentry auth issue 2022-12-02 23:32:04 +01:00
52f41f0b3b bump version to v3.6.15 2022-12-02 23:22:54 +01:00
3b709d606b Merge pull request #1120 from AmruthPillai/feature/sentry-integration
[Feature] Sentry Integration (Optional for Self Hosted)
2022-12-02 23:21:59 +01:00
2e5fafac62 remove unnecessary envs 2022-12-02 23:21:00 +01:00
ea2aee2d25 add release version to sentry ci process 2022-12-02 23:16:48 +01:00
e36fbb5f64 Update .env.example 2022-12-02 22:13:07 +01:00
5221ef707b Merge branch 'main' into feature/sentry-integration 2022-12-02 22:10:58 +01:00
f0df806f01 remove link underline style 2022-12-02 22:10:08 +01:00
9d01d6a833 update version ot 3.6.14 2022-12-02 13:59:13 +01:00
1914ebb9ae fix links in PDF 2022-12-02 13:55:56 +01:00
686dba90c9 Merge pull request #1114 from tryallthethings/pdf-template-fix
Fix for links in PDFs, template fix
2022-12-02 13:41:37 +01:00
95dc3bf571 Merge pull request #1108 from tryallthethings/translation-fix
Translation fix
2022-12-02 13:37:35 +01:00
1c8fdbf848 Merge pull request #1107 from tryallthethings/main
fix: made some missing texts translatable
2022-12-02 13:37:25 +01:00
d8357c9959 Fix: Every other instance of invoked clsx hence adding it here as well. 2022-11-26 14:37:48 +01:00
90e994377b Fix: Adding quotation marks seems to fix #1112 2022-11-26 14:36:56 +01:00
82c6ee6d5d fix: Updated German translation. A lot of minor changes as well as rephrasing of whole sentences. This translation is now also 100% formal, instead of a mix of formal and informal. 2022-11-25 18:42:37 +01:00
7b615e73c3 fix: Changed German language description to formal 2022-11-25 18:42:00 +01:00
268e4a87fe Revert "fix: made some missing texts translatable"
This reverts commit deb4e0a0de.
2022-11-25 18:39:09 +01:00
73f8eb84c9 Revert "fix: Updated German translation. A lot of minor changes as well as rephrasing of whole sentences."
This reverts commit e0a42fd928.
2022-11-25 18:39:07 +01:00
a31ef89996 Revert "fix: Changed German language description to formal"
This reverts commit d6bca7ebab.
2022-11-25 18:38:55 +01:00
d6bca7ebab fix: Changed German language description to formal 2022-11-25 18:29:55 +01:00
e0a42fd928 fix: Updated German translation. A lot of minor changes as well as rephrasing of whole sentences. 2022-11-25 18:27:28 +01:00
deb4e0a0de fix: made some missing texts translatable
BREAKING CHANGE: locales without the new fields will display the field name
2022-11-25 18:22:29 +01:00
a687062866 Merge pull request #1105 from tryallthethings/patch-3
Update date.ts
2022-11-25 18:10:11 +01:00
700439c8a8 Update date.ts
Added a common date format for Germany.
2022-11-25 18:00:08 +01:00
fb09283e53 add some more properties for sentry 2022-11-25 13:07:40 +01:00
88ac365e03 use v1.28.0-focal instead of next-jammy 2022-11-25 11:52:14 +01:00
aec78cf875 lay the ground work for sentry integration 2022-11-25 11:32:57 +01:00
77c587681b using fetch instead of axios, should fix the issue 2022-11-24 22:25:23 +01:00
7ac8b906d9 add await 2022-11-24 22:16:00 +01:00
e9a5f86a6a using fetch instead of axios, server side 2022-11-24 22:14:01 +01:00
7238a3b50e some more logs 2022-11-24 22:01:00 +01:00
ebe13fa82e push a bunch of console.logs 2022-11-24 21:45:42 +01:00
6ee290a625 add logs to check what's wrong 2022-11-24 21:31:43 +01:00
69f2b7070f remove arm64 support for the time being, because of upstream support 2022-11-24 21:09:32 +01:00
11bea1c7c4 updating version to v3.6.13 2022-11-24 20:35:28 +01:00
68a1dc65c1 update pnpm-lock.yaml 2022-11-24 17:04:11 +01:00
4b1ce539d5 remove sentry integration 2022-11-24 16:58:36 +01:00
a6fbb8191d Update docker-build-push.yml 2022-11-24 16:32:22 +01:00
552ff281b8 I have no idea what I'm doing here. 2022-11-24 16:29:36 +01:00
54fad2f6d8 update docker-build-push.yml 2022-11-24 16:20:40 +01:00
78edcd7d0e fix typo in github workflow 2022-11-24 16:02:39 +01:00
a8034b21d5 attempting to fix github actions 2022-11-24 15:58:27 +01:00
f0e95905d2 trying out env instead of secrets 2022-11-24 15:44:28 +01:00
69a5276614 attempt to fix locale issue 2022-11-24 15:40:49 +01:00
2e62eea351 fix sentry issue: 28c5a41aea3c4435902046e56c435e56 2022-11-24 15:38:21 +01:00
13d972b8f3 update docker-build-push.yaml 2022-11-24 15:15:35 +01:00
03cb198e95 move from env to secrets 2022-11-24 15:11:55 +01:00
67ee55b502 fix env for sentry auth token 2022-11-24 15:01:33 +01:00
b5998d7f3a pass sentry token to docker build push step 2022-11-24 14:58:26 +01:00
f71cf99b77 remove .git from .dockerignore 2022-11-24 13:27:43 +01:00
a2092a6a39 revert version back to 3.6.12 2022-11-24 12:58:15 +01:00
43c09666a0 add sentry CLI to github actions 2022-11-24 11:42:03 +01:00
0da23f95fd Merge pull request #1101 from stonespheres/patch-1
Fix link typo
2022-11-24 11:25:17 +01:00
e8f44e2142 update pnpm-lock.yaml 2022-11-24 11:23:37 +01:00
fbb237e982 Fix link typo
Bad practice on my part for last commit - did not check before push.
Link under table of contents fixed and now directs to the documentation at https://docs.rxresu.me
2022-11-24 18:22:57 +08:00
7f7c1d7b87 update version to 3.6.13 2022-11-24 11:21:45 +01:00
be0b7f20f9 integrate sentry for error logging 2022-11-24 11:21:30 +01:00
0672988fff Merge pull request #1100 from stonespheres/patch-1
Fixed formatting and typos on README.md
2022-11-24 11:01:38 +01:00
75dad60cb5 Fixed formatting and typos on README.md
- Under Table of Contents: Fixed the formatting error for the link to the docs.
- Under Languages: fixed typo Ukranian -> Ukrainian
- Under Building from Source: ...head over to the doc's -> head over to the docs
2022-11-24 17:47:29 +08:00
0140e3fce0 update version to 3.6.12 2022-11-23 15:20:34 +01:00
42d0e14b98 fix styling issues and theme cascades across all templates 2022-11-23 15:20:04 +01:00
9a42d684fb add branching deploy condition 2022-11-23 14:15:36 +01:00
ab6ad65445 update github actions to a more streamlined workflow using gh matrix 2022-11-23 14:10:14 +01:00
b613764ccc fix matrix variable name 2022-11-23 13:59:25 +01:00
ac44d0489f change name of test action so as to not trigger further actions 2022-11-23 13:56:59 +01:00
c57e6fbbb8 fix versioning of github action package 2022-11-23 13:56:22 +01:00
6c6da215c8 add on: [workflow_dispatch] to test github action 2022-11-23 13:55:15 +01:00
be700c7629 Testing a new streamlined GitHub Actions workflow 2022-11-23 13:53:41 +01:00
b697f73492 fix #1096 2022-11-23 13:11:29 +01:00
3106f94989 - update version to v.3.6.11
- update dependencies to latest versions
2022-11-23 13:04:59 +01:00
50f41f73d5 Add detailed description to page title, to increase SEO 2022-11-23 12:57:07 +01:00
83e3f59e68 fix #1082 2022-11-23 12:47:29 +01:00
056c61e985 resolves #1061, resolves #1027, resolves #1007, resolves #1001, resolves #987, resolves #890, resolves #882, resolves #837 2022-11-23 12:24:17 +01:00
d1a1b68302 fix #1095: make PDF_DELETION_TIME optional, add default value 2022-11-23 11:51:28 +01:00
6bd7b9a50f Merge pull request #1092 from GETandSELECT/main
Update common.json - tiny translation error to German
2022-11-23 11:41:54 +01:00
e6967aab88 Update common.json 2022-11-22 10:03:14 +00:00
47e96803e3 fix password recovery link 2022-11-19 09:37:23 +01:00
f9ef4d0a64 fix max width of description 2022-11-18 09:42:59 +01:00
c4b4e6013f Merge pull request #1073 from AmruthPillai/dependabot/gradle/app/org.jetbrains.kotlin.android-1.7.21
Bump org.jetbrains.kotlin.android from 1.7.20 to 1.7.21 in /app
2022-11-15 11:36:04 +01:00
24bbc46c32 Merge pull request #1075 from RobbeVanslambrouck/main
fix typos in English and Dutch translation
2022-11-15 11:35:54 +01:00
85bc9ef124 fix typos in English and Dutch translation 2022-11-14 17:29:09 +01:00
33755a8573 remove console.log 2022-11-14 10:06:19 +01:00
ab45321889 fixes #1074 2022-11-14 10:05:51 +01:00
940b310f64 Bump org.jetbrains.kotlin.android from 1.7.20 to 1.7.21 in /app
Bumps [org.jetbrains.kotlin.android](https://github.com/JetBrains/kotlin) from 1.7.20 to 1.7.21.
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v1.7.20...v1.7.21)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlin.android
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-14 02:06:07 +00:00
8026241b6c release: v3.6.9 2022-11-13 14:28:47 +01:00
89b35392bd Merge pull request #1060 from sashokbg/feat/multiple_work_sections
feat: additional work sections
2022-11-13 10:28:28 +01:00
62eb239ec4 Merge pull request #1023 from klejejs/main
Add PDF file caching
2022-11-13 10:28:19 +01:00
7fdf8c1f0c Merge pull request #1069 from arvaid/main
fixed grammatical and stylistic errors in Hungarian translation
2022-11-07 09:28:00 +01:00
538697238a fixed grammatical and stylistic errors in Hungarian translation 2022-11-06 14:46:55 +01:00
7bc4a998fe feat: additional work sections 2022-11-03 17:59:15 +01:00
e33df485ab Merge pull request #951 from Leopere/patch-3
Cleanup superfluous docker-compose.yml declarations
2022-11-02 23:36:21 +01:00
36ae54fe17 Merge branch 'main' into patch-3 2022-11-02 23:36:13 +01:00
50958fd6df Merge pull request #1058 from klejejs/fix/zip_file_upload_crash
Fix server crash when non-zip file is uploaded
2022-10-26 07:57:49 +02:00
e9e595f0d0 Fix server crash when non-zip file is uploaded 2022-10-25 22:03:18 +03:00
43ddfba777 Add scheduled deletion for cached PDF files 2022-10-25 21:10:40 +03:00
78a32961d7 Add PDF file caching 2022-10-25 20:16:39 +03:00
9b1f3eda05 Merge pull request #1053 from AmruthPillai/i18n_main
New Crowdin updates
2022-10-24 07:58:36 +02:00
1154621e5c Merge pull request #1057 from AmruthPillai/dependabot/github_actions/docker/setup-buildx-action-2.2.1
Bump docker/setup-buildx-action from 2.1.0 to 2.2.1
2022-10-24 07:58:25 +02:00
e7aeee77a7 Merge pull request #1056 from AmruthPillai/dependabot/github_actions/digitalocean/action-doctl-2.2.0
Bump digitalocean/action-doctl from 2.1.1 to 2.2.0
2022-10-24 07:58:16 +02:00
fab3988a36 Bump docker/setup-buildx-action from 2.1.0 to 2.2.1
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.1.0 to 2.2.1.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.1.0...v2.2.1)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-24 02:53:50 +00:00
354cad88d3 Bump digitalocean/action-doctl from 2.1.1 to 2.2.0
Bumps [digitalocean/action-doctl](https://github.com/digitalocean/action-doctl) from 2.1.1 to 2.2.0.
- [Release notes](https://github.com/digitalocean/action-doctl/releases)
- [Commits](https://github.com/digitalocean/action-doctl/compare/v2.1.1...v2.2.0)

---
updated-dependencies:
- dependency-name: digitalocean/action-doctl
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-24 02:53:45 +00:00
876f930f30 New translations builder.json (Indonesian) 2022-10-21 05:55:29 +02:00
5b3ea46f0f Merge pull request #1045 from AmruthPillai/dependabot/github_actions/docker/setup-buildx-action-2.1.0
Bump docker/setup-buildx-action from 2.0.0 to 2.1.0
2022-10-17 10:39:55 +02:00
37a2563c11 Merge pull request #1047 from AmruthPillai/dependabot/github_actions/docker/build-push-action-3.2.0
Bump docker/build-push-action from 3.1.1 to 3.2.0
2022-10-17 10:39:42 +02:00
cb977a146b Merge pull request #1046 from AmruthPillai/dependabot/github_actions/docker/login-action-2.1.0
Bump docker/login-action from 2.0.0 to 2.1.0
2022-10-17 10:39:31 +02:00
72b2551b6d Merge pull request #1044 from AmruthPillai/dependabot/github_actions/docker/setup-qemu-action-2.1.0
Bump docker/setup-qemu-action from 2.0.0 to 2.1.0
2022-10-17 10:39:22 +02:00
c94633e616 Bump docker/build-push-action from 3.1.1 to 3.2.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3.1.1 to 3.2.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3.1.1...v3.2.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-17 02:21:39 +00:00
7fee2d670f Bump docker/login-action from 2.0.0 to 2.1.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2.0.0...v2.1.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-17 02:21:36 +00:00
837b06eb38 Bump docker/setup-buildx-action from 2.0.0 to 2.1.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.0.0...v2.1.0)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-17 02:21:33 +00:00
2b8860b21c Bump docker/setup-qemu-action from 2.0.0 to 2.1.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2.0.0...v2.1.0)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-17 02:21:31 +00:00
3a7b98d30e Fix issue with variable accessor 2022-10-15 23:47:16 +02:00
fc0b69796f Cleanup superfluous docker-compose.yml declarations 2022-07-25 10:15:03 -04:00
128 changed files with 5162 additions and 3033 deletions

View File

@ -2,9 +2,9 @@
/app
# Build Artifacts
dist
.next
.turbo
**/.turbo
/server/dist
/client/.next
# IDEs
.vscode
@ -19,7 +19,7 @@ CHANGELOG.md
CODE_OF_CONDUCT.md
# Project Dependencies
node_modules
**/node_modules
# Docker
Dockerfile

View File

@ -1,7 +1,3 @@
# Turbo Cache (Optional)
TURBO_TEAM=
TURBO_TOKEN=
# Server + Client
TZ=UTC
PUBLIC_URL=http://localhost:3000
@ -15,7 +11,7 @@ POSTGRES_PASSWORD=postgres
# Server
SECRET_KEY=
POSTGRES_HOST=postgres
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_SSL_CERT=
JWT_SECRET=
@ -34,6 +30,7 @@ STORAGE_ENDPOINT=
STORAGE_URL_PREFIX=
STORAGE_ACCESS_KEY=
STORAGE_SECRET_KEY=
PDF_DELETION_TIME=345600000
# Flags (Client)
PUBLIC_FLAG_DISABLE_SIGNUPS=false
# Client
PUBLIC_FLAG_DISABLE_SIGNUPS=false

View File

@ -14,6 +14,7 @@
// TypeScript ESLint
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off"

View File

@ -8,14 +8,21 @@ on:
- completed
jobs:
deploy:
on-success:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Install DigitalOcean CLI
uses: digitalocean/action-doctl@v2.1.1
uses: digitalocean/action-doctl@v2.3.0
with:
token: ${{ secrets.DIGITALOCEAN_TOKEN }}
- name: Create Deployment with Latest Version
run: doctl apps create-deployment ${{ secrets.DIGITALOCEAN_APP_ID }} --wait --force-rebuild
on-failure:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
steps:
- name: Abruptly end the worklfow
run: exit 1

View File

@ -1,112 +1,58 @@
name: Build and Push Docker Image
on:
workflow_dispatch:
release:
types: [published]
jobs:
client:
name: Client
build_matrix:
name: Build and Push Docker Image
runs-on: ubuntu-latest
env:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
strategy:
matrix:
image: [client, server]
steps:
- name: Checkout the repository
uses: actions/checkout@v3.1.0
with:
fetch-depth: 2
uses: actions/checkout@v3.2.0
- id: version
name: Get Version
run: echo "version=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
name: App Version
uses: martinbeentjes/npm-get-version-action@v1.2.3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
uses: docker/setup-qemu-action@v2.1.0
with:
platforms: amd64
- id: buildx
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.0.0
with:
install: true
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
uses: docker/login-action@v2.0.0
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2.0.0
uses: docker/login-action@v2.1.0
with:
registry: ghcr.io
username: $GITHUB_REPOSITORY_OWNER
password: ${{ secrets.GH_TOKEN }}
- name: Build and Push Client Image
uses: docker/build-push-action@v3.1.1
- name: Build and Push Docker Image
uses: docker/build-push-action@v3.2.0
with:
context: .
push: true
file: client/Dockerfile
platforms: linux/amd64,linux/arm64
platforms: linux/amd64
file: ${{ matrix.image }}/Dockerfile
tags: |
amruthpillai/reactive-resume:client-latest
amruthpillai/reactive-resume:client-${{ env.version }}
ghcr.io/amruthpillai/reactive-resume:client-latest
ghcr.io/amruthpillai/reactive-resume:client-${{ env.version }}
server:
name: Server
runs-on: ubuntu-latest
env:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
steps:
- name: Checkout the repository
uses: actions/checkout@v3.1.0
with:
fetch-depth: 2
- id: version
name: Get Version
run: echo "version=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- id: buildx
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.0.0
with:
install: true
- name: Login to Docker Hub
uses: docker/login-action@v2.0.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2.0.0
with:
registry: ghcr.io
username: $GITHUB_REPOSITORY_OWNER
password: ${{ secrets.GH_TOKEN }}
- name: Build and Push Server Image
uses: docker/build-push-action@v3.1.1
with:
context: .
push: true
file: server/Dockerfile
platforms: linux/amd64,linux/arm64
tags: |
amruthpillai/reactive-resume:server-latest
amruthpillai/reactive-resume:server-${{ env.version }}
ghcr.io/amruthpillai/reactive-resume:server-latest
ghcr.io/amruthpillai/reactive-resume:server-${{ env.version }}
amruthpillai/reactive-resume:${{ matrix.image }}-latest
amruthpillai/reactive-resume:${{ matrix.image }}-${{ steps.version.outputs.current-version }}
ghcr.io/amruthpillai/reactive-resume:${{ matrix.image }}-latest
ghcr.io/amruthpillai/reactive-resume:${{ matrix.image }}-${{ steps.version.outputs.current-version }}

6
.gitignore vendored
View File

@ -1,6 +1,7 @@
# Environment Variables
.env
.env.*
!.env.gitpod
!.env.example
# Project Dependencies
@ -10,4 +11,7 @@ node_modules
.DS_Store
# Turbo
.turbo
.turbo
# Intellij
.idea

41
.gitpod.yml Normal file
View File

@ -0,0 +1,41 @@
tasks:
- name: Run PostgreSQL Database
command: docker run --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -d postgres
- name: Install Project Dependencies
command: |
pnpm install
pnpm dlx playwright install --with-deps chromium
gp sync-done deps
- name: Generate Environment Variables
init: gp sync-await deps
command: |
if [ -f .env ]; then
echo "Found .env in workspace, skipping generation"
else
pnpm generate-env
fi
gp sync-done env
- name: Build and Run Project
init: gp sync-await env
command: |
pnpm build
pnpm start
ports:
# PostgreSQL
- port: 5432
onOpen: ignore
visibility: private
# Client
- port: 3100
onOpen: ignore
visibility: public
# Client
- port: 3000
onOpen: open-browser
visibility: public

View File

@ -6,7 +6,8 @@
[![Project License](https://img.shields.io/github/license/AmruthPillai/Reactive-Resume?style=flat-square)](https://github.com/AmruthPillai/Reactive-Resume/blob/main/LICENSE)
[![Crowdin](https://badges.crowdin.net/reactive-resume/localized.svg)](https://translate.rxresu.me)
[![Docker Pulls](https://img.shields.io/docker/pulls/amruthpillai/reactive-resume?style=flat-square)](https://hub.docker.com/r/amruthpillai/reactive-resume)
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/AmruthPillai/Reactive-Resume/Build%20and%20Push%20Docker%20Image?label=docker%20build&style=flat-square)
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/AmruthPillai/Reactive-Resume/docker-build-push.yml?branch=main&label=docker%20build&style=flat-square)
[![Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/AmruthPillai/Reactive-Resume)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FAmruthPillai%2FReactive-Resume.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FAmruthPillai%2FReactive-Resume?ref=badge_shield)
## [Go to App](https://rxresu.me) | [Docs](https://docs.rxresu.me)
@ -18,7 +19,7 @@ You have complete control over what goes into your resume, how it looks, what co
## Table of Contents
- [Reactive Resume](#reactive-resume)
- [Go to App | [Docs](https://docs.rxresu.me)](#go-to-app--docs)
- [Go to App | Docs](#go-to-app--docs)
- [Table of Contents](#table-of-contents)
- [Features](#features)
- [Languages](#languages)
@ -93,7 +94,7 @@ You have complete control over what goes into your resume, how it looks, what co
- Swedish (Svenska)
- Tamil (தமிழ்)
- Turkish (Türkçe)
- Ukranian (Українська мова)
- Ukrainian (Українська мова)
- Vietnamese (Tiếng Việt)
Help by [translating Reactive Resume](https://translate.rxresu.me) to your language!
@ -104,7 +105,11 @@ The docs include an extensive [Tutorial](https://docs.rxresu.me/tutorial) sectio
## Build from Source
For extensive information on how to build the app on your local machine, head over to the docs's [Source Code](https://docs.rxresu.me/source-code) section.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/AmruthPillai/Reactive-Resume)
Initially building the image and project on Gitpod will take at least ~10 minutes, so please be patient on first launch.
For extensive information on how to build the app on your local machine, head over to the docs [Source Code](https://docs.rxresu.me/source-code) section.
## Contributing

View File

@ -1,7 +1,7 @@
plugins {
id 'com.android.application' version '7.1.2' apply false
id 'com.android.library' version '7.1.2' apply false
id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
}
task clean(type: Delete) {

2
client/.gitignore vendored
View File

@ -39,4 +39,4 @@ yarn-error.log*
__ENV.js
# next-sitemap
sitemap*.xml
sitemap*.xml

View File

@ -21,12 +21,6 @@ COPY --from=dependencies /app/node_modules ./node_modules
COPY --from=dependencies /app/schema/node_modules ./schema/node_modules
COPY --from=dependencies /app/client/node_modules ./client/node_modules
ARG TURBO_TEAM
ARG TURBO_TOKEN
ENV TURBO_TEAM $TURBO_TEAM
ENV TURBO_TOKEN $TURBO_TOKEN
RUN pnpm run build --filter client
FROM base as production
@ -46,6 +40,6 @@ EXPOSE 3000
ENV PORT 3000
HEALTHCHECK --interval=30s --timeout=20s --retries=3 --start-period=15s \
CMD curl -fSs 127.0.0.1:3000 || exit 1
CMD curl -fSs localhost:3000 || exit 1
CMD [ "pnpm", "run", "start", "--filter", "client" ]

View File

@ -13,6 +13,7 @@ import {
} from '@mui/icons-material';
import { ButtonBase, Divider, Tooltip, useMediaQuery, useTheme } from '@mui/material';
import clsx from 'clsx';
import dayjs from 'dayjs';
import { get } from 'lodash';
import { useTranslation } from 'next-i18next';
import toast from 'react-hot-toast';
@ -67,8 +68,9 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
const slug = get(resume, 'slug');
const username = get(resume, 'user.username');
const updatedAt = get(resume, 'updatedAt');
const url = await mutateAsync({ username, slug });
const url = await mutateAsync({ username, slug, lastUpdated: dayjs(updatedAt).unix().toString() });
download(url);
};

View File

@ -5,7 +5,7 @@
}
.wrapper {
@apply h-full w-full #{!important};
@apply h-full w-full overflow-visible #{!important};
}
.artboard {

View File

@ -36,13 +36,6 @@
top: calc(279mm - 19px);
}
}
.markdown {
ul {
padding-left: 1.5em;
text-indent: -1.5em;
}
}
}
.pageNumber {

View File

@ -26,24 +26,24 @@ const Page: React.FC<Props> = ({ page, showPageNumbers = false }) => {
const theme: ThemeConfig = get(resume, 'metadata.theme');
const customCSS: CustomCSS = get(resume, 'metadata.css');
const template: string = get(resume, 'metadata.template');
const pageConfig: PageConfig = get(resume, 'metadata.page');
const typography: Typography = get(resume, 'metadata.typography');
const pageConfig: PageConfig = get(resume, 'metadata.page', {} as PageConfig);
const themeCSS = useMemo(() => !isEmpty(theme) && generateThemeStyles(theme), [theme]);
const typographyCSS = useMemo(() => !isEmpty(typography) && generateTypographyStyles(typography), [typography]);
const TemplatePage: React.FC<PageProps> | null = useMemo(() => templateMap[template].component, [template]);
return (
<div data-page={page + 1} data-format={pageConfig.format || 'A4'} className={styles.container}>
<div className={styles.container} data-page={page + 1} data-format={pageConfig?.format || 'A4'}>
<div
className={clsx({
reset: true,
[styles.page]: true,
[styles.break]: breakLine,
[styles['format-letter']]: pageConfig?.format === 'Letter',
[css(themeCSS)]: true,
[css(typographyCSS)]: true,
[css(customCSS.value)]: customCSS.visible,
[styles['format-letter']]: pageConfig?.format === 'Letter',
})}
>
{TemplatePage && <TemplatePage page={page} />}

View File

@ -1,14 +1,15 @@
import { Add, Star } from '@mui/icons-material';
import { Button, Divider, IconButton, SwipeableDrawer, Tooltip, useMediaQuery, useTheme } from '@mui/material';
import { Section as SectionRecord } from '@reactive-resume/schema';
import cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get';
import Link from 'next/link';
import { useTranslation } from 'next-i18next';
import { useMemo } from 'react';
import React, { ReactComponentElement, useMemo } from 'react';
import { validate } from 'uuid';
import Logo from '@/components/shared/Logo';
import { getCustomSections, left } from '@/config/sections';
import { getCustomSections, getSectionsByType, left } from '@/config/sections';
import { setSidebarState } from '@/store/build/buildSlice';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { addSection } from '@/store/resume/resumeSlice';
@ -52,7 +53,49 @@ const LeftSidebar = () => {
items: [],
};
dispatch(addSection({ value: newSection }));
dispatch(addSection({ value: newSection, type: 'custom' }));
};
const sectionsList = () => {
const sectionsComponents: Array<ReactComponentElement<any>> = [];
for (const item of left) {
const id = (item as any).id;
const component = (item as any).component;
const type = component.props.type;
const addMore = !!component.props.addMore;
sectionsComponents.push(
<section key={id} id={id}>
{component}
</section>
);
if (addMore) {
const additionalSections = getSectionsByType(sections, type);
const elements = [];
for (const element of additionalSections) {
const newId = element.id;
const props = cloneDeep(component.props);
props.path = 'sections.' + newId;
props.name = element.name;
props.isDeletable = true;
props.addMore = false;
props.isDuplicated = true;
const newComponent = React.cloneElement(component, props);
elements.push(
<section key={newId} id={`section-${newId}`}>
{newComponent}
</section>
);
}
sectionsComponents.push(...elements);
}
}
return sectionsComponents;
};
return (
@ -65,12 +108,10 @@ const LeftSidebar = () => {
variant={isDesktop ? 'persistent' : 'temporary'}
>
<div className={styles.container}>
<nav className="overflow-y-scroll">
<nav className="overflow-y-auto">
<div>
<Link href="/dashboard">
<a className="inline-flex">
<Logo size={40} />
</a>
<Logo size={40} />
</Link>
<Divider />
</div>
@ -89,7 +130,7 @@ const LeftSidebar = () => {
{customSections.map(({ id }) => (
<Tooltip key={id} title={get(sections, `${id}.name`, '') as string} placement="right" arrow>
<IconButton onClick={() => handleClick(id)}>
<IconButton onClick={() => id && handleClick(id)}>
<Star />
</IconButton>
</Tooltip>
@ -100,15 +141,11 @@ const LeftSidebar = () => {
</nav>
<main>
{left.map(({ id, component }) => (
<section key={id} id={id}>
{component}
</section>
))}
{sectionsList()}
{customSections.map(({ id }) => (
<section key={id} id={`section-${id}`}>
<Section path={`sections.${id}`} isEditable isHideable isDeletable />
<Section path={`sections.${id}`} type="custom" isEditable isHideable isDeletable />
</section>
))}

View File

@ -1,6 +1,6 @@
import { Add } from '@mui/icons-material';
import { Button } from '@mui/material';
import { ListItem } from '@reactive-resume/schema';
import { ListItem, Section as SectionRecord, SectionType } from '@reactive-resume/schema';
import clsx from 'clsx';
import get from 'lodash/get';
import { useTranslation } from 'next-i18next';
@ -10,28 +10,34 @@ import Heading from '@/components/shared/Heading';
import List from '@/components/shared/List';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { ModalName, setModalState } from '@/store/modal/modalSlice';
import { duplicateItem } from '@/store/resume/resumeSlice';
import { duplicateItem, duplicateSection } from '@/store/resume/resumeSlice';
import SectionSettings from './SectionSettings';
type Props = {
path: `sections.${string}`;
type?: SectionType;
name?: string;
titleKey?: string;
subtitleKey?: string;
isEditable?: boolean;
isHideable?: boolean;
isDeletable?: boolean;
addMore?: boolean;
isDuplicated?: boolean;
};
const Section: React.FC<Props> = ({
path,
name = 'Section Name',
type = 'basic',
titleKey = 'title',
subtitleKey = 'subtitle',
isEditable = false,
isHideable = false,
isDeletable = false,
addMore = false,
isDuplicated = false,
}) => {
const { t } = useTranslation();
@ -41,22 +47,40 @@ const Section: React.FC<Props> = ({
const visibility = useAppSelector<boolean>((state) => get(state.resume.present, `${path}.visible`, true));
const handleAdd = () => {
const id = path.split('.')[1];
const modal: ModalName = validate(id) ? 'builder.sections.custom' : `builder.${path}`;
const modal: ModalName = `builder.sections.${type}`;
dispatch(setModalState({ modal, state: { open: true, payload: { path } } }));
};
const handleEdit = (item: ListItem) => {
const id = path.split('.')[1];
const modal: ModalName = validate(id) ? 'builder.sections.custom' : `builder.${path}`;
let modal: ModalName = validate(id) ? 'builder.sections.custom' : `builder.${path}`;
const payload = validate(id) ? { path, item } : { item };
if (isDuplicated) {
modal = `builder.sections.${type}`;
payload.path = path;
}
dispatch(setModalState({ modal, state: { open: true, payload } }));
};
const handleDuplicate = (item: ListItem) => dispatch(duplicateItem({ path: `${path}.items`, value: item }));
const handleDuplicateSection = () => {
const newSection: SectionRecord = {
name: `${heading}`,
type: type,
visible: true,
columns: 2,
items: [],
isDuplicated: true,
};
dispatch(duplicateSection({ value: newSection, type }));
};
return (
<>
<Heading path={path} name={name} isEditable={isEditable} isHideable={isHideable} isDeletable={isDeletable} />
@ -77,6 +101,16 @@ const Section: React.FC<Props> = ({
{t<string>('builder.common.actions.add', { token: heading })}
</Button>
</footer>
{addMore ? (
<div className="py-6 text-right">
<Button fullWidth variant="outlined" startIcon={<Add />} onClick={handleDuplicateSection}>
{t<string>('builder.common.actions.duplicate')}
</Button>
</div>
) : (
<></>
)}
</>
);
};

View File

@ -43,7 +43,7 @@ const RightSidebar = () => {
variant={isDesktop ? 'persistent' : 'temporary'}
>
<div className={styles.container}>
<nav className="overflow-y-scroll">
<nav className="overflow-y-auto">
<div>
<Avatar size={40} />
<Divider />

View File

@ -17,7 +17,9 @@ const CustomCSS = () => {
const dispatch = useAppDispatch();
const customCSS: CustomCSSType = useAppSelector((state) => get(state.resume.present, 'metadata.css', {}));
const customCSS: CustomCSSType = useAppSelector((state) =>
get(state.resume.present, 'metadata.css', {} as CustomCSSType)
);
const handleChange = (value: string | undefined) => {
dispatch(setResumeState({ path: 'metadata.css.value', value }));

View File

@ -1,5 +1,6 @@
import { PictureAsPdf, Schema } from '@mui/icons-material';
import { List, ListItem, ListItemButton, ListItemText } from '@mui/material';
import dayjs from 'dayjs';
import get from 'lodash/get';
import pick from 'lodash/pick';
import { useTranslation } from 'next-i18next';
@ -45,8 +46,9 @@ const Export = () => {
const slug = get(resume, 'slug');
const username = get(resume, 'user.username');
const updatedAt = get(resume, 'updatedAt');
const url = await mutateAsync({ username, slug });
const url = await mutateAsync({ username, slug, lastUpdated: dayjs(updatedAt).unix().toString() });
download(url);
};

View File

@ -3,7 +3,7 @@ import { Button } from '@mui/material';
import { useTranslation } from 'next-i18next';
import Heading from '@/components/shared/Heading';
import { DOCS_URL, DONATION_URL, GITHUB_ISSUES_URL, GITHUB_URL } from '@/constants/index';
import { DOCS_URL, DONATION_URL, GITHUB_ISSUES_URL, GITHUB_URL, REDDIT_URL } from '@/constants/index';
import styles from './Links.module.scss';
@ -50,6 +50,12 @@ const Links = () => {
</Button>
</a>
<a href={REDDIT_URL} target="_blank" rel="noreferrer">
<Button variant="text" startIcon={<Link />}>
{t<string>('builder.rightSidebar.sections.links.reddit')}
</Button>
</a>
<a href={DOCS_URL} target="_blank" rel="noreferrer">
<Button variant="text" startIcon={<Link />}>
{t<string>('builder.rightSidebar.sections.links.docs')}

View File

@ -47,11 +47,11 @@ const Settings = () => {
const id: number = useMemo(() => get(resume, 'id'), [resume]);
const slug: string = useMemo(() => get(resume, 'slug'), [resume]);
const username: string = useMemo(() => get(resume, 'user.username'), [resume]);
const pageConfig: PageConfig = useMemo(() => get(resume, 'metadata.page'), [resume]);
const dateConfig: DateConfig = useMemo(() => get(resume, 'metadata.date'), [resume]);
const pageConfig: PageConfig | undefined = useMemo(() => get(resume, 'metadata.page'), [resume]);
const isDarkMode = useMemo(() => theme === 'dark', [theme]);
const exampleDateString = useMemo(() => `Eg. ${dayjs().utc().format(dateConfig.format)}`, [dateConfig.format]);
const exampleDateString = useMemo(() => `Eg. ${dayjs().format(dateConfig.format)}`, [dateConfig.format]);
const themeString = useMemo(() => (isDarkMode ? 'Matte Black Everything' : 'As bright as your future'), [isDarkMode]);
const { mutateAsync: loadSampleDataMutation } = useMutation<Resume, ServerError, LoadSampleDataParams>(
@ -98,7 +98,7 @@ const Settings = () => {
<>
<Heading path="metadata.settings" name={t<string>('builder.rightSidebar.sections.settings.heading')} />
<List sx={{ padding: 0 }}>
<List disablePadding>
{/* Global Settings */}
<>
<ListSubheader disableSticky className="rounded">
@ -212,7 +212,7 @@ const Settings = () => {
{t<string>('builder.rightSidebar.sections.settings.resume.heading')}
</ListSubheader>
<ListItem>
<ListItem disableGutters>
<ListItemButton onClick={handleLoadSampleData}>
<ListItemIcon>
<Anchor />
@ -224,7 +224,7 @@ const Settings = () => {
</ListItemButton>
</ListItem>
<ListItem>
<ListItem disableGutters>
<ListItemButton onClick={handleResetResume}>
<ListItemIcon>
<DeleteForever />

View File

@ -31,7 +31,14 @@ const Templates = () => {
<div key={template.id} className={styles.template}>
<div className={clsx(styles.preview, { [styles.selected]: template.id === currentTemplate })}>
<ButtonBase onClick={() => handleChange(template)}>
<Image src={template.preview} alt={template.name} className="rounded-sm" layout="fill" priority />
<Image
fill
priority
alt={template.name}
src={template.preview}
className="rounded-sm"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</ButtonBase>
</div>

View File

@ -16,9 +16,7 @@ type Props = {
const ResumeCard: React.FC<Props> = ({ modal, icon: Icon, title, subtitle }) => {
const dispatch = useAppDispatch();
const handleClick = () => {
dispatch(setModalState({ modal, state: { open: true } }));
};
const handleClick = () => dispatch(setModalState({ modal, state: { open: true } }));
return (
<section className={styles.resume}>

View File

@ -115,9 +115,7 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
}}
>
<ButtonBase className={styles.preview}>
{resume.image ? (
<Image src={resume.image} alt={resume.name} objectFit="cover" layout="fill" priority />
) : null}
{resume.image ? <Image src={resume.image} alt={resume.name} priority width={400} height={0} /> : null}
</ButtonBase>
</Link>

View File

@ -47,9 +47,9 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
<Image
width={size}
height={size}
alt={user?.name}
className={styles.avatar}
src={getGravatarUrl(email, size)}
alt={user?.name ?? 'User Avatar'}
/>
</IconButton>

View File

@ -4,8 +4,8 @@ type Props = {
size?: 256 | 64 | 48 | 40 | 32;
};
const Logo: React.FC<Props> = ({ size = 64 }) => {
return <Image alt="Reactive Resume" src="/images/logos/logo.svg" className="rounded" width={size} height={size} />;
};
const Logo: React.FC<Props> = ({ size = 64 }) => (
<Image alt="Reactive Resume" src="/images/logos/logo.svg" className="rounded" width={size} height={size} priority />
);
export default Logo;

View File

@ -1,7 +1,9 @@
import clsx from 'clsx';
import { isEmpty } from 'lodash';
import ReactMarkdown from 'react-markdown';
import rehypeKatex from 'rehype-katex';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
type Props = {
children?: string;
@ -12,7 +14,11 @@ const Markdown: React.FC<Props> = ({ className, children }) => {
if (!children || isEmpty(children)) return null;
return (
<ReactMarkdown remarkPlugins={[remarkGfm]} className={clsx('markdown', className)}>
<ReactMarkdown
className={clsx('markdown', className)}
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
>
{children}
</ReactMarkdown>
);

View File

@ -22,6 +22,7 @@ const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, m
const dispatch = useAppDispatch();
const stateValue = useAppSelector((state) => get(state.resume.present, path, ''));
const dateFormat = useAppSelector((state) => state.resume.present.metadata.date.format);
useEffect(() => {
setValue(stateValue);
@ -56,14 +57,16 @@ const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, m
if (type === 'date') {
return (
<DatePicker
showToolbar
openTo="year"
label={label}
value={value}
toolbarFormat={dateFormat}
views={['year', 'month', 'day']}
renderInput={(params) => <TextField {...params} error={false} className={className} />}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && onChangeValue('');
date && dayjs(date).utc().isValid() && onChangeValue(dayjs(date).utc().toISOString());
date && dayjs(date).isValid() && onChangeValue(dayjs(date).format('YYYY-MM-DD'));
}}
/>
);

View File

@ -13,7 +13,7 @@ export const languages: Language[] = [
{ code: 'ca', name: 'Catalan', localName: 'Valencian' },
{ code: 'cs', name: 'Czech', localName: 'čeština' },
{ code: 'da', name: 'Danish', localName: 'Dansk' },
{ code: 'de', name: 'German', localName: 'Deutsch' },
{ code: 'de', name: 'German', localName: 'Deutsch Formell / Sie' },
{ code: 'el', name: 'Greek', localName: 'Ελληνικά' },
{ code: 'en', name: 'English' },
{ code: 'es', name: 'Spanish', localName: 'Español' },

View File

@ -23,7 +23,7 @@ import {
VolunteerActivism,
Work,
} from '@mui/icons-material';
import { Section as SectionRecord } from '@reactive-resume/schema';
import { Section as SectionRecord, SectionType } from '@reactive-resume/schema';
import isEmpty from 'lodash/isEmpty';
import Basics from '@/components/build/LeftSidebar/sections/Basics';
@ -60,59 +60,136 @@ export const left: SidebarSection[] = [
{
id: 'work',
icon: <Work />,
component: <Section path="sections.work" titleKey="name" subtitleKey="position" isEditable isHideable />,
component: (
<Section
type={'work'}
addMore={true}
path="sections.work"
titleKey="name"
subtitleKey="position"
isEditable
isHideable
/>
),
},
{
id: 'education',
icon: <School />,
component: <Section path="sections.education" titleKey="institution" subtitleKey="area" isEditable isHideable />,
component: (
<Section
type={'education'}
path="sections.education"
titleKey="institution"
subtitleKey="area"
isEditable
isHideable
/>
),
},
{
id: 'awards',
icon: <EmojiEvents />,
component: <Section path="sections.awards" titleKey="title" subtitleKey="awarder" isEditable isHideable />,
component: (
<Section type={'awards'} path="sections.awards" titleKey="title" subtitleKey="awarder" isEditable isHideable />
),
},
{
id: 'certifications',
icon: <CardGiftcard />,
component: <Section path="sections.certifications" titleKey="name" subtitleKey="issuer" isEditable isHideable />,
component: (
<Section
type={'certifications'}
path="sections.certifications"
titleKey="name"
subtitleKey="issuer"
isEditable
isHideable
/>
),
},
{
id: 'publications',
icon: <MenuBook />,
component: <Section path="sections.publications" titleKey="name" subtitleKey="publisher" isEditable isHideable />,
component: (
<Section
type={'publications'}
path="sections.publications"
titleKey="name"
subtitleKey="publisher"
isEditable
isHideable
/>
),
},
{
id: 'skills',
icon: <Architecture />,
component: <Section path="sections.skills" titleKey="name" subtitleKey="level" isEditable isHideable />,
component: (
<Section type={'skills'} path="sections.skills" titleKey="name" subtitleKey="level" isEditable isHideable />
),
},
{
id: 'languages',
icon: <Language />,
component: <Section path="sections.languages" titleKey="name" subtitleKey="level" isEditable isHideable />,
component: (
<Section type={'languages'} path="sections.languages" titleKey="name" subtitleKey="level" isEditable isHideable />
),
},
{
id: 'interests',
icon: <Sailing />,
component: <Section path="sections.interests" titleKey="name" subtitleKey="keywords" isEditable isHideable />,
component: (
<Section
type={'interests'}
path="sections.interests"
titleKey="name"
subtitleKey="keywords"
isEditable
isHideable
/>
),
},
{
id: 'volunteer',
icon: <VolunteerActivism />,
component: (
<Section path="sections.volunteer" titleKey="organization" subtitleKey="position" isEditable isHideable />
<Section
type={'volunteer'}
path="sections.volunteer"
titleKey="organization"
subtitleKey="position"
isEditable
isHideable
/>
),
},
{
id: 'projects',
icon: <Coffee />,
component: <Section path="sections.projects" titleKey="name" subtitleKey="description" isEditable isHideable />,
component: (
<Section
type={'projects'}
path="sections.projects"
titleKey="name"
subtitleKey="description"
isEditable
isHideable
/>
),
},
{
id: 'references',
icon: <Groups />,
component: <Section path="sections.references" titleKey="name" subtitleKey="relationship" isEditable isHideable />,
component: (
<Section
type={'references'}
path="sections.references"
titleKey="name"
subtitleKey="relationship"
isEditable
isHideable
/>
),
},
];
@ -164,7 +241,19 @@ export const right: SidebarSection[] = [
},
];
export const getCustomSections = (sections: Record<string, SectionRecord>): Array<Required<SectionRecord>> => {
export const getSectionsByType = (sections: Record<string, SectionRecord>, type: SectionType): SectionRecord[] => {
if (isEmpty(sections)) return [];
return Object.entries(sections).reduce((acc, [id, section]) => {
if (section.type.startsWith(type) && section.isDuplicated) {
return [...acc, { ...section, id }];
}
return acc;
}, [] as SectionRecord[]);
};
export const getCustomSections = (sections: Record<string, SectionRecord>): SectionRecord[] => {
if (isEmpty(sections)) return [];
return Object.entries(sections).reduce((acc, [id, section]) => {
@ -173,7 +262,7 @@ export const getCustomSections = (sections: Record<string, SectionRecord>): Arra
}
return acc;
}, [] as Array<Required<SectionRecord>>);
}, [] as SectionRecord[]);
};
const sections = [...left, ...right];

View File

@ -13,6 +13,7 @@ export const DOCS_URL = 'https://docs.rxresu.me';
export const DONATION_URL = 'https://paypal.me/RajaRajanA';
export const TRANSLATE_URL = 'https://translate.rxresu.me/';
export const DIGITALOCEAN_URL = 'https://pillai.xyz/digitalocean';
export const REDDIT_URL = 'https://www.reddit.com/r/reactiveresume/';
export const GITHUB_URL = 'https://github.com/AmruthPillai/Reactive-Resume';
export const PRODUCT_HUNT_URL = 'https://www.producthunt.com/posts/reactive-resume-v3';
export const GITHUB_ISSUES_URL = 'https://github.com/AmruthPillai/Reactive-Resume/issues/new/choose';

View File

@ -169,7 +169,8 @@ const LoginModal: React.FC = () => {
<p className="text-xs">
<Trans t={t} i18nKey="modals.auth.login.recover-text">
In case you have forgotten your password, you can <a onClick={handleRecoverAccount}>recover your account here.</a>
In case you have forgotten your password, you can
<a onClick={handleRecoverAccount}>recover your account here.</a>
</Trans>
</p>
</BaseModal>

View File

@ -134,7 +134,7 @@ const AwardModal: React.FC = () => {
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
}}
renderInput={(params) => (
<TextField

View File

@ -134,7 +134,7 @@ const CertificateModal: React.FC = () => {
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
}}
renderInput={(params) => (
<TextField

View File

@ -60,13 +60,14 @@ const CustomModal: React.FC = () => {
const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal['builder.sections.custom']);
const path: string = get(payload, 'path', '');
const path: string = get(payload, 'path', 'sections.custom');
const item: FormData = get(payload, 'item', null);
const isEditMode = useMemo(() => !!item, [item]);
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
@ -150,7 +151,7 @@ const CustomModal: React.FC = () => {
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
}}
renderInput={(params) => (
<TextField
@ -174,7 +175,7 @@ const CustomModal: React.FC = () => {
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
}}
renderInput={(params) => (
<TextField
@ -260,9 +261,9 @@ const CustomModal: React.FC = () => {
multiline
minRows={3}
maxRows={6}
label={t<string>('builder.common.form.summary.label')}
className="col-span-2"
error={!!fieldState.error}
label={t<string>('builder.common.form.summary.label')}
helperText={fieldState.error?.message || <MarkdownSupported />}
{...field}
/>

View File

@ -173,7 +173,7 @@ const EducationModal: React.FC = () => {
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
}}
renderInput={(params) => (
<TextField
@ -197,7 +197,7 @@ const EducationModal: React.FC = () => {
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
}}
renderInput={(params) => (
<TextField

View File

@ -143,7 +143,7 @@ const ProjectModal: React.FC = () => {
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
}}
renderInput={(params) => (
<TextField
@ -167,7 +167,7 @@ const ProjectModal: React.FC = () => {
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
}}
renderInput={(params) => (
<TextField

View File

@ -134,7 +134,7 @@ const PublicationModal: React.FC = () => {
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
}}
renderInput={(params) => (
<TextField

View File

@ -140,7 +140,7 @@ const VolunteerModal: React.FC = () => {
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
}}
renderInput={(params) => (
<TextField
@ -164,7 +164,7 @@ const VolunteerModal: React.FC = () => {
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
}}
renderInput={(params) => (
<TextField

View File

@ -2,7 +2,7 @@ import { joiResolver } from '@hookform/resolvers/joi';
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
import { Button, TextField } from '@mui/material';
import { DatePicker } from '@mui/x-date-pickers';
import { SectionPath, WorkExperience } from '@reactive-resume/schema';
import { WorkExperience } from '@reactive-resume/schema';
import dayjs from 'dayjs';
import Joi from 'joi';
import get from 'lodash/get';
@ -20,8 +20,6 @@ import { addItem, editItem } from '@/store/resume/resumeSlice';
type FormData = WorkExperience;
const path: SectionPath = 'sections.work';
const defaultState: FormData = {
name: '',
position: '',
@ -50,10 +48,12 @@ const WorkModal: React.FC = () => {
const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
const { open: isOpen, payload } = useAppSelector((state) => state.modal['builder.sections.work']);
const path: string = get(payload, 'path', 'sections.work');
const item: FormData = get(payload, 'item', null);
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
const isEditMode = useMemo(() => !!item, [item]);
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
@ -77,7 +77,7 @@ const WorkModal: React.FC = () => {
const handleClose = () => {
dispatch(
setModalState({
modal: `builder.${path}`,
modal: 'builder.sections.work',
state: { open: false },
})
);
@ -140,7 +140,7 @@ const WorkModal: React.FC = () => {
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
}}
renderInput={(params) => (
<TextField
@ -164,7 +164,7 @@ const WorkModal: React.FC = () => {
views={['year', 'month', 'day']}
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
isEmpty(keyboardInputValue) && field.onChange('');
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
}}
renderInput={(params) => (
<TextField

View File

@ -10,75 +10,76 @@
"dependencies": {
"@beam-australia/react-env": "^3.1.1",
"@date-io/dayjs": "^2.16.0",
"@emotion/css": "^11.10.0",
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@hello-pangea/dnd": "^16.0.1",
"@hookform/resolvers": "2.9.9",
"@emotion/css": "^11.10.5",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@hello-pangea/dnd": "^16.2.0",
"@hookform/resolvers": "2.9.10",
"@monaco-editor/react": "^4.4.6",
"@mui/icons-material": "^5.10.9",
"@mui/lab": "^5.0.0-alpha.103",
"@mui/material": "^5.10.9",
"@mui/system": "^5.10.9",
"@mui/x-date-pickers": "5.0.4",
"@next/env": "^12.3.1",
"@react-oauth/google": "^0.2.8",
"@reduxjs/toolkit": "^1.8.6",
"axios": "^1.1.2",
"@mui/icons-material": "^5.11.0",
"@mui/lab": "^5.0.0-alpha.114",
"@mui/material": "^5.11.3",
"@mui/system": "^5.11.2",
"@mui/x-date-pickers": "5.0.12",
"@next/env": "^13.1.1",
"@react-oauth/google": "^0.5.1",
"@reduxjs/toolkit": "^1.9.1",
"axios": "^1.2.2",
"clsx": "^1.2.1",
"dayjs": "^1.11.5",
"dayjs": "^1.11.7",
"downloadjs": "^1.4.7",
"joi": "^17.6.3",
"joi": "^17.7.0",
"lodash": "^4.17.21",
"md5-hex": "^4.0.0",
"monaco-editor": "^0.34.0",
"nanoid": "^3.3.4",
"next": "12.3.1",
"next-i18next": "^12.1.0",
"monaco-editor": "^0.34.1",
"nanoid": "3.3.4",
"next": "13.1.1",
"next-i18next": "^13.0.2",
"react": "^18.2.0",
"react-colorful": "^5.6.1",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "^18.2.0",
"react-hook-form": "^7.37.0",
"react-hook-form": "^7.41.3",
"react-hot-toast": "2.4.0",
"react-hotkeys-hook": "^3.4.7",
"react-icons": "^4.6.0",
"react-markdown": "^8.0.3",
"react-icons": "^4.7.1",
"react-markdown": "^8.0.4",
"react-query": "^3.39.2",
"react-redux": "^8.0.4",
"react-redux": "^8.0.5",
"react-zoom-pan-pinch": "^2.1.3",
"redux": "^4.2.0",
"redux-persist": "^6.0.0",
"redux-saga": "^1.2.1",
"redux-saga": "^1.2.2",
"redux-undo": "^1.0.1",
"rehype-katex": "^6.0.2",
"remark-gfm": "^3.0.1",
"sharp": "^0.31.1",
"remark-math": "^5.1.1",
"sharp": "^0.31.3",
"uuid": "^9.0.0",
"webfontloader": "^1.6.28"
},
"devDependencies": {
"@babel/core": "^7.19.3",
"@babel/core": "^7.20.7",
"@reactive-resume/schema": "workspace:*",
"eslint-plugin-unused-imports": "^2.0.0",
"@tailwindcss/typography": "^0.5.7",
"@tailwindcss/typography": "^0.5.8",
"@types/downloadjs": "^1.4.3",
"@types/lodash": "^4.14.186",
"@types/node": "^18.11.0",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"@types/react-redux": "^7.1.24",
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.18",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
"@types/react-redux": "^7.1.25",
"@types/tailwindcss": "^3.0.11",
"@types/uuid": "^8.3.4",
"@types/uuid": "^9.0.0",
"@types/webfontloader": "^1.6.35",
"autoprefixer": "^10.4.12",
"autoprefixer": "^10.4.13",
"csstype": "^3.1.1",
"eslint-config-next": "^12.3.1",
"eslint-plugin-tailwindcss": "^3.6.2",
"next-sitemap": "^3.1.25",
"postcss": "^8.4.18",
"sass": "^1.55.0",
"tailwindcss": "^3.1.8",
"typescript": "^4.8.4"
"eslint-config-next": "^13.1.1",
"eslint-plugin-tailwindcss": "^3.8.0",
"eslint-plugin-unused-imports": "^2.0.0",
"next-sitemap": "^3.1.44",
"postcss": "^8.4.20",
"sass": "^1.57.1",
"tailwindcss": "^3.2.4",
"typescript": "^4.9.4"
}
}

View File

@ -2,6 +2,7 @@ import { Download, Downloading } from '@mui/icons-material';
import { ButtonBase } from '@mui/material';
import { Resume } from '@reactive-resume/schema';
import clsx from 'clsx';
import dayjs from 'dayjs';
import download from 'downloadjs';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
@ -60,10 +61,12 @@ const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
}, [dispatch, initialData]);
useEffect(() => {
if (!isEmpty(resume) && router.locale !== resume.metadata.locale) {
const locale = get(resume, 'metadata.locale', 'en');
if (!isEmpty(resume) && router.locale !== locale) {
const { pathname, asPath, query } = router;
router.push({ pathname, query }, asPath, { locale: resume.metadata.locale });
router.push({ pathname, query }, asPath, { locale });
}
}, [resume, router]);
@ -96,7 +99,9 @@ const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
const handleDownload = async () => {
try {
const url = await mutateAsync({ username, slug });
const updatedAt = get(resume, 'updatedAt');
const url = await mutateAsync({ username, slug, lastUpdated: dayjs(updatedAt).unix().toString() });
download(url);
} catch {

View File

@ -27,7 +27,7 @@ type Props = {
export const getServerSideProps: GetServerSideProps<Props | Promise<Props>, QueryParams> = async ({
query,
locale,
locale = 'en',
}) => {
const { username, slug, secretKey } = query as QueryParams;
@ -35,7 +35,7 @@ export const getServerSideProps: GetServerSideProps<Props | Promise<Props>, Quer
if (isEmpty(secretKey)) throw new Error('There is no secret key!');
const resume = await fetchResumeByIdentifier({ username, slug, options: { secretKey } });
const displayLocale = resume.metadata.locale || locale || 'en';
const displayLocale = get(resume, 'metadata.locale') ?? locale;
return {
props: {

View File

@ -21,7 +21,7 @@ import WrapperRegistry from '@/wrappers/index';
const App = ({ Component, pageProps }: AppProps): JSX.Element => (
<>
<Head>
<title>Reactive Resume</title>
<title>Reactive Resume | A free and open source resume builder</title>
<meta
name="description"

View File

@ -2,7 +2,7 @@ import { NextPage } from 'next';
import NextDocument, { DocumentContext, Head, Html, Main, NextScript } from 'next/document';
const Document: NextPage = () => (
<Html>
<Html lang="en">
<Head />
<body>

View File

@ -17,13 +17,11 @@ import { fetchResumes } from '@/services/resume';
import { useAppDispatch } from '@/store/hooks';
import styles from '@/styles/pages/Dashboard.module.scss';
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => {
return {
props: {
...(await serverSideTranslations(locale, ['common', 'modals', 'dashboard'])),
},
};
};
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => ({
props: {
...(await serverSideTranslations(locale, ['common', 'modals', 'dashboard'])),
},
});
const Dashboard: NextPage = () => {
const { t } = useTranslation();
@ -48,9 +46,7 @@ const Dashboard: NextPage = () => {
<header>
<Link href="/">
<a>
<Logo size={40} />
</a>
<Logo size={40} />
</Link>
<Avatar size={40} />
@ -58,15 +54,15 @@ const Dashboard: NextPage = () => {
<main className={styles.resumes}>
<ResumeCard
modal="dashboard.create-resume"
icon={Add}
modal="dashboard.create-resume"
title={t<string>('dashboard.create-resume.title')}
subtitle={t<string>('dashboard.create-resume.subtitle')}
/>
<ResumeCard
modal="dashboard.import-external"
icon={ImportExport}
modal="dashboard.import-external"
title={t<string>('dashboard.import-external.title')}
subtitle={t<string>('dashboard.import-external.subtitle')}
/>

View File

@ -20,15 +20,13 @@ import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
import styles from '@/styles/pages/Home.module.scss';
import { DIGITALOCEAN_URL, DOCS_URL, DONATION_URL, GITHUB_URL } from '../constants';
import { DIGITALOCEAN_URL, DOCS_URL, DONATION_URL, GITHUB_URL, REDDIT_URL } from '../constants';
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => {
return {
props: {
...(await serverSideTranslations(locale, ['common', 'modals', 'landing'])),
},
};
};
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => ({
props: {
...(await serverSideTranslations(locale, ['common', 'modals', 'landing'])),
},
});
const Home: NextPage = () => {
const { t } = useTranslation();
@ -39,11 +37,8 @@ const Home: NextPage = () => {
const isLoggedIn = useAppSelector((state) => state.auth.isLoggedIn);
const handleLogin = () => dispatch(setModalState({ modal: 'auth.login', state: { open: true } }));
const handleRegister = () => dispatch(setModalState({ modal: 'auth.register', state: { open: true } }));
const handleToggle = () => dispatch(setTheme({ theme: theme === 'light' ? 'dark' : 'light' }));
const handleLogout = () => dispatch(logout());
return (
@ -117,7 +112,13 @@ const Home: NextPage = () => {
<div className={styles.screenshots}>
{screenshots.map(({ src, alt }) => (
<a key={src} href={src} className={styles.image} target="_blank" rel="noreferrer">
<Image src={src} alt={alt} layout="fill" objectFit="cover" />
<Image
fill
src={src}
alt={alt}
className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</a>
))}
</div>
@ -176,6 +177,12 @@ const Home: NextPage = () => {
</Button>
</a>
<a href={REDDIT_URL} target="_blank" rel="noreferrer">
<Button variant="text" startIcon={<LinkIcon />}>
{t<string>('landing.links.links.reddit')}
</Button>
</a>
<a href={DONATION_URL} target="_blank" rel="noreferrer">
<Button variant="text" startIcon={<LinkIcon />}>
{t<string>('landing.links.links.donate')}
@ -186,7 +193,13 @@ const Home: NextPage = () => {
<section className={styles.section}>
<a href={DIGITALOCEAN_URL} target="_blank" rel="noreferrer">
<Image src={`/images/sponsors/${theme=="dark"?"digitalocean":"digitaloceanLight"}.svg`} alt="Powered By DigitalOcean" width={200} height={40} />
<Image
src={`/images/sponsors/${theme == 'dark' ? 'digitalocean' : 'digitaloceanLight'}.svg`}
style={{ width: 200, height: 40, objectFit: 'contain' }}
alt="Powered By DigitalOcean"
width={200}
height={40}
/>
</a>
</section>

View File

@ -2,6 +2,7 @@ import { Download, Downloading } from '@mui/icons-material';
import { ButtonBase } from '@mui/material';
import { Resume } from '@reactive-resume/schema';
import clsx from 'clsx';
import dayjs from 'dayjs';
import download from 'downloadjs';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
@ -69,7 +70,11 @@ const Preview: NextPage<Props> = ({ shortId }) => {
const handleDownload = async () => {
try {
const url = await mutateAsync({ username: resume.user.username, slug: resume.slug });
const url = await mutateAsync({
username: resume.user.username,
slug: resume.slug,
lastUpdated: dayjs(resume.updatedAt).unix().toString(),
});
download(url);
} catch {

View File

@ -1,9 +1,10 @@
{
"common": {
"actions": {
"add": "Neue {{token}} hinzufügen",
"delete": "Löschen {{token}}",
"edit": "Bearbeiten {{token}}"
"add": "{{token}} hinzufügen",
"delete": "{{token}} löschen",
"edit": "{{token}} bearbeiten",
"duplicate": "Abschnitt duplizieren"
},
"columns": {
"heading": "Spalten",
@ -17,10 +18,10 @@
"label": "Beschreibung"
},
"email": {
"label": "E-Mail Adresse"
"label": "E-Mail-Adresse"
},
"end-date": {
"help-text": "Dieses Feld leer lassen, wenn noch vorhanden",
"help-text": "Dieses Feld leer lassen, wenn dieser Eintrag noch kein Enddatum hat.",
"label": "Enddatum"
},
"keywords": {
@ -69,7 +70,7 @@
"empty-text": "Diese Liste ist leer."
},
"tooltip": {
"delete-item": "Sind Sie sicher, dass Sie dieses Element löschen möchten? Dies ist eine unumkehrbare Aktion.",
"delete-item": "Sind Sie sicher, dass Sie dieses Element löschen möchten? Dies lässt sich nicht rückgängig machen.",
"delete-section": "Abschnitt löschen",
"rename-section": "Abschnitt umbenennen",
"toggle-visibility": "Sichtbarkeit umschalten"
@ -86,7 +87,7 @@
"zoom-in": "Vergrößern",
"zoom-out": "Verkleinern",
"undo": "Rückgängig machen",
"redo": "Redo"
"redo": "Wiederholen"
}
},
"header": {
@ -96,8 +97,8 @@
"rename": "Umbenennen",
"share-link": "Link teilen",
"tooltips": {
"delete": "Sind Sie sicher, dass Sie diesen Lebenslauf löschen möchten? Dies ist eine unumkehrbare Aktion.",
"share-link": "Du musst die Sichtbarkeit deines Lebenslaufs in die Öffentlichkeit ändern, um ihn für andere sichtbar zu machen."
"delete": "Sind Sie sicher, dass Sie diesen Lebenslauf löschen möchten? Dies lässt sich nicht rückgängig machen.",
"share-link": "Sie müssen die Sichtbarkeit Ihres Lebenslaufs in Öffentlich ändern, um ihn für andere sichtbar zu machen."
}
}
},
@ -106,7 +107,7 @@
"awards": {
"form": {
"awarder": {
"label": "Auszeichnung"
"label": "Auszeichner"
}
}
},
@ -119,7 +120,7 @@
"label": "Überschrift"
},
"name": {
"label": "Voller Name"
"label": "Vollständiger Name"
},
"birthdate": {
"label": "Geburtsdatum"
@ -127,7 +128,7 @@
"photo-filters": {
"effects": {
"border": {
"label": "Grenze"
"label": "Rahmen"
},
"grayscale": {
"label": "Graustufen"
@ -158,13 +159,13 @@
"education": {
"form": {
"area-study": {
"label": "Studienbereich"
"label": "Studienfach"
},
"courses": {
"label": "Kurse"
},
"degree": {
"label": "Grad"
"label": "Abschluss"
},
"grade": {
"label": "Note"
@ -176,7 +177,7 @@
},
"location": {
"address": {
"label": "Adresse"
"label": "Straße"
},
"city": {
"label": "Stadt"
@ -184,12 +185,12 @@
"country": {
"label": "Land"
},
"heading": "Standort",
"heading": "Anschrift",
"postal-code": {
"label": "Postleitzahl"
},
"region": {
"label": "Region"
"label": "Bundesland"
}
},
"profiles": {
@ -201,7 +202,7 @@
"label": "Benutzername"
}
},
"heading": "Profiles",
"heading": "Soziale Netzwerke",
"heading_one": "Profil"
},
"publications": {
@ -239,16 +240,16 @@
"heading": "Exportieren",
"json": {
"primary": "JSON",
"secondary": "Laden Sie eine JSON-Version Ihres Lebenslaufs herunter, die Sie wieder in Reaktives Lebenslauf importieren können."
"secondary": "Laden Sie eine JSON-Version Ihres Lebenslaufs herunter, die Sie wieder in Reactive Resume importieren können."
},
"pdf": {
"loading": {
"primary": "PDF wird erstellt",
"secondary": "Bitte warten Sie, wenn Ihr PDF generiert wird, dies kann bis zu 15 Sekunden dauern."
"secondary": "Bitte warten Sie, während Ihr PDF generiert wird. Dies kann bis zu 15 Sekunden dauern."
},
"normal": {
"primary": "PDF",
"secondary": "Laden Sie sich ein PDF Ihres Lebenslaufs herunter, das Sie ausdrucken und an Ihren Traumjob senden können. Diese Datei kann nicht zur weiteren Bearbeitung importiert werden."
"secondary": "Laden Sie sich ein PDF Ihres Lebenslaufs herunter, dass Sie ausdrucken oder an Ihren Traumarbeitgeber senden können. Diese Datei kann nicht zur weiteren Bearbeitung importiert werden."
}
}
},
@ -256,18 +257,20 @@
"heading": "Layout",
"tooltip": {
"reset-layout": "Layout zurücksetzen"
}
},
"main": "Hauptteil",
"sidebar": "Seitenleiste"
},
"links": {
"bugs-features": {
"body": "Hält Sie etwas davon ab, einen Lebenslauf zu erstellen? Oder haben Sie eine tolle Idee, die Sie hinzufügen möchten? Erhöhen Sie einen Eintrag auf GitHub, um loszulegen.",
"button": "GitHub Themen",
"heading": "Fehler? Feature-Anfragen?"
"body": "Sind Sie bei der Erstellung Ihres Lebenslaufs auf ein Problem gestoßen? Oder haben Sie eine tolle Idee, die Sie hinzufügen möchten? Erstellen Sie ein Ticket auf GitHub.",
"button": "GitHub Issues",
"heading": "Fehler? Verbesserungsvorschläge?"
},
"donate": {
"body": "Wenn Ihnen Reactive Resume gefallen hat, denken Sie bitte darüber nach, so viel wie möglich zu spenden, damit die App für immer kostenlos und werbefrei bleibt.",
"body": "Sollte Ihnen Reactive Resume gefallen, möchte ich Sie bitten, etwas zu spenden, damit die App für immer kostenlos und werbefrei bleibt.",
"button": "Kaufe mir einen Kaffee",
"heading": "Spenden an Reaktives Lebenslauf"
"heading": "Spenden Sie an Reactive Resume."
},
"github": "Quellcode",
"docs": "Dokumentation",
@ -277,43 +280,44 @@
"global": {
"date": {
"primary": "Datum",
"secondary": "Datumsformat für die gesamte App"
"secondary": "Datumsformat für die gesamte App.",
"prefix": "Z.B."
},
"heading": "Globale",
"heading": "Global",
"language": {
"primary": "Sprache",
"secondary": "Sprache anzeigen, die in der gesamten App verwendet wird"
"secondary": "Anzeigesprache, die in der gesamten App verwendet wird."
},
"theme": {
"primary": "Thema"
"primary": "App Design"
}
},
"heading": "Einstellungen",
"page": {
"format": {
"primary": "Papier größe",
"secondary": "Legt die Abmessungen Ihrer Lebenslaufseiten fest"
"primary": "Papiergröße",
"secondary": "Legt die Seitenabmessungen Ihres Lebenslaufs fest."
},
"break-line": {
"primary": "Linie anhalten",
"secondary": "Zeile auf allen Seiten anzeigen, um die Höhe einer A4-Seite zu markieren"
"primary": "Seitenumbruch anzeigen",
"secondary": "Zeigt den Seitenumbruch als Linie auf allen Seiten an."
},
"heading": "Seite",
"orientation": {
"disabled": "Hat keine Auswirkung, wenn nur eine Seite vorhanden ist",
"disabled": "Hat keine Auswirkung, wenn nur eine Seite vorhanden ist.",
"primary": "Ausrichtung",
"secondary": "Ob Seiten horizontal oder vertikal angezeigt werden sollen"
"secondary": "Legt fest, ob Seiten horizontal oder vertikal angezeigt werden sollen."
}
},
"resume": {
"heading": "Lebenslauf",
"reset": {
"primary": "Alles zurücksetzen",
"secondary": "Zu viele Fehler gemacht? Klicken Sie hier, um alle Änderungen zurückzusetzen und bei Null zu beginnen. Sei vorsichtig, diese Aktion kann nicht rückgängig gemacht werden."
"secondary": "Zu viele Fehler gemacht? Klicken Sie hier, um alle Änderungen zurückzusetzen und von vorne zu beginnen. Vorsicht! Diese Aktion kann nicht rückgängig gemacht werden."
},
"sample": {
"primary": "Beispieldaten laden",
"secondary": "Nicht sicher, wo man anfangen soll? Klicken Sie hier, um ein paar Beispieldaten zu laden, um zu sehen, wie ein vollständiger Lebenslauf aussieht."
"secondary": "Sie sind nicht sicher, wo Sie anfangen sollen? Klicken Sie hier, um Beispieldaten zu laden. So können Sie sich ansehen, wie ein vollständiger Lebenslauf aussieht."
}
}
},
@ -323,8 +327,8 @@
"label": "Kurze URL bevorzugen"
},
"visibility": {
"subtitle": "Erlaube jemandem mit einem Link deinen Lebenslauf anzusehen",
"title": "Öffentlich"
"subtitle": "Erlaubt jedem, dem Sie diesen Link schicken, Ihren Lebenslauf anzusehen.",
"title": "Öffentlich zugänglich"
}
},
"templates": {
@ -333,16 +337,16 @@
"theme": {
"form": {
"background": {
"label": "Hintergrund"
"label": "Hintergrundfarbe"
},
"primary": {
"label": "Primär"
"label": "Primärfarbe"
},
"text": {
"label": "Text"
"label": "Textfarbe"
}
},
"heading": "Thema"
"heading": "Lebenslauf Design"
},
"typography": {
"form": {

View File

@ -13,14 +13,14 @@
"help-text": "Dieser Abschnitt unterstützt <1>Markdown</1> Formatierung."
},
"date": {
"present": "Gegenwärtig"
"present": "Heute"
},
"subtitle": "Ein freier und Open-Source-Lebenslauf-Builder.",
"subtitle": "Ein kostenloser Open Source Lebenslauf-Baukasten.",
"title": "Reaktives Lebenslauf",
"toast": {
"error": {
"upload-file-size": "Bitte laden Sie nur Dateien unter 2 Megabytes hoch.",
"upload-photo-size": "Bitte laden Sie nur Fotos unter 2 Megabytes hoch, vorzugsweise quadratisch."
"upload-photo-size": "Bitte laden Sie nur Fotos unter 2 Megabytes hoch, am besten in einem quadratischen Format."
},
"success": {
"resume-link-copied": "Ein Link zu deinem Lebenslauf wurde in deine Zwischenablage kopiert."

View File

@ -1,25 +1,25 @@
{
"create-resume": {
"subtitle": "Bei Null anfangen",
"subtitle": "Mit einem leeren Lebenslauf starten",
"title": "Neuen Lebenslauf erstellen"
},
"import-external": {
"subtitle": "LinkedIn, JSON Resume, Reaktives Lebenslauf",
"title": "Aus externen Quellen importieren"
"subtitle": "LinkedIn, JSON Lebenslauf, Reactive Resume",
"title": "Aus externer Quelle importieren"
},
"resume": {
"menu": {
"delete": "Löschen",
"duplicate": "Duplikat",
"duplicate": "Duplizieren",
"open": "Öffnen",
"rename": "Umbenennen",
"share-link": "Einen Link teilen",
"share-link": "Link teilen",
"tooltips": {
"delete": "Möchten Sie diesen Lebenslauf wirklich löschen? Dies ist eine irreversible Aktion.",
"delete": "Möchten Sie diesen Lebenslauf wirklich löschen? Dies lässt sich nicht rückgängig machen.",
"share-link": "Sie müssen die Sichtbarkeit Ihres Lebenslaufs auf öffentlich ändern, um ihn für andere sichtbar zu machen."
}
},
"timestamp": "Zuletzt vor {{timestamp}} aktualisiert"
"timestamp": "Zuletzt vor {{timestamp}} geändert."
},
"title": "Dashboard"
}

View File

@ -1,6 +1,6 @@
{
"actions": {
"app": "Gehe zu App",
"app": "Gehe zur App",
"login": "Anmelden",
"logout": "Ausloggen",
"register": "Registrieren"
@ -9,16 +9,16 @@
"heading": "Eigenschaften",
"list": {
"ads": "Keine Werbung",
"export": "Exportieren Sie Ihren Lebenslauf in JSON oder PDF Format",
"free": "Frei, für immer",
"import": "Importiere Daten von LinkedIn, JSON Lebenslauf",
"languages": "In mehreren Sprachen zugänglich",
"more": "Und viel mehr aufregende Features, <1>lesen Sie alles hier</1>",
"tracking": "Keine Benutzerverfolgung"
"export": "Exportieren Sie Ihren Lebenslauf als JSON oder PDF Format",
"free": "Kostenlos, für immer",
"import": "Importieren Sie Ihre Daten von LinkedIn oder als JSON Lebenslauf",
"languages": "In mehreren Sprachen verfügbar",
"more": "Und viele weitere aufregende Features. <1>Hier gibt es mehr informationen (in englischer Sprache)</1>",
"tracking": "Kein Benutzertracking"
}
},
"links": {
"heading": "Verknüpfungen",
"heading": "Links",
"links": {
"donate": "Spenden",
"github": "Quellcode",
@ -32,11 +32,11 @@
},
"testimonials": {
"heading": "Referenzen",
"body": "Gut oder schlecht, ich würde gerne Ihre Meinung über Reactive Resume und wie die Erfahrung war für Sie.<br/>Hier sind einige der Nachrichten, die von Benutzern auf der ganzen Welt gesendet werden.",
"contact": "Du kannst mich über <1>meine E-Mail</1> oder über das Kontaktformular auf <3>meiner Website</3>erreichen."
"body": "Egal ob gut oder schlecht - ich würde gerne Ihre Meinung über Reactive Resume hören und welche Erfahrungen Sie gemacht haben.<br/>Hier sind einige der Nachrichten, die mir von Benutzern auf der ganzen Welt zugesandt wurden.",
"contact": "Sie können mich über <1>meine E-Mail</1> oder über das Kontaktformular auf <3>meiner Website</3> erreichen."
},
"summary": {
"body": "Reaktives Lebenslauf ist ein freier und Open-Source-Lebenslauf-Builder, der gebaut wurde, um die weltlichen Aufgaben zu machen, zu erstellen, Aktualisieren und teilen Sie Ihren Lebenslauf so einfach wie 1, 2, 3. Mit dieser App kannst du mehrere Bewerbungen erstellen, sie mit Recruitern oder Freunden über einen einzigartigen Link teilen und sie als PDF ausdrucken. alle kostenlos, keine Werbung, keine Verfolgung, ohne die Integrität und Privatsphäre Ihrer Daten zu verlieren.",
"body": "Reactive Resume ist ein kostenloser Open Source Lebenslauf-Builder, der gebaut wurde, um die langweilige Aufgabe einen Lebenslauf zu erstellen, aktuell zu halten und zu teilen so einfach wie möglich zu machen. Mit dieser App können Sie mehrere Lebensläufe erstellen, sie mit Recruitern oder Freunden über einen einzigartigen Link teilen und als PDF exportieren. Kostenlos, ohne Werbung, kein Tracking, ohne die Integrität und Privatsphäre Ihrer Daten zu verlieren.",
"heading": "Zusammenfassung"
}
}

View File

@ -2,16 +2,16 @@
"auth": {
"forgot-password": {
"actions": {
"send-email": "Passwort zurücksetzen E-Mail senden"
"send-email": "Passwort zurücksetzen E-Mail senden."
},
"body": "Geben Sie einfach die E-Mail-Adresse ein, die mit dem Konto verknüpft ist, das Sie wiederherstellen möchten.",
"body": "Geben Sie die E-Mail-Adresse des Benutzerkontos ein, dass Sie wiederherstellen möchten.",
"form": {
"email": {
"label": "E-Mail-Addresse"
}
},
"heading": "Passwort vergessen?",
"help-text": "Wenn das Konto existiert, erhalten Sie eine E-Mail mit einem Link zum Zurücksetzen Ihres Passworts."
"help-text": "Sollte das Konto existieren, erhalten Sie eine E-Mail mit einem Link zum Zurücksetzen des Passworts."
},
"login": {
"actions": {
@ -24,12 +24,12 @@
},
"username": {
"help-text": "Sie können auch Ihre E-Mail-Adresse eingeben",
"label": "Nutzername"
"label": "Benutzername"
}
},
"heading": "Bei Ihrem Konto anmelden",
"recover-text": "Falls Sie Ihr Passwort vergessen haben, können Sie <1>Ihr Konto wiederherstellen</1> hier einrichten.",
"register-text": "Wenn Sie keinen haben, können Sie hier <1>ein Konto erstellen</1> anlegen."
"recover-text": "Falls Sie Ihr Passwort vergessen haben, können Sie es <1>hier zurücksetzen</1>.",
"register-text": "Sollten Sie kein Benutzerkonto haben, können Sie <1>hier ein Konto anlegen</1>."
},
"register": {
"actions": {

View File

@ -3,6 +3,7 @@
"actions": {
"add": "Add New {{token}}",
"delete": "Delete {{token}}",
"duplicate": "Duplicate Section",
"edit": "Edit {{token}}"
},
"columns": {
@ -80,13 +81,13 @@
"center-artboard": "Center Artboard",
"copy-link": "Copy Link to Resume",
"export-pdf": "Export PDF",
"redo": "Redo",
"toggle-orientation": "Toggle Page Orientation",
"toggle-page-break-line": "Toggle Page Break Line",
"toggle-sidebars": "Toggle Sidebars",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out",
"undo": "Undo",
"redo": "Redo"
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out"
}
},
"header": {
@ -114,6 +115,9 @@
"actions": {
"photo-filters": "Photo Filters"
},
"birthdate": {
"label": "Date of Birth"
},
"heading": "Basics",
"headline": {
"label": "Headline"
@ -121,9 +125,6 @@
"name": {
"label": "Full Name"
},
"birthdate": {
"label": "Date of Birth"
},
"photo-filters": {
"effects": {
"border": {
@ -244,7 +245,7 @@
"pdf": {
"loading": {
"primary": "Generating PDF",
"secondary": "Please wait as your PDF gets generated, this may take upto 15 seconds."
"secondary": "Please wait as your PDF gets generated, this may take up to 15 seconds."
},
"normal": {
"primary": "PDF",
@ -264,14 +265,15 @@
"button": "GitHub Issues",
"heading": "Bugs? Feature Requests?"
},
"docs": "Documentation",
"donate": {
"body": "If you liked using Reactive Resume, please consider donating as much as you can to the cause of keeping the app up and running, without ads and free forever.",
"button": "Buy me a coffee",
"heading": "Donate to Reactive Resume"
},
"github": "Source Code",
"docs": "Documentation",
"heading": "Links"
"heading": "Links",
"reddit": "Reddit"
},
"settings": {
"global": {
@ -290,14 +292,14 @@
},
"heading": "Settings",
"page": {
"format": {
"primary": "Paper Size",
"secondary": "Determines the dimensions of your resume pages"
},
"break-line": {
"primary": "Break Line",
"secondary": "Show a line on all pages to mark the height of an A4 page"
},
"format": {
"primary": "Paper Size",
"secondary": "Determines the dimensions of your resume pages"
},
"heading": "Page",
"orientation": {
"disabled": "Has no effect when there is only one page",

View File

@ -20,7 +20,7 @@
"toast": {
"error": {
"upload-file-size": "Please upload only files under 2 megabytes.",
"upload-photo-size": "Please upload only photos under 2 megabytes, preferrably square."
"upload-photo-size": "Please upload only photos under 2 megabytes, preferably square."
},
"success": {
"resume-link-copied": "A link to your resume has been copied to your clipboard."

View File

@ -20,23 +20,24 @@
"links": {
"heading": "Links",
"links": {
"docs": "Documentation",
"donate": "Donate",
"github": "Source Code",
"docs": "Documentation",
"privacy": "Privacy Policy",
"reddit": "Reddit",
"service": "Terms of Service"
}
},
"screenshots": {
"heading": "Screenshots"
},
"testimonials": {
"heading": "Testimonials",
"body": "Good or bad, I would love to hear your opinion on Reactive Resume and how the experience has been for you.<br/>Here are some of the messages sent in by users across the world.",
"contact": "You can reach out to me through <1>my email</1> or through the contact form on <3>my website</3>."
},
"summary": {
"body": "Reactive Resume is a free and open source resume builder that's built to make the mundane tasks of creating, updating and sharing your resume as easy as 1, 2, 3. With this app, you can create multiple resumes, share them with recruiters or friends through a unique link and print it as a PDF, all for free, no ads, no tracking, without losing the integrity and privacy of your data.",
"heading": "Summary"
},
"testimonials": {
"body": "Good or bad, I would love to hear your opinion on Reactive Resume and how the experience has been for you.<br/>Here are some of the messages sent in by users across the world.",
"contact": "You can reach out to me through <1>my email</1> or through the contact form on <3>my website</3>.",
"heading": "Testimonials"
}
}

View File

@ -3,6 +3,7 @@
"actions": {
"add": "{{token}} جدید اضافه کنید",
"delete": "حذف {{token}}",
"duplicate": "بخش تکراری",
"edit": "ویرایش {{token}}"
},
"columns": {
@ -80,13 +81,13 @@
"center-artboard": "قرار دادن صفحه در مرکز",
"copy-link": "کپی کردن لینک رزومه",
"export-pdf": "خروجی PDF",
"redo": "دوباره انجام دهید",
"toggle-orientation": "تغییر وضعیت جهت‌گیری صفحه",
"toggle-page-break-line": "تغییر وضعیت خط شکست صفحه",
"toggle-sidebars": "باز/بسته کردن نوار کنار صفحه",
"zoom-in": "بزرگ‌نمایی",
"zoom-out": "کوچک‌نمایی",
"undo": "واگرد",
"redo": "دوباره انجام دهید"
"zoom-in": "بزرگ‌نمایی",
"zoom-out": "کوچک‌نمایی"
}
},
"header": {
@ -114,6 +115,9 @@
"actions": {
"photo-filters": "فیلترهای تصویر"
},
"birthdate": {
"label": "تاریخ تولد"
},
"heading": "موارد پایه",
"headline": {
"label": "سرصفحه"
@ -121,9 +125,6 @@
"name": {
"label": "نام کامل"
},
"birthdate": {
"label": "تاریخ تولد"
},
"photo-filters": {
"effects": {
"border": {
@ -264,14 +265,15 @@
"button": "GitHub Issues",
"heading": "باگ‌ها؟ درخواست ویژگی جدید؟"
},
"docs": "مستندات",
"donate": {
"body": "اگر استفاده از Reactive Resume را دوست داشتید، لطفاً تا جایی که می توانید کمک مالی کنید تا برنامه را بدون تبلیغات و برای همیشه رایگان نگه دارید.",
"button": "برای من یک قهوه بخر",
"heading": "کمک مالی به Reactive Resume"
},
"github": "کد منبع",
"docs": "مستندات",
"heading": "لینک‌ها"
"heading": "لینک‌ها",
"reddit": "ردیت"
},
"settings": {
"global": {
@ -290,14 +292,14 @@
},
"heading": "تنظیمات",
"page": {
"format": {
"primary": "اندازه کاغذ",
"secondary": "ابعاد صفحات رزومه شما را تعیین می کند"
},
"break-line": {
"primary": "خط شکست",
"secondary": "برای مشخص کردن ارتفاع صفحه A4 یک خط در همه صفحات نشان داده شود"
},
"format": {
"primary": "اندازه کاغذ",
"secondary": "ابعاد صفحات رزومه شما را تعیین می کند"
},
"heading": "صفحه",
"orientation": {
"disabled": "زمانی که تنها یک صفحه وجود دارد، تاثیری ندارد",

View File

@ -20,23 +20,24 @@
"links": {
"heading": "لینک‌ها",
"links": {
"docs": "مستندات",
"donate": "حمایت مالی",
"github": "کد منبع",
"docs": "مستندات",
"privacy": "حریم خصوصی",
"reddit": "ردیت",
"service": "شرایط سرویس‌دهی"
}
},
"screenshots": {
"heading": "اسکرین‌شات‌ها"
},
"testimonials": {
"heading": "نظرات کاربران",
"body": "خوب یا بد، من دوست دارم نظر شما را در مورد Reactive Resume و اینکه تجربه کار با آن برای شما چگونه بوده است را بدانم.<br/>تعدادی از پیام های ارسال شده توسط کاربران در سراسر جهان را اینجا می‌بینید.",
"contact": "می‌توانید از طریق <1>ایمیل من</1> یا فرم تماس در <3>وب‌سایت من</3> با من در ارتباط باشید."
},
"summary": {
"body": "Reactive Resume یک رزومه ساز رایگان و متن‌باز است که برای ایجاد، به روز رسانی و به اشتراک گذاری رزومه شما به آسانی شمردن ۱، ۲، ۳ ساخته شده است. با این برنامه، می توانید چندین رزومه ایجاد کنید و آنها را با کارفرماها یا دوستان از طریق یک لینک منحصر به فرد و چاپ آن به صورت PDF، همه به صورت رایگان، بدون تبلیغات، بدون ردیابی، بدون از دست دادن امنیت و حریم خصوصی داده های شما، به اشتراک بگذارید.",
"heading": "درباره من"
},
"testimonials": {
"body": "خوب یا بد، من دوست دارم نظر شما را در مورد Reactive Resume و اینکه تجربه کار با آن برای شما چگونه بوده است را بدانم.<br/>تعدادی از پیام های ارسال شده توسط کاربران در سراسر جهان را اینجا می‌بینید.",
"contact": "می‌توانید از طریق <1>ایمیل من</1> یا فرم تماس در <3>وب‌سایت من</3> با من در ارتباط باشید.",
"heading": "نظرات کاربران"
}
}

View File

@ -1,7 +1,7 @@
{
"common": {
"actions": {
"add": "Új hozzáadása {{token}}",
"add": "Új {{token}} hozzáadása",
"delete": "{{token}} törlése",
"edit": "{{token}} szerkesztése"
},
@ -20,7 +20,7 @@
"label": "E-mail cím"
},
"end-date": {
"help-text": "Hagyja üresen ezt a mezőt, ha még mindig tart van",
"help-text": "Hagyja üresen ezt a mezőt, ha még folyamatban van",
"label": "Befejezés dátuma"
},
"keywords": {
@ -51,7 +51,7 @@
"label": "Összegzés"
},
"title": {
"label": "Cím"
"label": "Titulus"
},
"url": {
"label": "Honlap"
@ -63,30 +63,30 @@
"list": {
"actions": {
"delete": "Törlés",
"duplicate": "Duplikálás",
"duplicate": "Másolás",
"edit": "Szerkesztés"
},
"empty-text": "Ez a lista üres."
},
"tooltip": {
"delete-item": "Biztosan törli ezt az elemet? Ez egy visszafordíthatatlan művelet.",
"delete-item": "Biztosan törli ezt az elemet? Ez a művelet nem visszavonható.",
"delete-section": "Szakasz törlése",
"rename-section": "Szakasz átnevezése",
"toggle-visibility": "Láthatóság váltása"
"toggle-visibility": "Láthatóság ki/be"
}
},
"controller": {
"tooltip": {
"center-artboard": "Központi rajztábla",
"copy-link": "Link másolása az önéletrajzba",
"copy-link": "Önéletrajz link másolása",
"export-pdf": "Exportálás PDF-be",
"toggle-orientation": "Oldaltájolás váltása",
"toggle-page-break-line": "Oldaltörés vonal váltása",
"toggle-sidebars": "Az oldalsávok váltása",
"toggle-orientation": "Oldaltájolás",
"toggle-page-break-line": "Oldaltörés vonal ki/be",
"toggle-sidebars": "Oldalsávok ki/be",
"zoom-in": "Nagyítás",
"zoom-out": "Kicsinyítés",
"undo": "Undo",
"redo": "Redo"
"undo": "Visszavonás",
"redo": "Mégis"
}
},
"header": {
@ -96,7 +96,7 @@
"rename": "Átnevezés",
"share-link": "Link megosztása",
"tooltips": {
"delete": "Biztos, hogy törölni szeretné ezt az önéletrajzot? Ez egy visszafordíthatatlan művelet.",
"delete": "Biztos, hogy törölni szeretné ezt az önéletrajzot? Ez a művelet nem visszavonható.",
"share-link": "Az önéletrajz láthatóságát nyilvánosra kell változtatnia, hogy mások számára is látható legyen."
}
}
@ -138,7 +138,7 @@
"heading": "Alak"
},
"size": {
"heading": "Méret (px-ben)"
"heading": "Méret (pixel)"
}
},
"photo-upload": {
@ -189,7 +189,7 @@
"label": "Irányítószám"
},
"region": {
"label": "Vidék"
"label": "Régió"
}
},
"profiles": {
@ -248,7 +248,7 @@
},
"normal": {
"primary": "PDF",
"secondary": "Töltse le önéletrajzának PDF formátumát, amelyet kinyomtathat és elküldhet álmai munkahelyére. Ez a fájl nem importálható vissza további szerkesztéshez."
"secondary": "Töltse le önéletrajzát PDF formátumban, amelyet kinyomtathat és elküldhet álmai munkahelyére. Ez a fájl nem importálható vissza további szerkesztéshez."
}
}
},
@ -265,7 +265,7 @@
"heading": "Hibák? Funkciókérés?"
},
"donate": {
"body": "Ha tetszett a Reactive Resume, kérjük, fontolja meg, hogy amennyit csak tud, adományozzon arra, hogy az alkalmazás folyamatosan működjön, hirdetések nélkül és örökké ingyenesen.",
"body": "Ha elégedett a Reactive Resume alkalmazással, kérjük, fontolja meg, hogy tetszőleges összeggel támogassa munkánkat, hogy továbbra is ingyenes és hirdetésmentes lehessen.",
"button": "Vegyél nekem egy kávét",
"heading": "Adományozzon a Reactive Resume-nak"
},
@ -277,12 +277,12 @@
"global": {
"date": {
"primary": "Dátum",
"secondary": "Az alkalmazásban használható dátumformátum"
"secondary": "Az alkalmazásban használt dátumformátum"
},
"heading": "Globális",
"language": {
"primary": "Nyelv",
"secondary": "Az alkalmazásban használható megjelenítési nyelv"
"secondary": "Az alkalmazásban használt megjelenítési nyelv"
},
"theme": {
"primary": "Téma"
@ -292,7 +292,7 @@
"page": {
"format": {
"primary": "Papírméret",
"secondary": "Meghatározza az önéletrajzi oldalak méreteit"
"secondary": "Meghatározza az önéletrajz oldalméreteit"
},
"break-line": {
"primary": "Törésvonal",
@ -301,7 +301,7 @@
"heading": "oldal",
"orientation": {
"disabled": "Nincs hatása, ha csak egy oldal van",
"primary": "Irányultság",
"primary": "Tájolás",
"secondary": "Az oldalak vízszintes vagy függőleges megjelenítése"
}
},
@ -320,7 +320,7 @@
"sharing": {
"heading": "Megosztás",
"short-url": {
"label": "Rövid URL-t részesítsen előnyben"
"label": "Rövid URL előnyben részesítése"
},
"visibility": {
"subtitle": "A link birtokában bárki megtekintheti önéletrajzát",
@ -336,7 +336,7 @@
"label": "Háttér"
},
"primary": {
"label": "Elsődleges"
"label": "Elsődleges "
},
"text": {
"label": "Szöveg"
@ -356,7 +356,7 @@
"heading": "Tipográfia",
"widgets": {
"body": {
"label": "Test"
"label": "Szövegtörzs"
},
"headings": {
"label": "Címsorok"

View File

@ -6,14 +6,14 @@
}
},
"footer": {
"credit": "<1>Amruth Pillai szenvedélyes projektje</1>",
"credit": "<1>Amruth Pillai hobbi projektje</1>",
"license": "A közösség által, a közösségért."
},
"markdown": {
"help-text": "Ez a szakasz támogatja a <1>markdown</1> formázást."
},
"date": {
"present": "Ajándék"
"present": "Jelenleg is"
},
"subtitle": "Ingyenes és nyílt forráskódú önéletrajzkészítő.",
"title": "Reactive Resume",

View File

@ -4,18 +4,18 @@
"title": "Új önéletrajz létrehozása"
},
"import-external": {
"subtitle": "LinkedIn, JSON önéletrajz, reaktív önéletrajz",
"subtitle": "LinkedIn, JSON önéletrajz, Reactive Resume",
"title": "Importálás külső forrásokból"
},
"resume": {
"menu": {
"delete": "Törlés",
"duplicate": "Másolat",
"open": "Nyisd ki",
"open": "Megnyitás",
"rename": "Átnevezés",
"share-link": "Link megosztása",
"tooltips": {
"delete": "Biztos, hogy törölni szeretné ezt az önéletrajzot? Ez egy visszafordíthatatlan művelet.",
"delete": "Biztos, hogy törölni szeretné ezt az önéletrajzot? Ez a művelet nem visszavonható.",
"share-link": "Az önéletrajz láthatóságát nyilvánosra kell változtatnia, hogy mások számára is látható legyen."
}
},

View File

@ -1,6 +1,6 @@
{
"actions": {
"app": "Lépjen az App",
"app": "Alkalmazás indítása",
"login": "Belépés",
"logout": "Kijelentkezés",
"register": "Regisztráció"
@ -8,7 +8,7 @@
"features": {
"heading": "Jellemzők",
"list": {
"ads": "Nincs reklám",
"ads": "Reklámmentes",
"export": "Exportálja önéletrajzát JSON vagy PDF formátumba",
"free": "Ingyenes, örökre",
"import": "Adatok importálása a LinkedInből, JSON Resume",
@ -20,11 +20,11 @@
"links": {
"heading": "Linkek",
"links": {
"donate": "Adományoz",
"donate": "Adományozás",
"github": "Forráskód",
"docs": "Dokumentáció",
"privacy": "Adatvédelmi irányelvek",
"service": "Szolgáltatási feltételek"
"service": "Felhasználói feltételek"
}
},
"screenshots": {

View File

@ -2,7 +2,7 @@
"auth": {
"forgot-password": {
"actions": {
"send-email": "Jelszó visszaállítása e-mail küldése"
"send-email": "Jelszó helyreállító e-mail küldése"
},
"body": "Csak adja meg a helyreállítani kívánt fiókhoz társított e-mail címet.",
"form": {
@ -27,14 +27,14 @@
"label": "Felhasználónév"
}
},
"heading": "Jelentkezz be a fiókodba",
"heading": "Jelentkezzen be a fiókba",
"recover-text": "Ha elfelejtette jelszavát, <1>visszaállíthatja fiókját</1> itt.",
"register-text": "Ha nem rendelkezik fiókkal, <1>létrehozhat egy fiókot</1> itt."
},
"register": {
"actions": {
"register": "Regisztráció",
"google": "Regisztráljon a Google-nál"
"google": "Regisztráljon Google fiókkal"
},
"body": "Kérjük, adja meg személyes adatait fiók létrehozásához.",
"form": {
@ -87,7 +87,7 @@
"label": "Nyilvánosan elérhető?"
},
"slug": {
"label": "Meztelen csiga"
"label": "Saját URL"
}
},
"heading": "Új önéletrajz létrehozása"
@ -126,7 +126,7 @@
"label": "Név"
},
"slug": {
"label": "Meztelen csiga"
"label": "Saját URL"
}
},
"heading": "Nevezze át önéletrajzát"

View File

@ -14,7 +14,7 @@
"label": "Tanggal"
},
"description": {
"label": "Keterangan"
"label": "Deskripsi"
},
"email": {
"label": "Alamat Email"
@ -130,7 +130,7 @@
"label": "Batas"
},
"grayscale": {
"label": "Grayscale"
"label": "Tingkat keabuan"
},
"heading": "Efek"
},
@ -138,7 +138,7 @@
"heading": "Bentuk"
},
"size": {
"heading": "Besar (dalam px)"
"heading": "Ukuran (dalam px)"
}
},
"photo-upload": {
@ -158,7 +158,7 @@
"education": {
"form": {
"area-study": {
"label": "Area belajar"
"label": "Bidang Studi"
},
"courses": {
"label": "Kursus"
@ -170,7 +170,7 @@
"label": "Tingkatan"
},
"institution": {
"label": "Lembaga"
"label": "Institusi"
}
}
},

View File

@ -267,7 +267,7 @@
"donate": {
"body": "Als Je Reactive Resume graag gebruikt, kun je overwegen zoveel mogelijk te doneren om de app in de lucht te houden, zonder advertenties en voor altijd gratis.",
"button": "Betaal me een koffie",
"heading": "Doneer aan Reactiv Resume"
"heading": "Doneer aan Reactive Resume"
},
"github": "Broncode",
"docs": "Documentatie",

View File

@ -2,7 +2,7 @@
"avatar": {
"menu": {
"greeting": "Hallo",
"logout": "Afmelden"
"logout": "Uitloggen"
}
},
"footer": {
@ -20,7 +20,7 @@
"toast": {
"error": {
"upload-file-size": "Upload alleen bestanden onder de 2 megabytes.",
"upload-photo-size": "Upload alleen foto's onder de 2 megabytes, bij voorkeur vierkante."
"upload-photo-size": "Upload alleen foto's onder de 2 megabytes, bij voorkeur vierkant."
},
"success": {
"resume-link-copied": "Een link naar jouw CV is naar het klembord gekopieerd."

View File

@ -18,8 +18,8 @@ export type LoginWithGoogleParams = {
export type RegisterParams = {
name: string;
username: string;
email: string;
username: string;
password: string;
};

View File

@ -1,5 +1,5 @@
import env from '@beam-australia/react-env';
import _axios from 'axios';
import _axios, { RawAxiosRequestHeaders } from 'axios';
import Router from 'next/router';
import { logout } from '@/store/auth/authSlice';
@ -19,9 +19,8 @@ const axios = _axios.create({ baseURL });
axios.interceptors.request.use((config) => {
const { accessToken } = store.getState().auth;
// @ts-ignore
config.headers = {
...config.headers,
...(config.headers as RawAxiosRequestHeaders),
Authorization: `Bearer ${accessToken}`,
};

View File

@ -3,7 +3,12 @@ import axios from './axios';
export type PrintResumeAsPdfParams = {
username: string;
slug: string;
lastUpdated: string;
};
export const printResumeAsPdf = (printResumeAsPdfParams: PrintResumeAsPdfParams): Promise<string> =>
axios.get(`/printer/${printResumeAsPdfParams.username}/${printResumeAsPdfParams.slug}`).then((res) => res.data);
axios
.get(
`/printer/${printResumeAsPdfParams.username}/${printResumeAsPdfParams.slug}?lastUpdated=${printResumeAsPdfParams.lastUpdated}`
)
.then((res) => res.data);

View File

@ -1,3 +1,4 @@
import env from '@beam-australia/react-env';
import { Resume } from '@reactive-resume/schema';
import { AxiosResponse } from 'axios';
@ -62,9 +63,10 @@ export const fetchResumeByIdentifier = async ({
options = { secretKey: '' },
}: FetchResumeByIdentifierParams) => {
if (!isBrowser) {
const serverUrl = env('SERVER_URL');
const secretKey = options.secretKey;
return axios.get<Resume>(`/resume/${username}/${slug}`, { params: { secretKey } }).then((res) => res.data);
return fetch(`${serverUrl}/resume/${username}/${slug}?secretKey=${secretKey}`).then((response) => response.json());
}
return axios.get<Resume>(`/resume/${username}/${slug}`).then((res) => res.data);

View File

@ -34,7 +34,7 @@ const store = configureStore({
},
});
sagaMiddleware.run(syncSaga);
sagaMiddleware.run(() => syncSaga(store.dispatch));
export const persistor = persistStore(store);

View File

@ -9,6 +9,7 @@ export type ModalName =
| 'dashboard.import-external'
| 'dashboard.rename-resume'
| 'builder.sections.profile'
| 'builder.sections.work'
| `builder.sections.${string}`;
export type ModalState = {

View File

@ -1,4 +1,4 @@
import { ListItem, Profile, Resume, Section } from '@reactive-resume/schema';
import { ListItem, Profile, Resume, Section, SectionType } from '@reactive-resume/schema';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get';
@ -7,6 +7,8 @@ import pick from 'lodash/pick';
import set from 'lodash/set';
import { v4 as uuidv4 } from 'uuid';
import { getSectionsByType } from '@/config/sections';
type SetResumeStatePayload = { path: string; value: unknown };
type AddItemPayload = { path: string; value: ListItem };
@ -17,7 +19,7 @@ type DuplicateItemPayload = { path: string; value: ListItem };
type DeleteItemPayload = { path: string; value: ListItem };
type AddSectionPayload = { value: Section };
type AddSectionPayload = { value: Section; type: SectionType };
type DeleteSectionPayload = { path: string };
@ -38,7 +40,7 @@ export const resumeSlice = createSlice({
addItem: (state: Resume, action: PayloadAction<AddItemPayload>) => {
const { path, value } = action.payload;
const id = uuidv4();
const list = get(state, path, []);
const list: ListItem[] = get(state, path, []);
const item = merge(value, { id });
list.push(item);
@ -80,6 +82,15 @@ export const resumeSlice = createSlice({
state.sections[id] = value;
state.metadata.layout[0][0].push(id);
},
duplicateSection: (state: Resume, action: PayloadAction<AddSectionPayload>) => {
const { value, type } = action.payload;
const id = getSectionsByType(state.sections, type).length + 1;
value.name = value.name + '-' + id;
state.sections[`${type}-${id}`] = value;
state.metadata.layout[0][0].push(`${type}-${id}`);
},
deleteSection: (state: Resume, action: PayloadAction<DeleteSectionPayload>) => {
const { path } = action.payload;
const id = path.split('.')[1];
@ -119,6 +130,7 @@ export const {
duplicateItem,
deleteItem,
addSection,
duplicateSection,
deleteSection,
addPage,
deletePage,

View File

@ -3,7 +3,7 @@ import debounce from 'lodash/debounce';
import { select, takeLatest } from 'redux-saga/effects';
import { updateResume } from '@/services/resume';
import { RootState } from '@/store/index';
import { AppDispatch, RootState } from '@/store/index';
import {
addItem,
@ -12,23 +12,26 @@ import {
deleteSection,
duplicateItem,
editItem,
setResume,
setResumeState,
} from '../resume/resumeSlice';
const DEBOUNCE_WAIT = 1000;
const debouncedSync = debounce((resume: Resume) => updateResume(resume), DEBOUNCE_WAIT);
const debouncedSync = debounce(
(resume: Resume, dispatch: AppDispatch) => updateResume(resume).then((resume) => dispatch(setResume(resume))),
DEBOUNCE_WAIT
);
function* handleSync() {
function* handleSync(dispatch: AppDispatch) {
const resume: Resume = yield select((state: RootState) => state.resume.present);
debouncedSync(resume);
debouncedSync(resume, dispatch);
}
function* syncSaga() {
yield takeLatest(
[setResumeState, addItem, editItem, duplicateItem, deleteItem, addSection, deleteSection],
handleSync
function* syncSaga(dispatch: AppDispatch) {
yield takeLatest([setResumeState, addItem, editItem, duplicateItem, deleteItem, addSection, deleteSection], () =>
handleSync(dispatch)
);
}

View File

@ -2,6 +2,9 @@
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
// KaTeX (for remark-math)
@import url('https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css');
// Tailwind CSS
@tailwind base;
@tailwind components;
@ -22,11 +25,23 @@
}
p {
@apply leading-relaxed;
@apply leading-normal;
}
a {
@apply cursor-pointer font-medium hover:underline;
@apply cursor-pointer font-medium;
}
.markdown {
@apply prose prose-sm leading-normal max-w-none prose-ul:p-0 prose-ul:my-0 prose-p:my-0;
ul li {
@apply ml-4 list-outside;
}
.footnotes p {
@apply inline;
}
}
}

View File

@ -52,7 +52,7 @@
@apply grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6;
.image {
@apply relative h-64 rounded hover:opacity-75;
@apply relative h-48 rounded hover:opacity-75;
@apply border-2 dark:border-neutral-700;
}
}

View File

@ -17,7 +17,7 @@ const Castform: React.FC<PageProps> = ({ page }) => {
const isFirstPage = useMemo(() => page === 0, [page]);
const layout: string[][] = useAppSelector((state) => state.resume.present.metadata.layout[page]);
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);

View File

@ -6,7 +6,7 @@ import { useMemo } from 'react';
import { useAppSelector } from '@/store/hooks';
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
const darkerPrimary = useMemo(() => darken(theme.primary, 0.2), [theme.primary]);
return (

View File

@ -19,7 +19,7 @@ export const MastheadSidebar: React.FC = () => {
const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector(
(state) => state.resume.present.basics
);
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);
@ -35,34 +35,55 @@ export const MastheadSidebar: React.FC = () => {
/>
)}
<div>
<div className={clsx({ invert: contrast === 'light' })}>
<h1 className="mb-1">{name}</h1>
<p className="opacity-75">{headline}</p>
</div>
<div className={clsx('flex flex-col gap-2.5', css(`svg { color: ${color} }`))}>
<DataDisplay icon={<Room />} className="!gap-2 text-xs">
<DataDisplay icon={<Room />} className="!gap-2 text-xs" textClassName={clsx({ invert: contrast === 'light' })}>
{formatLocation(location)}
</DataDisplay>
<DataDisplay icon={<Cake />} className="!gap-2 text-xs">
<DataDisplay icon={<Cake />} className="!gap-2 text-xs" textClassName={clsx({ invert: contrast === 'light' })}>
{formatDateString(birthdate, dateFormat)}
</DataDisplay>
<DataDisplay icon={<Email />} className="!gap-2 text-xs" link={`mailto:${email}`}>
<DataDisplay
icon={<Email />}
className="!gap-2 text-xs"
link={`mailto:${email}`}
textClassName={clsx({ invert: contrast === 'light' })}
>
{email}
</DataDisplay>
<DataDisplay icon={<Phone />} className="!gap-2 text-xs" link={`tel:${phone}`}>
<DataDisplay
icon={<Phone />}
className="!gap-2 text-xs"
link={`tel:${phone}`}
textClassName={clsx({ invert: contrast === 'light' })}
>
{phone}
</DataDisplay>
<DataDisplay icon={<Public />} link={website && addHttp(website)} className="!gap-2 text-xs">
<DataDisplay
icon={<Public />}
link={website && addHttp(website)}
className="!gap-2 text-xs"
textClassName={clsx({ invert: contrast === 'light' })}
>
{website}
</DataDisplay>
{profiles.map(({ id, username, network, url }) => (
<DataDisplay key={id} icon={getProfileIcon(network)} link={url && addHttp(url)} className="!gap-2 text-xs">
<DataDisplay
key={id}
icon={getProfileIcon(network)}
link={url && addHttp(url)}
className="!gap-2 text-xs"
textClassName={clsx({ invert: contrast === 'light' })}
>
{username}
</DataDisplay>
))}

View File

@ -1,5 +1,6 @@
import { Email, Link, Phone } from '@mui/icons-material';
import { ListItem, Section as SectionType } from '@reactive-resume/schema';
import clsx from 'clsx';
import get from 'lodash/get';
import isArray from 'lodash/isArray';
import isEmpty from 'lodash/isEmpty';
@ -21,10 +22,12 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline',
keywordsPath = 'keywords',
}) => {
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {} as SectionType));
const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const layout: string[][][] = useAppSelector((state) => get(state.resume.present, 'metadata.layout'));
const sectionId = useMemo(() => section.id || path.replace('sections.', ''), [path, section]);
const isSidebarSection = useMemo(() => layout.some((row) => row[1].includes(sectionId)), [layout, sectionId]);
if (!section.visible) return null;
@ -35,7 +38,7 @@ const Section: React.FC<SectionProps> = ({
<Heading>{section.name}</Heading>
<div
className="grid items-start gap-4"
className={clsx('grid items-start gap-4', { invert: isSidebarSection })}
style={{ gridTemplateColumns: `repeat(${section.columns}, minmax(0, 1fr))` }}
>
{section.items.map((item: ListItem) => {
@ -44,13 +47,13 @@ const Section: React.FC<SectionProps> = ({
subtitle = parseListItemPath(item, subtitlePath),
headline = parseListItemPath(item, headlinePath),
keywords: string[] = get(item, keywordsPath),
url: string = get(item, 'url'),
summary: string = get(item, 'summary'),
level: string = get(item, 'level'),
levelNum: number = get(item, 'levelNum'),
phone: string = get(item, 'phone'),
email: string = get(item, 'email'),
date = formatDateString(get(item, 'date'), dateFormat);
url: string = get(item, 'url', ''),
level: string = get(item, 'level', ''),
phone: string = get(item, 'phone', ''),
email: string = get(item, 'email', ''),
summary: string = get(item, 'summary', ''),
levelNum: number = get(item, 'levelNum', 0),
date = formatDateString(get(item, 'date', ''), dateFormat);
return (
<div key={id} id={id} className="grid gap-1">
@ -76,8 +79,13 @@ const Section: React.FC<SectionProps> = ({
key={index}
className="mr-2 h-3 w-3 rounded-full border"
style={{
borderColor: 'var(--primary-color)',
backgroundColor: levelNum / (10 / 5) > index ? 'var(--primary-color)' : '',
borderColor: isSidebarSection ? 'var(--text-color)' : 'var(--primary-color)',
backgroundColor:
levelNum / (10 / 5) > index
? isSidebarSection
? 'var(--text-color)'
: 'var(--primary-color)'
: '',
}}
/>
))}
@ -94,7 +102,7 @@ const Section: React.FC<SectionProps> = ({
</DataDisplay>
)}
{keywords && <div>{keywords.join(', ')}</div>}
{keywords && <span>{keywords.join(', ')}</span>}
{(phone || email) && (
<div className="grid gap-1">

View File

@ -18,7 +18,7 @@ const Gengar: React.FC<PageProps> = ({ page }) => {
const isFirstPage = useMemo(() => page === 0, [page]);
const layout: string[][] = useAppSelector((state) => state.resume.present.metadata.layout[page]);
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
const backgroundColor: string = useMemo(() => alpha(theme.primary, 0.15), [theme.primary]);
const color = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);

View File

@ -4,7 +4,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks';
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
return (
<h3

View File

@ -20,7 +20,7 @@ export const MastheadSidebar: React.FC = () => {
const { name, headline, photo, email, phone, birthdate, website, location, profiles } = useAppSelector(
(state) => state.resume.present.basics
);
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
const iconColor = useMemo(() => (contrast === 'dark' ? theme.text : theme.background), [theme, contrast]);
@ -36,34 +36,55 @@ export const MastheadSidebar: React.FC = () => {
/>
)}
<div>
<div className={clsx({ invert: contrast === 'light' })}>
<h1 className="mb-1">{name}</h1>
<p className="opacity-75">{headline}</p>
</div>
<div className={clsx('flex flex-col gap-2.5', css(`svg { color: ${iconColor} }`))}>
<DataDisplay icon={<Room />} className="!gap-2 text-xs">
<DataDisplay icon={<Room />} className="!gap-2 text-xs" textClassName={clsx({ invert: contrast === 'light' })}>
{formatLocation(location)}
</DataDisplay>
<DataDisplay icon={<Cake />} className="!gap-2 text-xs">
<DataDisplay icon={<Cake />} className="!gap-2 text-xs" textClassName={clsx({ invert: contrast === 'light' })}>
{formatDateString(birthdate, dateFormat)}
</DataDisplay>
<DataDisplay icon={<Email />} className="!gap-2 text-xs" link={`mailto:${email}`}>
<DataDisplay
icon={<Email />}
className="!gap-2 text-xs"
link={`mailto:${email}`}
textClassName={clsx({ invert: contrast === 'light' })}
>
{email}
</DataDisplay>
<DataDisplay icon={<Phone />} className="!gap-2 text-xs" link={`tel:${phone}`}>
<DataDisplay
icon={<Phone />}
className="!gap-2 text-xs"
link={`tel:${phone}`}
textClassName={clsx({ invert: contrast === 'light' })}
>
{phone}
</DataDisplay>
<DataDisplay icon={<Public />} link={website && addHttp(website)} className="!gap-2 text-xs">
<DataDisplay
icon={<Public />}
link={website && addHttp(website)}
className="!gap-2 text-xs"
textClassName={clsx({ invert: contrast === 'light' })}
>
{website}
</DataDisplay>
{profiles.map(({ id, username, network, url }) => (
<DataDisplay key={id} icon={getProfileIcon(network)} link={url && addHttp(url)} className="!gap-2 text-xs">
<DataDisplay
key={id}
icon={getProfileIcon(network)}
link={url && addHttp(url)}
className="!gap-2 text-xs"
textClassName={clsx({ invert: contrast === 'light' })}
>
{username}
</DataDisplay>
))}

View File

@ -10,7 +10,7 @@ import { useAppSelector } from '@/store/hooks';
import { SectionProps } from '@/templates/sectionMap';
import DataDisplay from '@/templates/shared/DataDisplay';
import { formatDateString } from '@/utils/date';
import { addHttp, parseListItemPath } from '@/utils/template';
import { parseListItemPath } from '@/utils/template';
import Heading from './Heading';
@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline',
keywordsPath = 'keywords',
}) => {
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {} as SectionType));
const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
@ -45,13 +45,13 @@ const Section: React.FC<SectionProps> = ({
subtitle = parseListItemPath(item, subtitlePath),
headline = parseListItemPath(item, headlinePath),
keywords: string[] = get(item, keywordsPath),
url: string = get(item, 'url'),
summary: string = get(item, 'summary'),
level: string = get(item, 'level'),
levelNum: number = get(item, 'levelNum'),
phone: string = get(item, 'phone'),
email: string = get(item, 'email'),
date = formatDateString(get(item, 'date'), dateFormat);
url: string = get(item, 'url', ''),
level: string = get(item, 'level', ''),
phone: string = get(item, 'phone', ''),
email: string = get(item, 'email', ''),
summary: string = get(item, 'summary', ''),
levelNum: number = get(item, 'levelNum', 0),
date = formatDateString(get(item, 'date', ''), dateFormat);
return (
<div key={id} id={id} className="grid gap-1">
@ -75,7 +75,7 @@ const Section: React.FC<SectionProps> = ({
{Array.from(Array(8).keys()).map((_, index) => (
<div
key={index}
className="mr-1 h-2 w-4 rounded-sm border"
className="mr-1 h-2 w-full max-w-[1rem] rounded-sm border"
style={{
borderColor: primaryColor,
backgroundColor: levelNum / (10 / 8) > index ? primaryColor : '',
@ -90,7 +90,7 @@ const Section: React.FC<SectionProps> = ({
{summary && <Markdown>{summary}</Markdown>}
{url && (
<DataDisplay icon={<Link />} link={addHttp(url)}>
<DataDisplay icon={<Link />} link={url}>
{url}
</DataDisplay>
)}

View File

@ -13,7 +13,7 @@ type Props = {
};
const BadgeDisplay: React.FC<Props> = ({ items }) => {
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
if (!isArray(items) || isEmpty(items)) return null;
@ -21,15 +21,8 @@ const BadgeDisplay: React.FC<Props> = ({ items }) => {
return (
<ul className="mt-1 flex flex-wrap gap-2 text-xs">
{items.map((item) => (
<li
key={item}
className="rounded-sm px-2 py-0.5"
style={{
color: contrast === 'dark' ? theme.text : theme.background,
backgroundColor: alpha(theme.primary, 0.75),
}}
>
{item}
<li key={item} className="rounded-sm px-2 py-0.5" style={{ backgroundColor: alpha(theme.primary, 0.75) }}>
<span style={{ color: contrast === 'dark' ? theme.text : theme.background }}>{item}</span>
</li>
))}
</ul>

View File

@ -4,7 +4,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks';
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
return (
<h3

View File

@ -22,7 +22,7 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline',
keywordsPath = 'keywords',
}) => {
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {} as SectionType));
const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
@ -46,13 +46,13 @@ const Section: React.FC<SectionProps> = ({
subtitle = parseListItemPath(item, subtitlePath),
headline = parseListItemPath(item, headlinePath),
keywords: string[] = get(item, keywordsPath),
url: string = get(item, 'url'),
summary: string = get(item, 'summary'),
level: string = get(item, 'level'),
levelNum: number = get(item, 'levelNum'),
phone: string = get(item, 'phone'),
email: string = get(item, 'email'),
date = formatDateString(get(item, 'date'), dateFormat);
url: string = get(item, 'url', ''),
level: string = get(item, 'level', ''),
phone: string = get(item, 'phone', ''),
email: string = get(item, 'email', ''),
summary: string = get(item, 'summary', ''),
levelNum: number = get(item, 'levelNum', 0),
date = formatDateString(get(item, 'date', ''), dateFormat);
return (
<div key={id} id={id} className="grid gap-1">

View File

@ -12,7 +12,7 @@ type Props = {
};
const BadgeDisplay: React.FC<Props> = ({ items }) => {
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
if (!isArray(items) || isEmpty(items)) return null;
@ -20,15 +20,8 @@ const BadgeDisplay: React.FC<Props> = ({ items }) => {
return (
<ul className="my-1 flex flex-wrap items-start justify-center gap-1.5">
{items.map((item) => (
<li
key={item}
className="rounded-lg px-2 py-0.5 text-xs"
style={{
color: contrast === 'dark' ? theme.text : theme.background,
backgroundColor: theme.primary,
}}
>
{item}
<li key={item} className="rounded-lg px-2 py-0.5 text-xs" style={{ backgroundColor: theme.primary }}>
<span style={{ color: contrast === 'dark' ? theme.text : theme.background }}>{item}</span>
</li>
))}
</ul>

View File

@ -1,4 +1,4 @@
import { Email, Phone } from '@mui/icons-material';
import { Email, Link, Phone } from '@mui/icons-material';
import { ListItem, Section as SectionType } from '@reactive-resume/schema';
import get from 'lodash/get';
import isArray from 'lodash/isArray';
@ -8,8 +8,9 @@ import { useMemo } from 'react';
import Markdown from '@/components/shared/Markdown';
import { useAppSelector } from '@/store/hooks';
import { SectionProps } from '@/templates/sectionMap';
import DataDisplay from '@/templates/shared/DataDisplay';
import { formatDateString } from '@/utils/date';
import { addHttp, parseListItemPath } from '@/utils/template';
import { parseListItemPath } from '@/utils/template';
import BadgeDisplay from './BadgeDisplay';
import Heading from './Heading';
@ -21,7 +22,7 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline',
keywordsPath = 'keywords',
}) => {
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {} as SectionType));
const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
@ -45,13 +46,13 @@ const Section: React.FC<SectionProps> = ({
subtitle = parseListItemPath(item, subtitlePath),
headline = parseListItemPath(item, headlinePath),
keywords: string[] = get(item, keywordsPath),
url: string = get(item, 'url'),
summary: string = get(item, 'summary'),
level: string = get(item, 'level'),
levelNum: number = get(item, 'levelNum'),
phone: string = get(item, 'phone'),
email: string = get(item, 'email'),
date = formatDateString(get(item, 'date'), dateFormat);
url: string = get(item, 'url', ''),
level: string = get(item, 'level', ''),
phone: string = get(item, 'phone', ''),
email: string = get(item, 'email', ''),
summary: string = get(item, 'summary', ''),
levelNum: number = get(item, 'levelNum', 0),
date = formatDateString(get(item, 'date', ''), dateFormat);
return (
<div key={id} id={id} className="grid gap-1">
@ -87,9 +88,9 @@ const Section: React.FC<SectionProps> = ({
{url && (
<div className="inline-flex justify-center">
<a href={addHttp(url)} target="_blank" rel="noreferrer">
<DataDisplay link={url} icon={<Link />}>
{url}
</a>
</DataDisplay>
</div>
)}

View File

@ -1,5 +1,5 @@
.container {
@apply grid grid-cols-2 gap-4 px-6 py-4;
@apply grid grid-cols-2 gap-4 px-6 py-4 items-start;
.main {
@apply grid gap-4;

View File

@ -4,7 +4,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks';
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
return (
<h2

View File

@ -16,7 +16,7 @@ const Masthead: React.FC = () => {
const { name, photo, headline, summary, email, phone, birthdate, website, location, profiles } = useAppSelector(
(state) => state.resume.present.basics
);
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
return (
<div>

View File

@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline',
keywordsPath = 'keywords',
}) => {
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {} as SectionType));
const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
@ -45,13 +45,13 @@ const Section: React.FC<SectionProps> = ({
subtitle = parseListItemPath(item, subtitlePath),
headline = parseListItemPath(item, headlinePath),
keywords: string[] = get(item, keywordsPath),
url: string = get(item, 'url'),
summary: string = get(item, 'summary'),
level: string = get(item, 'level'),
levelNum: number = get(item, 'levelNum'),
phone: string = get(item, 'phone'),
email: string = get(item, 'email'),
date = formatDateString(get(item, 'date'), dateFormat);
url: string = get(item, 'url', ''),
level: string = get(item, 'level', ''),
phone: string = get(item, 'phone', ''),
email: string = get(item, 'email', ''),
summary: string = get(item, 'summary', ''),
levelNum: number = get(item, 'levelNum', 0),
date = formatDateString(get(item, 'date', ''), dateFormat);
return (
<div key={id} className="mb-2 grid gap-1">

View File

@ -4,7 +4,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks';
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
return (
<h4 className="mb-2 font-bold uppercase" style={{ color: theme.primary }}>

View File

@ -10,7 +10,7 @@ import { useAppSelector } from '@/store/hooks';
import { SectionProps } from '@/templates/sectionMap';
import DataDisplay from '@/templates/shared/DataDisplay';
import { formatDateString } from '@/utils/date';
import { addHttp, parseListItemPath } from '@/utils/template';
import { parseListItemPath } from '@/utils/template';
import Heading from './Heading';
@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline',
keywordsPath = 'keywords',
}) => {
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {} as SectionType));
const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
@ -45,13 +45,13 @@ const Section: React.FC<SectionProps> = ({
subtitle = parseListItemPath(item, subtitlePath),
headline = parseListItemPath(item, headlinePath),
keywords: string[] = get(item, keywordsPath),
url: string = get(item, 'url'),
summary: string = get(item, 'summary'),
level: string = get(item, 'level'),
levelNum: number = get(item, 'levelNum'),
phone: string = get(item, 'phone'),
email: string = get(item, 'email'),
date = formatDateString(get(item, 'date'), dateFormat);
url: string = get(item, 'url', ''),
level: string = get(item, 'level', ''),
phone: string = get(item, 'phone', ''),
email: string = get(item, 'email', ''),
summary: string = get(item, 'summary', ''),
levelNum: number = get(item, 'levelNum', 0),
date = formatDateString(get(item, 'date', ''), dateFormat);
return (
<div key={id} id={id} className="grid gap-1">
@ -90,7 +90,7 @@ const Section: React.FC<SectionProps> = ({
{summary && <Markdown>{summary}</Markdown>}
{url && (
<DataDisplay icon={<Link />} link={addHttp(url)} className="text-xs">
<DataDisplay icon={<Link />} link={url} className="text-xs">
{url}
</DataDisplay>
)}

View File

@ -4,7 +4,7 @@ import get from 'lodash/get';
import { useAppSelector } from '@/store/hooks';
const Heading: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
return (
<h3

View File

@ -1,5 +1,6 @@
import { Cake, Email, Phone, Public, Room } from '@mui/icons-material';
import { ThemeConfig } from '@reactive-resume/schema';
import clsx from 'clsx';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { useMemo } from 'react';
@ -62,7 +63,7 @@ export const MastheadSidebar: React.FC = () => {
};
export const MastheadMain: React.FC = () => {
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {}));
const theme: ThemeConfig = useAppSelector((state) => get(state.resume.present, 'metadata.theme', {} as ThemeConfig));
const contrast = useMemo(() => getContrastColor(theme.primary), [theme.primary]);
const { name, summary, headline } = useAppSelector((state) => state.resume.present.basics);
@ -72,14 +73,14 @@ export const MastheadMain: React.FC = () => {
className="grid gap-2 p-4"
style={{ color: contrast === 'dark' ? theme.text : theme.background, backgroundColor: theme.primary }}
>
<div>
<div className={clsx({ invert: contrast === 'light' })}>
<h1>{name}</h1>
<p className="opacity-75">{headline}</p>
</div>
<hr className="opacity-25" />
<Markdown>{summary}</Markdown>
<Markdown className={clsx({ invert: contrast === 'light' })}>{summary}</Markdown>
</div>
);
};

View File

@ -21,7 +21,7 @@ const Section: React.FC<SectionProps> = ({
headlinePath = 'headline',
keywordsPath = 'keywords',
}) => {
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {}));
const section: SectionType = useAppSelector((state) => get(state.resume.present, path, {} as SectionType));
const dateFormat: string = useAppSelector((state) => get(state.resume.present, 'metadata.date.format'));
const primaryColor: string = useAppSelector((state) => get(state.resume.present, 'metadata.theme.primary'));
@ -45,13 +45,13 @@ const Section: React.FC<SectionProps> = ({
subtitle = parseListItemPath(item, subtitlePath),
headline = parseListItemPath(item, headlinePath),
keywords: string[] = get(item, keywordsPath),
url: string = get(item, 'url'),
summary: string = get(item, 'summary'),
level: string = get(item, 'level'),
levelNum: number = get(item, 'levelNum'),
phone: string = get(item, 'phone'),
email: string = get(item, 'email'),
date = formatDateString(get(item, 'date'), dateFormat);
url: string = get(item, 'url', ''),
level: string = get(item, 'level', ''),
phone: string = get(item, 'phone', ''),
email: string = get(item, 'email', ''),
summary: string = get(item, 'summary', ''),
levelNum: number = get(item, 'levelNum', 0),
date = formatDateString(get(item, 'date', ''), dateFormat);
return (
<div key={id} id={id} className="grid gap-1">

View File

@ -1,3 +1,4 @@
import { find } from 'lodash';
import get from 'lodash/get';
import React from 'react';
import { validate } from 'uuid';
@ -44,11 +45,21 @@ const sectionMap = (Section: React.FC<SectionProps>): Record<string, JSX.Element
});
export const getSectionById = (id: string, Section: React.FC<SectionProps>): JSX.Element => {
// Check if section id is a custom section (an uuid)
if (validate(id)) {
return <Section key={id} path={`sections.${id}`} />;
}
return get(sectionMap(Section), id);
// Check if section id is a predefined seciton in config
const predefinedSection = get(sectionMap(Section), id);
if (predefinedSection) {
return predefinedSection;
}
// Other ways section should be a cloned section
const section = find(sectionMap(Section), (element, key) => id.includes(key));
return React.cloneElement(section!, { path: `sections.${id}` });
};
export default sectionMap;

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