mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-10 04:22:27 +10:00
Compare commits
366 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7902f67f4f | |||
| 57dd110187 | |||
| 829375e87a | |||
| 0a15b4ebc9 | |||
| 2bff3fc20b | |||
| 1e997fe67c | |||
| dbf06455e4 | |||
| 42c7c9ade1 | |||
| 36c19bac3f | |||
| 44a9300aff | |||
| 610b5ba9d4 | |||
| 769e8811cd | |||
| 676fbcafe7 | |||
| 3935ae1e04 | |||
| ef6b765266 | |||
| 647dd6e682 | |||
| 43841e9962 | |||
| e2236c3207 | |||
| 7389d33ee5 | |||
| 4b21eabec9 | |||
| 1815b0fa21 | |||
| 6c4d3cbd56 | |||
| 8c2f3c8504 | |||
| 3aa7a98d9d | |||
| 5519ec898d | |||
| 1cd4c5d733 | |||
| 73d11c323f | |||
| 38812fcf25 | |||
| c22de12f12 | |||
| c94c971599 | |||
| c9a71a5917 | |||
| 8a29387470 | |||
| 592511b090 | |||
| af63fd38d4 | |||
| bf38b1b254 | |||
| 4a1c0079db | |||
| 5b6f6b7621 | |||
| 02587255fe | |||
| 9ef2a84ac2 | |||
| 77b1c5b536 | |||
| bf956fe18c | |||
| 4114f1e1dd | |||
| 668d39fa87 | |||
| 0d88a18757 | |||
| 0630369087 | |||
| 73af4a6859 | |||
| 99ddeb25a9 | |||
| 685aa06778 | |||
| 460abc6f1d | |||
| 04f02157ac | |||
| 828a4a8715 | |||
| 5b3141cd49 | |||
| 779d22101f | |||
| ef240b2110 | |||
| 32bb7354a4 | |||
| 0dcbad1f8a | |||
| a74921b27a | |||
| d4f47423c9 | |||
| 03f9a6543c | |||
| eb89cfcf5d | |||
| c52ef9ecb7 | |||
| c499abbb88 | |||
| 1a7ee88ecd | |||
| 16d19eb70f | |||
| 331346b99c | |||
| 95d265f672 | |||
| 315c7d6328 | |||
| 490e174564 | |||
| b5cde79f8b | |||
| d50f14bb78 | |||
| c13a751c1a | |||
| 5c37fc55d5 | |||
| 48a0f90597 | |||
| 05d3f1f06f | |||
| 4d43f6a642 | |||
| f7363ccdd7 | |||
| 07c91e9ac2 | |||
| cbe08f1d2c | |||
| c2617a8277 | |||
| fe72d2de41 | |||
| 23667e218f | |||
| 977fa72dde | |||
| 5197f954c0 | |||
| 58341e4cd2 | |||
| 1559703567 | |||
| 0a1fd50d07 | |||
| 1c19062c63 | |||
| 25cf594eb9 | |||
| 1c3beee6cd | |||
| 95c3d4c315 | |||
| 85df339e56 | |||
| d61ad44ebc | |||
| ccb1eff749 | |||
| bfb48e3aa7 | |||
| e2e08ad390 | |||
| f0dda06af3 | |||
| 4c4e77e21d | |||
| f364ae8929 | |||
| b52f292d89 | |||
| 8cac7f907c | |||
| a18a60679f | |||
| 5cc6a81b8c | |||
| 6ff212b698 | |||
| 56bcec5196 | |||
| 12019f90e9 | |||
| 7e6e69ed49 | |||
| a09a945e17 | |||
| df714dc8de | |||
| 28b63ef0c7 | |||
| 1b594dac61 | |||
| dd34a30ee0 | |||
| 0af398ceed | |||
| 04abd2cacc | |||
| a037a091e7 | |||
| f3a4c17cb4 | |||
| f06f7ad2e5 | |||
| aab2e5c8a9 | |||
| 4318dbe762 | |||
| ae3ff274ee | |||
| 164403c495 | |||
| 8595c92fb7 | |||
| 8f75f32f88 | |||
| 0d44189a5f | |||
| cd16a6d360 | |||
| 7b795bfaa4 | |||
| 8f78d47661 | |||
| 0b5e5a2ece | |||
| 9eade9514c | |||
| d744e06e96 | |||
| 9657c199d2 | |||
| 2dbe737b73 | |||
| f624699efa | |||
| e46f473754 | |||
| 767f4bf4bc | |||
| 1c5d025c15 | |||
| 8de8d89290 | |||
| 83662122a5 | |||
| 126482a760 | |||
| b04c22a27b | |||
| 63f88a3d1c | |||
| bd519db14f | |||
| a49aa42176 | |||
| 1a382db4d9 | |||
| c68f75dc8c | |||
| c12de0c013 | |||
| 4cafaf306a | |||
| 0238cf18a5 | |||
| 2f6072a7ba | |||
| 55dd2c5925 | |||
| a3e25f87fa | |||
| 9e82ea11c3 | |||
| 62fd63e41f | |||
| b91c175352 | |||
| 898e2314fc | |||
| bca2aa2fe5 | |||
| 427fdb717a | |||
| ee5b0187e2 | |||
| 94d05f33b4 | |||
| 35fe4e2774 | |||
| 317901a4d2 | |||
| 350ffcbc43 | |||
| 2c074a96c8 | |||
| 79f140b2d0 | |||
| 649c655ad5 | |||
| d5284a90d1 | |||
| bd18c53ab8 | |||
| 704c1ab7d4 | |||
| 1dbd7f221e | |||
| e1a47ffbe2 | |||
| 2add629970 | |||
| a48fcd9c97 | |||
| df7b00cb2c | |||
| 27fc939101 | |||
| 7c574d17e4 | |||
| 86a105f5a5 | |||
| 327bcc2b32 | |||
| a6cbd85010 | |||
| 371b820923 | |||
| 1d47fd0267 | |||
| 276fc95bb0 | |||
| 34c8861321 | |||
| 780b782579 | |||
| 9daa99fd5b | |||
| 76b3aa29cf | |||
| 25d4913fab | |||
| 0efeff3a4f | |||
| f56089925e | |||
| 5afae08f20 | |||
| 4bf114dfd6 | |||
| 23a3c2e624 | |||
| 71862f4354 | |||
| 6861c0f0fa | |||
| 9a18e74b90 | |||
| 4dd1b70079 | |||
| f9580fe716 | |||
| 3545f7939f | |||
| 9caad3bc0b | |||
| 5bdb92b1cf | |||
| 87d381fe8e | |||
| ccfb4d3cb0 | |||
| 763074a86c | |||
| 0f46895711 | |||
| aa736af0f5 | |||
| 1d9056f935 | |||
| 9cadd603f3 | |||
| b7b62d7bd0 | |||
| 820e6c90d3 | |||
| ea642d1b60 | |||
| ec006779a8 | |||
| 515be23c44 | |||
| c11aec8b44 | |||
| 3c2147e72c | |||
| 15a35e6243 | |||
| d53a5a492c | |||
| 0810e5ae6a | |||
| 881b183db5 | |||
| 15cea02872 | |||
| c195561df0 | |||
| fc725cfc0c | |||
| 9f54516e8c | |||
| 68a4cd9635 | |||
| ff01802f2f | |||
| bb900bc2e1 | |||
| 459f82b66b | |||
| 4b382243e4 | |||
| af074085d1 | |||
| 0c8c872668 | |||
| ddb29bb40d | |||
| aecb627ab7 | |||
| b8cd53cb59 | |||
| e61f6153c3 | |||
| 386e8ab902 | |||
| 5e8f02e3ca | |||
| f219562e72 | |||
| 29d94dfc14 | |||
| 622f5fc28c | |||
| 647f01e25c | |||
| 5a79c0e5c2 | |||
| 2a4c298572 | |||
| 1e59f73f79 | |||
| feb911aea0 | |||
| d0863d68c6 | |||
| 447d9b3ca1 | |||
| 86e66eb6a0 | |||
| b2c9515a63 | |||
| db04c5caee | |||
| 33526d5d13 | |||
| fc77b548d8 | |||
| bf7a168f2e | |||
| 17b1551bab | |||
| 8864243558 | |||
| 37aab7a16f | |||
| 86e1bdf7ea | |||
| 4547fd213d | |||
| 5aacec40cc | |||
| 1df78100ca | |||
| 9cd36fcb9b | |||
| 24b32eb917 | |||
| dec0e41fec | |||
| 42700ad2b2 | |||
| df51d79f6b | |||
| be1673a6a7 | |||
| 648f182e76 | |||
| 3aa56f0886 | |||
| b795534da7 | |||
| c67e2ac9f8 | |||
| beb418bd5d | |||
| 2b3d9533b0 | |||
| b061f139bd | |||
| ac569324cf | |||
| 357d197bb3 | |||
| 5eed1186ff | |||
| a87a9b3247 | |||
| 7f1c82cd91 | |||
| 048c1ed3ed | |||
| 9a2570d7e7 | |||
| 00b9c2156d | |||
| ff8b22274f | |||
| 786937f847 | |||
| c95efee8ec | |||
| 776d2f79a6 | |||
| 25a6b8cce6 | |||
| f6d7cae17b | |||
| 944a0b5fb1 | |||
| 7769653224 | |||
| ccdc5b5fae | |||
| 20158f573e | |||
| 87c60729b5 | |||
| a03a50b7c6 | |||
| fb85ccf501 | |||
| 3179442d8f | |||
| 33d3c52cd9 | |||
| 1d33e01a43 | |||
| 52ff221dd1 | |||
| 5afe178e23 | |||
| 9118b76084 | |||
| 5a62b527b9 | |||
| 2e9e14dc72 | |||
| 0a0b4893aa | |||
| 6277f81e26 | |||
| d550150787 | |||
| 7626b2153f | |||
| 6d17d1001d | |||
| 0273738d7a | |||
| 322df25ecc | |||
| ab3867d9a8 | |||
| 9bf8ec88f4 | |||
| 685f4d37a6 | |||
| f3b3fe8ac9 | |||
| d5fa49172a | |||
| b8303b9977 | |||
| 16d06c6356 | |||
| 79ddd887d9 | |||
| c394bc6725 | |||
| 9e6d7630f4 | |||
| e2fbdd3c2f | |||
| 849171af8f | |||
| 884975dda6 | |||
| 03cbf22c9b | |||
| a10cee2efa | |||
| 479c94a11d | |||
| c057f31e97 | |||
| d0bc9db6e5 | |||
| e2dd8dd1d7 | |||
| f2ff12faa6 | |||
| 50cc3d7da8 | |||
| 60b1f7a816 | |||
| 33d2bf043b | |||
| 86b20dcae6 | |||
| caf4936c9b | |||
| 7e864d2447 | |||
| ff324688f6 | |||
| efaeb1b341 | |||
| 488cb7f8a2 | |||
| 974fa08651 | |||
| 8f3312e8a8 | |||
| 57d5da0490 | |||
| daeb67319e | |||
| 213665bd1d | |||
| dfc48d6aa9 | |||
| d71d40453f | |||
| 635afbc892 | |||
| e90037e363 | |||
| a730359736 | |||
| 80acfe97c7 | |||
| b6267d07ba | |||
| 910f764823 | |||
| 7a8f302c21 | |||
| fb0c3b55c1 | |||
| f9579855a9 | |||
| 0dd1e2720a | |||
| 331d2d3d26 | |||
| f56554c2d4 | |||
| 98131b389c | |||
| 7cfe6288e1 | |||
| 84041ef2ff | |||
| 9a2af8079e | |||
| 633162d9af | |||
| 50baa0227d | |||
| 18da00f2e2 | |||
| f4f0b2c4b5 | |||
| b7d3007d31 | |||
| 67384981c1 | |||
| 4390bccfb9 | |||
| 8f5632c5ad | |||
| 1facd2ad11 |
5
.devcontainer/Dockerfile
Normal file
5
.devcontainer/Dockerfile
Normal file
@ -0,0 +1,5 @@
|
||||
ARG VARIANT="lts-bullseye"
|
||||
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
|
||||
|
||||
RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
27
.devcontainer/devcontainer.json
Normal file
27
.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "Node.js",
|
||||
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"args": { "VARIANT": "16-bullseye" }
|
||||
},
|
||||
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "lokalise.i18n-ally"],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [80, 5432],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "pnpm install",
|
||||
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "node",
|
||||
|
||||
"features": {
|
||||
"docker-from-docker": "latest"
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,19 @@
|
||||
# Build Artifacts
|
||||
dist
|
||||
.next
|
||||
.turbo
|
||||
|
||||
# IDEs
|
||||
.vscode
|
||||
|
||||
# Project Metadata
|
||||
.crowdin.yml
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
SECURITY.md
|
||||
CHANGELOG.md
|
||||
CODE_OF_CONDUCT.md
|
||||
|
||||
# Project Dependencies
|
||||
node_modules
|
||||
@ -18,4 +24,4 @@ Dockerfile
|
||||
docker-compose.yml
|
||||
|
||||
# Android App
|
||||
/app
|
||||
/app
|
||||
|
||||
51
.env.example
51
.env.example
@ -1,30 +1,39 @@
|
||||
# App
|
||||
# Turbo Cache (Optional)
|
||||
TURBO_TEAM=
|
||||
TURBO_TOKEN=
|
||||
|
||||
# Server + Client
|
||||
TZ=UTC
|
||||
SECRET_KEY=
|
||||
PUBLIC_URL=http://client:3000
|
||||
PUBLIC_SERVER_URL=http://server:3100
|
||||
PUBLIC_GOOGLE_CLIENT_ID=
|
||||
|
||||
# URLs
|
||||
PUBLIC_URL=http://<SERVER-IP>
|
||||
PUBLIC_SERVER_URL=http://<SERVER-IP>/api
|
||||
|
||||
# Database
|
||||
POSTGRES_HOST=localhost
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_USERNAME=postgres
|
||||
# Server + Database
|
||||
POSTGRES_DB=postgres
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_DATABASE=postgres
|
||||
POSTGRES_SSL_CERT=
|
||||
|
||||
# Auth
|
||||
# Server
|
||||
SECRET_KEY=
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_SSL_CERT=
|
||||
JWT_SECRET=
|
||||
JWT_EXPIRY_TIME=604800
|
||||
|
||||
# Google
|
||||
PUBLIC_GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
GOOGLE_API_KEY=
|
||||
MAIL_FROM_NAME=
|
||||
MAIL_FROM_EMAIL=
|
||||
MAIL_HOST=
|
||||
MAIL_PORT=
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
STORAGE_BUCKET=
|
||||
STORAGE_REGION=
|
||||
STORAGE_ENDPOINT=
|
||||
STORAGE_URL_PREFIX=
|
||||
STORAGE_ACCESS_KEY=
|
||||
STORAGE_SECRET_KEY=
|
||||
|
||||
# SendGrid (Optional)
|
||||
SENDGRID_API_KEY=
|
||||
SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID=
|
||||
SENDGRID_FROM_NAME=
|
||||
SENDGRID_FROM_EMAIL=
|
||||
# Flags (Client)
|
||||
PUBLIC_FLAG_DISABLE_SIGNUPS=false
|
||||
@ -1,31 +1,34 @@
|
||||
{
|
||||
"root": true,
|
||||
"ignorePatterns": ["/app"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
|
||||
"extends": ["plugin:@typescript-eslint/recommended"],
|
||||
"plugins": ["@typescript-eslint/eslint-plugin", "simple-import-sort", "unused-imports"],
|
||||
"rules": {
|
||||
// TypeScript ESLint
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
// Simple Import Sort
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
// Unused Imports
|
||||
// ESLint
|
||||
"no-unused-vars": "off",
|
||||
|
||||
// Unused Imports
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "none",
|
||||
"varsIgnorePattern": "^_",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
]
|
||||
],
|
||||
|
||||
// Simple Import Sort
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
|
||||
// TypeScript ESLint
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
||||
27
.github/ISSUE_TEMPLATE/bug-report.md
vendored
27
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@ -1,36 +1,43 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Create a report to help improve
|
||||
title: "[BUG] "
|
||||
title: '[BUG] '
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**Product Flavor**
|
||||
|
||||
- [ ] Managed (https://rxresu.me)
|
||||
- [ ] Self Hosted
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
<!-- Steps to reproduce the behavior: -->
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
- OS: <!--[e.g. iOS]-->
|
||||
- Browser <!--[e.g. chrome, safari]-->
|
||||
- Version <!--[e.g. 22]-->
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
<!-- Add any other context about the problem here. -->
|
||||
|
||||
15
.github/ISSUE_TEMPLATE/feature-request.md
vendored
15
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@ -1,20 +1,23 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEATURE] "
|
||||
title: '[FEATURE] '
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
|
||||
22
.github/dependabot.yml
vendored
Normal file
22
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: 'docker'
|
||||
directory: '/server'
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
|
||||
- package-ecosystem: 'docker'
|
||||
directory: '/client'
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
|
||||
- package-ecosystem: 'gradle'
|
||||
directory: '/app'
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
16
.github/workflows/close-stale.yml
vendored
16
.github/workflows/close-stale.yml
vendored
@ -1,16 +0,0 @@
|
||||
name: 'Close stale issues and PRs'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v5.0.0
|
||||
with:
|
||||
stale-pr-message: 'This PR is stale because it has been open for 30 days with no activity. Remove the stale label or comment on this PR, otherwise it would be closed in 5 days.'
|
||||
stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity. Remove the stale label or comment on this issue, otherwise it would be closed in 5 days.'
|
||||
days-before-stale: 30
|
||||
days-before-close: 5
|
||||
2
.github/workflows/digitalocean-deploy.yml
vendored
2
.github/workflows/digitalocean-deploy.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Install DigitalOcean CLI
|
||||
uses: digitalocean/action-doctl@v2.1.0
|
||||
uses: digitalocean/action-doctl@v2.1.1
|
||||
with:
|
||||
token: ${{ secrets.DIGITALOCEAN_TOKEN }}
|
||||
|
||||
|
||||
108
.github/workflows/docker-build-push.yml
vendored
108
.github/workflows/docker-build-push.yml
vendored
@ -5,26 +5,39 @@ on:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
docker_client:
|
||||
name: Docker (Client)
|
||||
client:
|
||||
name: Client
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.0.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
|
||||
- name: Login to Docker
|
||||
uses: docker/login-action@v1.14.1
|
||||
- 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 Client Image
|
||||
uses: docker/build-push-action@v2.9.0
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
@ -32,27 +45,42 @@ jobs:
|
||||
tags: |
|
||||
amruthpillai/reactive-resume:client-latest
|
||||
amruthpillai/reactive-resume:client-${{ steps.version.outputs.tag }}
|
||||
ghcr.io/amruthpillai/reactive-resume:client-latest
|
||||
ghcr.io/amruthpillai/reactive-resume:client-${{ steps.version.outputs.tag }}
|
||||
|
||||
docker_server:
|
||||
name: Docker (Server)
|
||||
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.0.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
|
||||
- name: Login to Docker
|
||||
uses: docker/login-action@v1.14.1
|
||||
- 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@v2.9.0
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
@ -60,61 +88,5 @@ jobs:
|
||||
tags: |
|
||||
amruthpillai/reactive-resume:server-latest
|
||||
amruthpillai/reactive-resume:server-${{ steps.version.outputs.tag }}
|
||||
|
||||
github_client:
|
||||
name: GitHub (Client)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.0.0
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1.14.1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: $GITHUB_REPOSITORY_OWNER
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Build and Push Client Image
|
||||
uses: docker/build-push-action@v2.9.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: client/Dockerfile
|
||||
tags: |
|
||||
ghcr.io/amruthpillai/reactive-resume:client-latest
|
||||
ghcr.io/amruthpillai/reactive-resume:client-${{ steps.version.outputs.tag }}
|
||||
|
||||
github_server:
|
||||
name: GitHub (Server)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.0.0
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1.14.1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: $GITHUB_REPOSITORY_OWNER
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Build and Push Server Image
|
||||
uses: docker/build-push-action@v2.9.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: server/Dockerfile
|
||||
tags: |
|
||||
ghcr.io/amruthpillai/reactive-resume:server-latest
|
||||
ghcr.io/amruthpillai/reactive-resume:server-${{ steps.version.outputs.tag }}
|
||||
|
||||
88
.github/workflows/docker-build.yml
vendored
Normal file
88
.github/workflows/docker-build.yml
vendored
Normal file
@ -0,0 +1,88 @@
|
||||
name: Build Docker Image
|
||||
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
client:
|
||||
name: Client
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- id: vars
|
||||
name: Get Short SHA
|
||||
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
||||
|
||||
- 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
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
|
||||
- name: Build Client Image
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
file: client/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
amruthpillai/reactive-resume:client-latest
|
||||
amruthpillai/reactive-resume:client-${{ steps.vars.outputs.sha_short }}
|
||||
ghcr.io/amruthpillai/reactive-resume:client-latest
|
||||
ghcr.io/amruthpillai/reactive-resume:client-${{ steps.vars.outputs.sha_short }}
|
||||
|
||||
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.0.2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- 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
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
|
||||
- name: Build Server Image
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
file: server/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
amruthpillai/reactive-resume:server-latest
|
||||
amruthpillai/reactive-resume:server-${{ steps.vars.outputs.sha_short }}
|
||||
ghcr.io/amruthpillai/reactive-resume:server-latest
|
||||
ghcr.io/amruthpillai/reactive-resume:server-${{ steps.vars.outputs.sha_short }}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -7,4 +7,7 @@
|
||||
node_modules
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
|
||||
# Turbo
|
||||
.turbo
|
||||
@ -1,6 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
pnpm install
|
||||
pnpm run lint
|
||||
pnpm run format
|
||||
2
.npmrc
Normal file
2
.npmrc
Normal file
@ -0,0 +1,2 @@
|
||||
auto-install-peers=true
|
||||
strict-peer-dependencies=false
|
||||
@ -18,6 +18,7 @@ CHANGELOG.md
|
||||
|
||||
# Project Dependencies
|
||||
node_modules
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
|
||||
8
.vscode/extensions.json
vendored
8
.vscode/extensions.json
vendored
@ -1,3 +1,7 @@
|
||||
{
|
||||
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "lokalise.i18n-ally"]
|
||||
}
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"lokalise.i18n-ally"
|
||||
]
|
||||
}
|
||||
12
.vscode/launch.json
vendored
12
.vscode/launch.json
vendored
@ -3,12 +3,12 @@
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Debug: Server",
|
||||
"port": 9229,
|
||||
"request": "attach",
|
||||
"restart": true,
|
||||
"stopOnEntry": false,
|
||||
"protocol": "inspector"
|
||||
"protocol": "inspector",
|
||||
"stopOnEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Debug: Client",
|
||||
@ -17,10 +17,10 @@
|
||||
"command": "pnpm run dev:client",
|
||||
"console": "integratedTerminal",
|
||||
"serverReadyAction": {
|
||||
"action": "debugWithChrome",
|
||||
"pattern": "started server on .+, url: (https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"action": "debugWithChrome"
|
||||
"uriFormat": "%s"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
25
.vscode/settings.json
vendored
25
.vscode/settings.json
vendored
@ -1,25 +1,22 @@
|
||||
{
|
||||
"css.validate": false,
|
||||
"scss.validate": false,
|
||||
"editor.wordWrap": "on",
|
||||
"npm.packageManager": "pnpm",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.wordWrap": "on",
|
||||
"eslint.workingDirectories": [
|
||||
"schema",
|
||||
"client",
|
||||
"server"
|
||||
],
|
||||
"i18n-ally.enabledFrameworks": [
|
||||
"react"
|
||||
],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": [
|
||||
"client/public/locales"
|
||||
],
|
||||
"i18n-ally.namespace": true,
|
||||
"i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}",
|
||||
"i18n-ally.sortKeys": true,
|
||||
"scss.validate": false
|
||||
"conventionalCommits.scopes": [
|
||||
"client",
|
||||
"server",
|
||||
"docker",
|
||||
"dependencies"
|
||||
]
|
||||
}
|
||||
179
CHANGELOG.md
179
CHANGELOG.md
@ -2,6 +2,185 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
### [3.6.2](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.5.3...v3.6.2) (2022-08-25)
|
||||
|
||||
### Features
|
||||
|
||||
- Implement Undo/Redo functionality across the resume builder section
|
||||
|
||||
### Improvements
|
||||
|
||||
- Update dependencies to the latest version
|
||||
- Cleanup ESLint configuration, add tailwindCSS formatting
|
||||
|
||||
### [3.5.3](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.5.2...v3.5.3) (2022-08-11)
|
||||
|
||||
### [3.5.2](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.5.1...v3.5.2) (2022-08-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **client:** :dizzy: add Finnish language support ([16d19eb](https://github.com/AmruthPillai/Reactive-Resume/commit/16d19eb70f64f768304f352d0f87102d328b57c1))
|
||||
|
||||
### [3.5.1](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.5.0...v3.5.1) (2022-07-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **client:** :sparkles: ask for confirmation when resetting a resume ([4d43f6a](https://github.com/AmruthPillai/Reactive-Resume/commit/4d43f6a6427198e62e9fcb995f1a28c0ee4de71e))
|
||||
* **docker:** :zap: remove ports from postgres docker instance ([07c91e9](https://github.com/AmruthPillai/Reactive-Resume/commit/07c91e9ac21e8ef120d08ab92363d8e48a55aaba))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **server:** :zap: don't initialize sendgrid if the apikey is empty ([05d3f1f](https://github.com/AmruthPillai/Reactive-Resume/commit/05d3f1f06fbffd899269a5c4dea3c52cf408125f))
|
||||
|
||||
## [3.5.0](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.4.8...v3.5.0) (2022-07-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **client:** :bug: attempt to fix the one-off date issue ([5197f95](https://github.com/AmruthPillai/Reactive-Resume/commit/5197f954c0baed3daf1c7e2c79b607354ef42024))
|
||||
* **client:** :bug: fix mui rendering of utc dates ([977fa72](https://github.com/AmruthPillai/Reactive-Resume/commit/977fa72ddeeeebf7463d43a820e85f783489a4dc))
|
||||
|
||||
### [3.4.8](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.4.7...v3.4.8) (2022-07-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **google:** add toast to display error message from google ([25cf594](https://github.com/AmruthPillai/Reactive-Resume/commit/25cf594eb948e1c2d6157028ee1fff2799df5f92))
|
||||
|
||||
### [3.4.7](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.4.6...v3.4.7) (2022-06-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **mui:** update mui datepickers to newer package ([bfb48e3](https://github.com/AmruthPillai/Reactive-Resume/commit/bfb48e3aa7e0575922841522edc1d38544d1884f))
|
||||
|
||||
### [3.4.6](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.4.5...v3.4.6) (2022-06-19)
|
||||
|
||||
## [3.6.0](https://github.com/dvd741-a/Reactive-Resume/compare/v3.3.4...v3.6.0) (2022-06-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **all:** upgrade to v3.4.0 ([87d381f](https://github.com/dvd741-a/Reactive-Resume/commit/87d381fe8eab9ca4624df5de6e8b9ab18a072b67))
|
||||
* **i18n:** add Hungrarian (Magyar) language ([35fe4e2](https://github.com/dvd741-a/Reactive-Resume/commit/35fe4e27744b6f7325b25db2cf3b626ed8598623))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **i18n:** fix language mismatch in exported pdf ([62fd63e](https://github.com/dvd741-a/Reactive-Resume/commit/62fd63e41fe10fba843a40fb08191f5944f2b2fc))
|
||||
* **typeorm:** update typeorm to latest 0.2.x for secpatch ([5bdb92b](https://github.com/dvd741-a/Reactive-Resume/commit/5bdb92b1cff9e56879f9bbf31801d6554a00a8d5))
|
||||
|
||||
## [3.5.0](https://github.com/dvd741-a/Reactive-Resume/compare/v3.3.4...v3.5.0) (2022-06-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **all:** upgrade to v3.4.0 ([87d381f](https://github.com/dvd741-a/Reactive-Resume/commit/87d381fe8eab9ca4624df5de6e8b9ab18a072b67))
|
||||
* **i18n:** add Hungrarian (Magyar) language ([35fe4e2](https://github.com/dvd741-a/Reactive-Resume/commit/35fe4e27744b6f7325b25db2cf3b626ed8598623))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **i18n:** fix language mismatch in exported pdf ([62fd63e](https://github.com/dvd741-a/Reactive-Resume/commit/62fd63e41fe10fba843a40fb08191f5944f2b2fc))
|
||||
* **typeorm:** update typeorm to latest 0.2.x for secpatch ([5bdb92b](https://github.com/dvd741-a/Reactive-Resume/commit/5bdb92b1cff9e56879f9bbf31801d6554a00a8d5))
|
||||
|
||||
### [3.4.5](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.4.4...v3.4.5) (2022-05-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **i18n:** fix language mismatch in exported pdf ([62fd63e](https://github.com/AmruthPillai/Reactive-Resume/commit/62fd63e41fe10fba843a40fb08191f5944f2b2fc))
|
||||
|
||||
## [3.4.4](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.4.3...v3.4.4) (2022-05-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **i18n:** add Hungrarian (Magyar) language ([35fe4e2](https://github.com/AmruthPillai/Reactive-Resume/commit/35fe4e27744b6f7325b25db2cf3b626ed8598623))
|
||||
|
||||
### [3.4.3](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.4.2...v3.4.3) (2022-05-01)
|
||||
|
||||
### [3.4.2](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.4.1...v3.4.2) (2022-04-30)
|
||||
|
||||
### [3.4.1](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.4.0...v3.4.1) (2022-04-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **typeorm:** update typeorm to latest 0.2.x for secpatch ([5bdb92b](https://github.com/AmruthPillai/Reactive-Resume/commit/5bdb92b1cff9e56879f9bbf31801d6554a00a8d5))
|
||||
|
||||
### [3.4.0](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.3.3...v3.4.0) (2022-04-30)
|
||||
|
||||
### [3.3.4](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.3.3...v3.3.4) (2022-04-09)
|
||||
|
||||
### [3.3.3](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.3.2...v3.3.3) (2022-04-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **profile:** add XING profile icon ([1e59f73](https://github.com/AmruthPillai/Reactive-Resume/commit/1e59f73f79a91d0264c0d2108906ee89d4eb27f8)), closes [#821](https://github.com/AmruthPillai/Reactive-Resume/issues/821)
|
||||
* **s3:** implement non-ephemeral storage through S3/DO Spaces ([feb911a](https://github.com/AmruthPillai/Reactive-Resume/commit/feb911aea06bacf58ea933d2803a2a89fe36e57b))
|
||||
|
||||
### [3.3.2](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.3.1...v3.3.2) (2022-04-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **types/react:** downgrade to <18 ([fc77b54](https://github.com/AmruthPillai/Reactive-Resume/commit/fc77b548d8d61530b2d158ff83f088bed12d5080))
|
||||
|
||||
### [3.3.1](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.3.0...v3.3.1) (2022-04-08)
|
||||
|
||||
## [3.3.0](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.11...v3.3.0) (2022-04-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **upgrade:** changes to code to support new template ([1df7810](https://github.com/AmruthPillai/Reactive-Resume/commit/1df78100ca0667ce9b7834cf2c25384eb21c67c2))
|
||||
|
||||
### What's Changed
|
||||
* New Crowdin updates by @AmruthPillai in https://github.com/AmruthPillai/Reactive-Resume/pull/791
|
||||
* Bump org.jetbrains.kotlin.android from 1.6.10 to 1.6.20 in /app by @dependabot in https://github.com/AmruthPillai/Reactive-Resume/pull/812
|
||||
* New Crowdin updates by @AmruthPillai in https://github.com/AmruthPillai/Reactive-Resume/pull/806
|
||||
* A new template - Leafish by @klejejs in https://github.com/AmruthPillai/Reactive-Resume/pull/811
|
||||
* Automatic multi-platform Docker image build by @schklom in https://github.com/AmruthPillai/Reactive-Resume/pull/817
|
||||
|
||||
### New Contributors
|
||||
* @klejejs made their first contribution in https://github.com/AmruthPillai/Reactive-Resume/pull/811
|
||||
* @schklom made their first contribution in https://github.com/AmruthPillai/Reactive-Resume/pull/817
|
||||
|
||||
### [3.2.11](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.10...v3.2.11) (2022-03-28)
|
||||
|
||||
### [3.2.10](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.9...v3.2.10) (2022-03-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **i18n:** add portuguese (pt) language to i18n locales ([7f1c82c](https://github.com/AmruthPillai/Reactive-Resume/commit/7f1c82cd9185ebb44486a16132eb44d5c2fb747a))
|
||||
|
||||
### [3.2.9](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.8...v3.2.9) (2022-03-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **i18n:** add nl and ru i18n locales to app ([03cbf22](https://github.com/AmruthPillai/Reactive-Resume/commit/03cbf22c9bee96cac8f228830b67b44529b7ecee))
|
||||
|
||||
### [3.2.8](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.7...v3.2.8) (2022-03-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **client/theme:** add theme switcher to landing page ([8f5632c](https://github.com/AmruthPillai/Reactive-Resume/commit/8f5632c5ad0bc8a4b3028c2806365717fedd78c9))
|
||||
* **flags:** introduce flags, disable_user_signups ([b6267d0](https://github.com/AmruthPillai/Reactive-Resume/commit/b6267d07ba2dcaed0da3946d136a0a9a01c441d5)), closes [#698](https://github.com/AmruthPillai/Reactive-Resume/issues/698)
|
||||
* **i18n:** add Vietnamese language to i18n locales ([4390bcc](https://github.com/AmruthPillai/Reactive-Resume/commit/4390bccfb9764f2d2730ec3a124b7befb6792e9a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **client/create-rename-slug:** fix slug accepting apostrophes and other special characters ([1facd2a](https://github.com/AmruthPillai/Reactive-Resume/commit/1facd2ad111cd9d990c808b3956d3915e8711acd)), closes [#706](https://github.com/AmruthPillai/Reactive-Resume/issues/706)
|
||||
* **disable_user_signups:** hide create account link under flag ([80acfe9](https://github.com/AmruthPillai/Reactive-Resume/commit/80acfe97c74bfa05b719285b19144144f3f7c5ba))
|
||||
|
||||
### [3.2.7](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.6...v3.2.7) (2022-03-18)
|
||||
|
||||
|
||||
|
||||
@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
|
||||
40
README.md
40
README.md
@ -1,11 +1,12 @@
|
||||
<img src="https://i.imgur.com/pc8Ingg.png" alt="Reactive Resume" width="256px" height="256px" />
|
||||
<img src="https://github.com/AmruthPillai/Reactive-Resume/blob/main/docs/static/logo.svg" alt="Reactive Resume" width="256px" height="256px" />
|
||||
|
||||
# Reactive Resume
|
||||
|
||||

|
||||

|
||||
[](https://github.com/AmruthPillai/Reactive-Resume/releases)
|
||||
[](https://github.com/AmruthPillai/Reactive-Resume/blob/main/LICENSE)
|
||||
[](https://translate.rxresu.me)
|
||||
[](https://hub.docker.com/r/amruthpillai/reactive-resume)
|
||||

|
||||
[](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)
|
||||
@ -14,6 +15,23 @@ Reactive Resume is a free and open source resume builder that’s built to make
|
||||
|
||||
You have complete control over what goes into your resume, how it looks, what colors, what templates, even the layout in which sections placed. Want a dark mode resume? It’s as easy as editing 3 values and you’re done. You don’t need to wait to see your changes either. Everything you type, everything you change, appears immediately on your resume and gets updated in real time.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Reactive Resume](#reactive-resume)
|
||||
- [Go to App | [Docs](https://docs.rxresu.me)](#go-to-app--docs)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Features](#features)
|
||||
- [Languages](#languages)
|
||||
- [Tutorial](#tutorial)
|
||||
- [Build from Source](#build-from-source)
|
||||
- [Contributing](#contributing)
|
||||
- [Report Bugs and Feature Requests](#report-bugs-and-feature-requests)
|
||||
- [Donations](#donations)
|
||||
- [💸 PayPal](#-paypal)
|
||||
- [Infrastructure](#infrastructure)
|
||||
- [Contributors Wall](#contributors-wall)
|
||||
- [License](#license)
|
||||
|
||||
## Features
|
||||
|
||||
- Free, forever
|
||||
@ -38,19 +56,33 @@ You have complete control over what goes into your resume, how it looks, what co
|
||||
|
||||
- Arabic (اَلْعَرَبِيَّةُ)
|
||||
- Bengali (বাংলা)
|
||||
- Bulgarian (български)
|
||||
- Chinese (中文)
|
||||
- Czech (čeština)
|
||||
- Danish (Dansk)
|
||||
- Dutch (Nederlands)
|
||||
- English
|
||||
- Finnish (Suomi)
|
||||
- French (Français)
|
||||
- German (Deutsch)
|
||||
- Greek (Ελληνικά)
|
||||
- Hebrew (Ivrit)
|
||||
- Hindi (हिन्दी)
|
||||
- Hungarian (Magyar)
|
||||
- Indonesian (Bahasa Indonesia)
|
||||
- Italian (Italiano)
|
||||
- Kannada (ಕನ್ನಡ)
|
||||
- Malayalam (മലയാളം)
|
||||
- Odia (ଓଡ଼ିଆ)
|
||||
- Persian (Farsi)
|
||||
- Polish (Polski)
|
||||
- Portuguese (Português)
|
||||
- Russian (русский)
|
||||
- Spanish (Español)
|
||||
- Swedish (Svenska)
|
||||
- Tamil (தமிழ்)
|
||||
- Turkish (Türkçe)
|
||||
- Vietnamese (Tiếng Việt)
|
||||
|
||||
Help by [translating Reactive Resume](https://translate.rxresu.me) to your language!
|
||||
|
||||
@ -74,7 +106,7 @@ This project makes use of [conventional commits](https://www.conventionalcommits
|
||||
|
||||
NOTE: Be sure to merge the latest from `main` before making a pull request!
|
||||
|
||||
## Bugs? Feature Requests?
|
||||
## Report Bugs and Feature Requests
|
||||
|
||||
Use the [GitHub Issues](https://github.com/AmruthPillai/Reactive-Resume/issues/new/choose) platform to notify me about bugs or new features that you would like to see in Reactive Resume. Please check before creating new issues as there might already be one.
|
||||
|
||||
|
||||
13
SECURITY.md
Normal file
13
SECURITY.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 3.x.x | :white_check_mark: |
|
||||
| 2.x.x | :x: |
|
||||
| 1.x.x | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Create an issue on GitHub or send me an email through the contact form on my website at https://amruthpillai.com/
|
||||
@ -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.6.10' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
|
||||
@ -1,8 +1,20 @@
|
||||
{
|
||||
"extends": ["../.eslintrc.json", "next/core-web-vitals"],
|
||||
"extends": ["../.eslintrc.json", "next/core-web-vitals", "plugin:tailwindcss/recommended"],
|
||||
"ignorePatterns": [".next", "__ENV.js"],
|
||||
"settings": {
|
||||
"next": {
|
||||
"rootDir": "client"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
// Next.js
|
||||
"@next/next/no-img-element": "off",
|
||||
"@next/next/no-sync-scripts": "off"
|
||||
"@next/next/no-sync-scripts": "off",
|
||||
|
||||
// React Hooks
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
|
||||
// Tailwind CSS
|
||||
"tailwindcss/no-custom-classname": ["warn", { "whitelist": ["preview-mode", "printer-mode", "markdown"] }]
|
||||
}
|
||||
}
|
||||
|
||||
5
client/.gitignore
vendored
5
client/.gitignore
vendored
@ -36,4 +36,7 @@ yarn-error.log*
|
||||
*.tsbuildinfo
|
||||
|
||||
# react-env
|
||||
__ENV.js
|
||||
__ENV.js
|
||||
|
||||
# next-sitemap
|
||||
sitemap*.xml
|
||||
@ -1,22 +1,19 @@
|
||||
FROM node:16-alpine as dependencies
|
||||
|
||||
RUN apk add --no-cache curl g++ make python3 \
|
||||
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
FROM node:lts-alpine AS base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-*.yaml ./
|
||||
RUN apk add --no-cache g++ git curl make python3 \
|
||||
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
|
||||
FROM base as dependencies
|
||||
|
||||
COPY package.json pnpm-*.yaml turbo.json ./
|
||||
COPY ./schema/package.json ./schema/package.json
|
||||
COPY ./client/package.json ./client/package.json
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
FROM node:16-alpine as builder
|
||||
|
||||
RUN apk add --no-cache curl g++ make python3 \
|
||||
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
|
||||
WORKDIR /app
|
||||
FROM base as builder
|
||||
|
||||
COPY . .
|
||||
|
||||
@ -24,21 +21,20 @@ 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
|
||||
|
||||
RUN pnpm run build:schema
|
||||
RUN pnpm run build:client
|
||||
ARG TURBO_TEAM
|
||||
ARG TURBO_TOKEN
|
||||
|
||||
FROM node:16-alpine as production
|
||||
ENV TURBO_TEAM $TURBO_TEAM
|
||||
ENV TURBO_TOKEN $TURBO_TOKEN
|
||||
|
||||
WORKDIR /app
|
||||
RUN pnpm run build --filter client
|
||||
|
||||
RUN apk add --no-cache curl \
|
||||
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
FROM base as production
|
||||
|
||||
COPY --from=builder /app/pnpm-*.yaml ./
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/package.json /app/pnpm-*.yaml /app/turbo.json ./
|
||||
COPY --from=builder /app/client/package.json ./client/package.json
|
||||
|
||||
RUN pnpm install -F client --frozen-lockfile --prod
|
||||
RUN pnpm install --filter client --prod --frozen-lockfile --workspace-root
|
||||
|
||||
COPY --from=builder /app/client/.next ./client/.next
|
||||
COPY --from=builder /app/client/public ./client/public
|
||||
@ -49,4 +45,7 @@ EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
|
||||
CMD [ "pnpm", "run", "start:client" ]
|
||||
HEALTHCHECK --interval=30s --timeout=20s --retries=3 --start-period=15s \
|
||||
CMD curl -fSs 127.0.0.1:3000 || exit 1
|
||||
|
||||
CMD [ "pnpm", "run", "start", "--filter", "client" ]
|
||||
@ -5,6 +5,8 @@ import {
|
||||
FilterCenterFocus,
|
||||
InsertPageBreak,
|
||||
Link,
|
||||
RedoOutlined,
|
||||
UndoOutlined,
|
||||
ViewSidebar,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
@ -16,6 +18,7 @@ import { useTranslation } from 'next-i18next';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation } from 'react-query';
|
||||
import { ReactZoomPanPinchRef } from 'react-zoom-pan-pinch';
|
||||
import { ActionCreators } from 'redux-undo';
|
||||
|
||||
import { ServerError } from '@/services/axios';
|
||||
import { printResumeAsPdf, PrintResumeAsPdfParams } from '@/services/printer';
|
||||
@ -31,14 +34,18 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
||||
const theme = useTheme();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const isDesktop = useMediaQuery(theme.breakpoints.up('sm'));
|
||||
const pages = useAppSelector((state) => state.resume.metadata.layout);
|
||||
|
||||
const { past, present: resume, future } = useAppSelector((state) => state.resume);
|
||||
const pages = get(resume, 'metadata.layout');
|
||||
const { left, right } = useAppSelector((state) => state.build.sidebar);
|
||||
const orientation = useAppSelector((state) => state.build.page.orientation);
|
||||
|
||||
const { mutateAsync, isLoading } = useMutation<string, ServerError, PrintResumeAsPdfParams>(printResumeAsPdf);
|
||||
|
||||
const handleUndo = () => dispatch(ActionCreators.undo());
|
||||
const handleRedo = () => dispatch(ActionCreators.redo());
|
||||
|
||||
const handleTogglePageBreakLine = () => dispatch(togglePageBreakLine());
|
||||
|
||||
const handleTogglePageOrientation = () => dispatch(togglePageOrientation());
|
||||
@ -52,7 +59,7 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
||||
const url = getResumeUrl(resume, { withHost: true });
|
||||
await navigator.clipboard.writeText(url);
|
||||
|
||||
toast.success(t('common.toast.success.resume-link-copied'));
|
||||
toast.success(t<string>('common.toast.success.resume-link-copied'));
|
||||
};
|
||||
|
||||
const handleExportPDF = async () => {
|
||||
@ -75,19 +82,33 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
||||
})}
|
||||
>
|
||||
<div className={styles.controller}>
|
||||
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.zoom-in') as string}>
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.undo')}>
|
||||
<ButtonBase onClick={handleUndo} className={clsx({ 'pointer-events-none opacity-50': past.length < 2 })}>
|
||||
<UndoOutlined fontSize="medium" />
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.redo')}>
|
||||
<ButtonBase onClick={handleRedo} className={clsx({ 'pointer-events-none opacity-50': future.length === 0 })}>
|
||||
<RedoOutlined fontSize="medium" />
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.zoom-in')}>
|
||||
<ButtonBase onClick={() => zoomIn(0.25)}>
|
||||
<ZoomIn fontSize="medium" />
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.zoom-out') as string}>
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.zoom-out')}>
|
||||
<ButtonBase onClick={() => zoomOut(0.25)}>
|
||||
<ZoomOut fontSize="medium" />
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.center-artboard') as string}>
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.center-artboard')}>
|
||||
<ButtonBase onClick={() => centerView(0.95)}>
|
||||
<FilterCenterFocus fontSize="medium" />
|
||||
</ButtonBase>
|
||||
@ -97,25 +118,26 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
||||
|
||||
{isDesktop && (
|
||||
<>
|
||||
{pages.length > 1 && (
|
||||
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.toggle-orientation') as string}>
|
||||
<ButtonBase onClick={handleTogglePageOrientation}>
|
||||
{orientation === 'vertical' ? (
|
||||
<AlignHorizontalCenter fontSize="medium" />
|
||||
) : (
|
||||
<AlignVerticalCenter fontSize="medium" />
|
||||
)}
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-orientation')}>
|
||||
<ButtonBase
|
||||
onClick={handleTogglePageOrientation}
|
||||
className={clsx({ 'pointer-events-none opacity-50': pages.length === 1 })}
|
||||
>
|
||||
{orientation === 'vertical' ? (
|
||||
<AlignHorizontalCenter fontSize="medium" />
|
||||
) : (
|
||||
<AlignVerticalCenter fontSize="medium" />
|
||||
)}
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.toggle-page-break-line') as string}>
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-page-break-line')}>
|
||||
<ButtonBase onClick={handleTogglePageBreakLine}>
|
||||
<InsertPageBreak fontSize="medium" />
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.toggle-sidebars') as string}>
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-sidebars')}>
|
||||
<ButtonBase onClick={handleToggleSidebar}>
|
||||
<ViewSidebar fontSize="medium" />
|
||||
</ButtonBase>
|
||||
@ -125,13 +147,13 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
||||
</>
|
||||
)}
|
||||
|
||||
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.copy-link') as string}>
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.copy-link')}>
|
||||
<ButtonBase onClick={handleCopyLink}>
|
||||
<Link fontSize="medium" />
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.export-pdf') as string}>
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.export-pdf')}>
|
||||
<ButtonBase onClick={handleExportPDF} disabled={isLoading}>
|
||||
<Download fontSize="medium" />
|
||||
</ButtonBase>
|
||||
|
||||
@ -13,7 +13,7 @@ import Page from './Page';
|
||||
const Center = () => {
|
||||
const orientation = useAppSelector((state) => state.build.page.orientation);
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const layout: string[][][] = get(resume, 'metadata.layout');
|
||||
|
||||
if (isEmpty(resume)) return null;
|
||||
|
||||
@ -57,7 +57,7 @@ const Header = () => {
|
||||
|
||||
const { mutateAsync: deleteMutation } = useMutation<void, ServerError, DeleteResumeParams>(deleteResume);
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const { left, right } = useAppSelector((state) => state.build.sidebar);
|
||||
|
||||
const name = useMemo(() => get(resume, 'name'), [resume]);
|
||||
@ -133,7 +133,7 @@ const Header = () => {
|
||||
const url = getResumeUrl(resume, { withHost: true });
|
||||
await navigator.clipboard.writeText(url);
|
||||
|
||||
toast.success(t('common.toast.success.resume-link-copied'));
|
||||
toast.success(t<string>('common.toast.success.resume-link-copied'));
|
||||
};
|
||||
|
||||
return (
|
||||
@ -166,14 +166,14 @@ const Header = () => {
|
||||
<ListItemIcon>
|
||||
<DriveFileRenameOutline className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('builder.header.menu.rename')}</ListItemText>
|
||||
<ListItemText>{t<string>('builder.header.menu.rename')}</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={handleDuplicate}>
|
||||
<ListItemIcon>
|
||||
<CopyAll className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('builder.header.menu.duplicate')}</ListItemText>
|
||||
<ListItemText>{t<string>('builder.header.menu.duplicate')}</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
{resume.public ? (
|
||||
@ -181,27 +181,27 @@ const Header = () => {
|
||||
<ListItemIcon>
|
||||
<LinkIcon className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('builder.header.menu.share-link')}</ListItemText>
|
||||
<ListItemText>{t<string>('builder.header.menu.share-link')}</ListItemText>
|
||||
</MenuItem>
|
||||
) : (
|
||||
<Tooltip arrow placement="right" title={t('builder.header.menu.tooltips.share-link') as string}>
|
||||
<Tooltip arrow placement="right" title={t<string>('builder.header.menu.tooltips.share-link')}>
|
||||
<div>
|
||||
<MenuItem>
|
||||
<ListItemIcon>
|
||||
<LinkIcon className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('builder.header.menu.share-link')}</ListItemText>
|
||||
<ListItemText>{t<string>('builder.header.menu.share-link')}</ListItemText>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip arrow placement="right" title={t('builder.header.menu.tooltips.delete') as string}>
|
||||
<Tooltip arrow placement="right" title={t<string>('builder.header.menu.tooltips.delete')}>
|
||||
<MenuItem onClick={handleDelete}>
|
||||
<ListItemIcon>
|
||||
<Delete className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('builder.header.menu.delete')}</ListItemText>
|
||||
<ListItemText>{t<string>('builder.header.menu.delete')}</ListItemText>
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
</Menu>
|
||||
|
||||
@ -20,7 +20,7 @@ type Props = PageProps & {
|
||||
const Page: React.FC<Props> = ({ page, showPageNumbers = false }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const breakLine: boolean = useAppSelector((state) => state.build.page.breakLine);
|
||||
|
||||
const theme: Theme = get(resume, 'metadata.theme');
|
||||
@ -48,9 +48,7 @@ const Page: React.FC<Props> = ({ page, showPageNumbers = false }) => {
|
||||
</div>
|
||||
|
||||
{showPageNumbers && (
|
||||
<h4 className={styles.pageNumber}>
|
||||
{t('builder.common.glossary.page')} {page + 1}
|
||||
</h4>
|
||||
<h4 className={styles.pageNumber}>{`${t<string>('builder.common.glossary.page')} ${page + 1}`}</h4>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -25,7 +25,7 @@ const LeftSidebar = () => {
|
||||
|
||||
const isDesktop = useMediaQuery(theme.breakpoints.up('lg'));
|
||||
|
||||
const sections = useAppSelector((state) => state.resume.sections);
|
||||
const sections = useAppSelector((state) => state.resume.present.sections);
|
||||
const { open } = useAppSelector((state) => state.build.sidebar.left);
|
||||
|
||||
const customSections = useMemo(() => getCustomSections(sections), [sections]);
|
||||
@ -81,14 +81,14 @@ const LeftSidebar = () => {
|
||||
arrow
|
||||
key={id}
|
||||
placement="right"
|
||||
title={get(sections, `${id}.name`, t<string>(`builder.leftSidebar.sections.${id}.heading`))}
|
||||
title={get(sections, `${id}.name`, t<string>(`builder.leftSidebar.sections.${id}.heading`)) as string}
|
||||
>
|
||||
<IconButton onClick={() => handleClick(id)}>{icon}</IconButton>
|
||||
</Tooltip>
|
||||
))}
|
||||
|
||||
{customSections.map(({ id }) => (
|
||||
<Tooltip key={id} title={get(sections, `${id}.name`, '')} placement="right" arrow>
|
||||
<Tooltip key={id} title={get(sections, `${id}.name`, '') as string} placement="right" arrow>
|
||||
<IconButton onClick={() => handleClick(id)}>
|
||||
<Star />
|
||||
</IconButton>
|
||||
@ -114,7 +114,9 @@ const LeftSidebar = () => {
|
||||
|
||||
<div className="py-6 text-right">
|
||||
<Button fullWidth variant="outlined" startIcon={<Add />} onClick={handleAddSection}>
|
||||
{t('builder.common.actions.add', { token: t('builder.leftSidebar.sections.section.heading') })}
|
||||
{t<string>('builder.common.actions.add', {
|
||||
token: t<string>('builder.leftSidebar.sections.section.heading'),
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@ -24,7 +24,7 @@ const Basics = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading path="sections.basics" name={t('builder.leftSidebar.sections.basics.heading')} />
|
||||
<Heading path="sections.basics" name={t<string>('builder.leftSidebar.sections.basics.heading')} />
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="grid items-center gap-4 sm:col-span-2 sm:grid-cols-3">
|
||||
@ -32,11 +32,11 @@ const Basics = () => {
|
||||
<PhotoUpload />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 w-full sm:col-span-2">
|
||||
<ResumeInput label={t('builder.leftSidebar.sections.basics.name.label')} path="basics.name" />
|
||||
<div className="grid w-full gap-2 sm:col-span-2">
|
||||
<ResumeInput label={t<string>('builder.leftSidebar.sections.basics.name.label')} path="basics.name" />
|
||||
|
||||
<Button variant="outlined" startIcon={<PhotoFilter />} onClick={handleClick}>
|
||||
{t('builder.leftSidebar.sections.basics.actions.photo-filters')}
|
||||
{t<string>('builder.leftSidebar.sections.basics.actions.photo-filters')}
|
||||
</Button>
|
||||
|
||||
<Popover
|
||||
@ -57,20 +57,30 @@ const Basics = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ResumeInput label={t('builder.common.form.email.label')} path="basics.email" className="sm:col-span-2" />
|
||||
<ResumeInput label={t('builder.common.form.phone.label')} path="basics.phone" />
|
||||
<ResumeInput label={t('builder.common.form.url.label')} path="basics.website" />
|
||||
<ResumeInput
|
||||
type="date"
|
||||
label={t<string>('builder.leftSidebar.sections.basics.birthdate.label')}
|
||||
path="basics.birthdate"
|
||||
className="sm:col-span-2"
|
||||
/>
|
||||
<ResumeInput
|
||||
label={t<string>('builder.common.form.email.label')}
|
||||
path="basics.email"
|
||||
className="sm:col-span-2"
|
||||
/>
|
||||
<ResumeInput label={t<string>('builder.common.form.phone.label')} path="basics.phone" />
|
||||
<ResumeInput label={t<string>('builder.common.form.url.label')} path="basics.website" />
|
||||
|
||||
<Divider className="sm:col-span-2" />
|
||||
|
||||
<ResumeInput
|
||||
label={t('builder.leftSidebar.sections.basics.headline.label')}
|
||||
label={t<string>('builder.leftSidebar.sections.basics.headline.label')}
|
||||
path="basics.headline"
|
||||
className="sm:col-span-2"
|
||||
/>
|
||||
<ResumeInput
|
||||
type="textarea"
|
||||
label={t('builder.common.form.summary.label')}
|
||||
label={t<string>('builder.common.form.summary.label')}
|
||||
path="basics.summary"
|
||||
className="sm:col-span-2"
|
||||
markdownSupported
|
||||
|
||||
@ -8,19 +8,28 @@ const Location = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading path="sections.location" name={t('builder.leftSidebar.sections.location.heading')} />
|
||||
<Heading path="sections.location" name={t<string>('builder.leftSidebar.sections.location.heading')} />
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<ResumeInput
|
||||
label={t('builder.leftSidebar.sections.location.address.label')}
|
||||
label={t<string>('builder.leftSidebar.sections.location.address.label')}
|
||||
path="basics.location.address"
|
||||
className="sm:col-span-2"
|
||||
/>
|
||||
<ResumeInput label={t('builder.leftSidebar.sections.location.city.label')} path="basics.location.city" />
|
||||
<ResumeInput label={t('builder.leftSidebar.sections.location.region.label')} path="basics.location.region" />
|
||||
<ResumeInput label={t('builder.leftSidebar.sections.location.country.label')} path="basics.location.country" />
|
||||
<ResumeInput
|
||||
label={t('builder.leftSidebar.sections.location.postal-code.label')}
|
||||
label={t<string>('builder.leftSidebar.sections.location.city.label')}
|
||||
path="basics.location.city"
|
||||
/>
|
||||
<ResumeInput
|
||||
label={t<string>('builder.leftSidebar.sections.location.region.label')}
|
||||
path="basics.location.region"
|
||||
/>
|
||||
<ResumeInput
|
||||
label={t<string>('builder.leftSidebar.sections.location.country.label')}
|
||||
path="basics.location.country"
|
||||
/>
|
||||
<ResumeInput
|
||||
label={t<string>('builder.leftSidebar.sections.location.postal-code.label')}
|
||||
path="basics.location.postalCode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -12,7 +12,7 @@ const PhotoFilters = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const photo: Photo = useAppSelector((state) => get(state.resume, 'basics.photo'));
|
||||
const photo: Photo = useAppSelector((state) => get(state.resume.present, 'basics.photo'));
|
||||
const size: number = get(photo, 'filters.size', 128);
|
||||
const shape: PhotoShape = get(photo, 'filters.shape', 'square');
|
||||
const grayscale: boolean = get(photo, 'filters.grayscale', false);
|
||||
@ -32,7 +32,7 @@ const PhotoFilters = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-5 dark:bg-neutral-800">
|
||||
<div>
|
||||
<h4 className="font-medium">{t('builder.leftSidebar.sections.basics.photo-filters.size.heading')}</h4>
|
||||
<h4 className="font-medium">{t<string>('builder.leftSidebar.sections.basics.photo-filters.size.heading')}</h4>
|
||||
|
||||
<div className="mx-2">
|
||||
<Slider
|
||||
@ -54,18 +54,20 @@ const PhotoFilters = () => {
|
||||
<Divider />
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium">{t('builder.leftSidebar.sections.basics.photo-filters.effects.heading')}</h4>
|
||||
<h4 className="font-medium">
|
||||
{t<string>('builder.leftSidebar.sections.basics.photo-filters.effects.heading')}
|
||||
</h4>
|
||||
|
||||
<div className="flex items-center">
|
||||
<FormControlLabel
|
||||
label={t('builder.leftSidebar.sections.basics.photo-filters.effects.grayscale.label') as string}
|
||||
label={t<string>('builder.leftSidebar.sections.basics.photo-filters.effects.grayscale.label')}
|
||||
control={
|
||||
<Checkbox color="secondary" checked={grayscale} onChange={(_, value) => handleSetGrayscale(value)} />
|
||||
}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
label={t('builder.leftSidebar.sections.basics.photo-filters.effects.border.label') as string}
|
||||
label={t<string>('builder.leftSidebar.sections.basics.photo-filters.effects.border.label')}
|
||||
control={<Checkbox color="secondary" checked={border} onChange={(_, value) => handleSetBorder(value)} />}
|
||||
/>
|
||||
</div>
|
||||
@ -74,7 +76,7 @@ const PhotoFilters = () => {
|
||||
<Divider />
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<h4 className="font-medium">{t('builder.leftSidebar.sections.basics.photo-filters.shape.heading')}</h4>
|
||||
<h4 className="font-medium">{t<string>('builder.leftSidebar.sections.basics.photo-filters.shape.heading')}</h4>
|
||||
|
||||
<ToggleButtonGroup exclusive value={shape} onChange={(_, value) => handleChangeShape(value)}>
|
||||
<ToggleButton size="small" value="square" className="w-14">
|
||||
|
||||
@ -21,8 +21,8 @@ const PhotoUpload: React.FC = () => {
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const id: number = useAppSelector((state) => get(state.resume, 'id'));
|
||||
const photo: Photo = useAppSelector((state) => get(state.resume, 'basics.photo'));
|
||||
const id: number = useAppSelector((state) => get(state.resume.present, 'id'));
|
||||
const photo: Photo = useAppSelector((state) => get(state.resume.present, 'basics.photo'));
|
||||
|
||||
const { mutateAsync: uploadMutation, isLoading } = useMutation<Resume, ServerError, UploadPhotoParams>(uploadPhoto);
|
||||
|
||||
@ -49,7 +49,7 @@ const PhotoUpload: React.FC = () => {
|
||||
const file = event.target.files[0];
|
||||
|
||||
if (file.size > FILE_UPLOAD_MAX_SIZE) {
|
||||
toast.error(t('common.toast.error.upload-photo-size'));
|
||||
toast.error(t<string>('common.toast.error.upload-photo-size'));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -67,8 +67,8 @@ const PhotoUpload: React.FC = () => {
|
||||
<Tooltip
|
||||
title={
|
||||
isEmpty(photo.url)
|
||||
? (t('builder.leftSidebar.sections.basics.photo-upload.tooltip.upload') as string)
|
||||
: (t('builder.leftSidebar.sections.basics.photo-upload.tooltip.remove') as string)
|
||||
? (t<string>('builder.leftSidebar.sections.basics.photo-upload.tooltip.upload') as string)
|
||||
: (t<string>('builder.leftSidebar.sections.basics.photo-upload.tooltip.remove') as string)
|
||||
}
|
||||
>
|
||||
<Avatar sx={{ width: 96, height: 96 }} src={photo.url} />
|
||||
|
||||
@ -28,7 +28,7 @@ const Profiles = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading path="sections.profiles" name={t('builder.leftSidebar.sections.profiles.heading')} />
|
||||
<Heading path="sections.profiles" name={t<string>('builder.leftSidebar.sections.profiles.heading')} />
|
||||
|
||||
<List
|
||||
path="basics.profiles"
|
||||
@ -40,8 +40,8 @@ const Profiles = () => {
|
||||
|
||||
<footer className="flex justify-end">
|
||||
<Button variant="outlined" startIcon={<Add />} onClick={handleAdd}>
|
||||
{t('builder.common.actions.add', {
|
||||
token: t('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
||||
{t<string>('builder.common.actions.add', {
|
||||
token: t<string>('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
||||
})}
|
||||
</Button>
|
||||
</footer>
|
||||
|
||||
@ -37,8 +37,8 @@ const Section: React.FC<Props> = ({
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector<string>((state) => get(state.resume, `${path}.name`, name));
|
||||
const visibility = useAppSelector<boolean>((state) => get(state.resume, `${path}.visible`, true));
|
||||
const heading = useAppSelector<string>((state) => get(state.resume.present, `${path}.name`, name));
|
||||
const visibility = useAppSelector<boolean>((state) => get(state.resume.present, `${path}.visible`, true));
|
||||
|
||||
const handleAdd = () => {
|
||||
const id = path.split('.')[1];
|
||||
@ -74,7 +74,7 @@ const Section: React.FC<Props> = ({
|
||||
<SectionSettings path={path} />
|
||||
|
||||
<Button variant="outlined" startIcon={<Add />} onClick={handleAdd}>
|
||||
{t('builder.common.actions.add', { token: heading })}
|
||||
{t<string>('builder.common.actions.add', { token: heading })}
|
||||
</Button>
|
||||
</footer>
|
||||
</>
|
||||
|
||||
@ -18,7 +18,7 @@ const SectionSettings: React.FC<Props> = ({ path }) => {
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const columns = useAppSelector<number>((state) => get(state.resume, `${path}.columns`, 2));
|
||||
const columns = useAppSelector<number>((state) => get(state.resume.present, `${path}.columns`, 2));
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
@ -32,7 +32,7 @@ const SectionSettings: React.FC<Props> = ({ path }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tooltip title={t('builder.common.columns.tooltip') as string}>
|
||||
<Tooltip title={t<string>('builder.common.columns.tooltip')}>
|
||||
<ButtonBase onClick={handleClick} sx={{ padding: 1, borderRadius: 1 }} className="opacity-50 hover:opacity-75">
|
||||
<ViewWeek /> <span className="ml-1.5 text-xs">{columns}</span>
|
||||
</ButtonBase>
|
||||
@ -48,7 +48,7 @@ const SectionSettings: React.FC<Props> = ({ path }) => {
|
||||
}}
|
||||
>
|
||||
<div className="p-5 dark:bg-neutral-800">
|
||||
<h4 className="mb-2 font-medium">{t('builder.common.columns.heading')}</h4>
|
||||
<h4 className="mb-2 font-medium">{t<string>('builder.common.columns.heading')}</h4>
|
||||
|
||||
<ToggleButtonGroup exclusive value={columns} onChange={(_, value: number) => handleSetColumns(value)}>
|
||||
{[1, 2, 3, 4].map((index) => (
|
||||
|
||||
@ -17,7 +17,7 @@ const CustomCSS = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const customCSS: CustomCSSType = useAppSelector((state) => get(state.resume, 'metadata.css', {}));
|
||||
const customCSS: CustomCSSType = useAppSelector((state) => get(state.resume.present, 'metadata.css', {}));
|
||||
|
||||
const handleChange = (value: string | undefined) => {
|
||||
dispatch(setResumeState({ path: 'metadata.css.value', value }));
|
||||
@ -25,7 +25,7 @@ const CustomCSS = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading path="metadata.css" name={t('builder.rightSidebar.sections.css.heading')} isHideable />
|
||||
<Heading path="metadata.css" name={t<string>('builder.rightSidebar.sections.css.heading')} isHideable />
|
||||
|
||||
<Editor
|
||||
height="200px"
|
||||
|
||||
@ -13,18 +13,18 @@ import { useAppSelector } from '@/store/hooks';
|
||||
const Export = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
|
||||
const { mutateAsync, isLoading } = useMutation<string, ServerError, PrintResumeAsPdfParams>(printResumeAsPdf);
|
||||
|
||||
const pdfListItemText = {
|
||||
normal: {
|
||||
primary: t('builder.rightSidebar.sections.export.pdf.normal.primary'),
|
||||
secondary: t('builder.rightSidebar.sections.export.pdf.normal.secondary'),
|
||||
primary: t<string>('builder.rightSidebar.sections.export.pdf.normal.primary'),
|
||||
secondary: t<string>('builder.rightSidebar.sections.export.pdf.normal.secondary'),
|
||||
},
|
||||
loading: {
|
||||
primary: t('builder.rightSidebar.sections.export.pdf.loading.primary'),
|
||||
secondary: t('builder.rightSidebar.sections.export.pdf.loading.secondary'),
|
||||
primary: t<string>('builder.rightSidebar.sections.export.pdf.loading.primary'),
|
||||
secondary: t<string>('builder.rightSidebar.sections.export.pdf.loading.secondary'),
|
||||
},
|
||||
};
|
||||
|
||||
@ -53,7 +53,7 @@ const Export = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading path="metadata.export" name={t('builder.rightSidebar.sections.export.heading')} />
|
||||
<Heading path="metadata.export" name={t<string>('builder.rightSidebar.sections.export.heading')} />
|
||||
|
||||
<List sx={{ padding: 0 }}>
|
||||
<ListItem sx={{ padding: 0 }}>
|
||||
@ -61,8 +61,8 @@ const Export = () => {
|
||||
<Schema />
|
||||
|
||||
<ListItemText
|
||||
primary={t('builder.rightSidebar.sections.export.json.primary')}
|
||||
secondary={t('builder.rightSidebar.sections.export.json.secondary')}
|
||||
primary={t<string>('builder.rightSidebar.sections.export.json.primary')}
|
||||
secondary={t<string>('builder.rightSidebar.sections.export.json.secondary')}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { DragDropContext, Draggable, DraggableLocation, Droppable, DropResult } from '@hello-pangea/dnd';
|
||||
import { Add, Close, Restore } from '@mui/icons-material';
|
||||
import { Button, IconButton, Tooltip } from '@mui/material';
|
||||
import clsx from 'clsx';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import get from 'lodash/get';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { DragDropContext, Draggable, DraggableLocation, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||
|
||||
import Heading from '@/components/shared/Heading';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
@ -23,8 +23,8 @@ const Layout = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const layout = useAppSelector((state) => state.resume.metadata.layout);
|
||||
const resumeSections = useAppSelector((state) => state.resume.sections);
|
||||
const layout = useAppSelector((state) => state.resume.present.metadata.layout);
|
||||
const resumeSections = useAppSelector((state) => state.resume.present.sections);
|
||||
|
||||
const onDragEnd = (dropResult: DropResult) => {
|
||||
const { source: srcLoc, destination: destLoc } = dropResult;
|
||||
@ -60,9 +60,9 @@ const Layout = () => {
|
||||
<>
|
||||
<Heading
|
||||
path="metadata.layout"
|
||||
name={t('builder.rightSidebar.sections.layout.heading')}
|
||||
name={t<string>('builder.rightSidebar.sections.layout.heading')}
|
||||
action={
|
||||
<Tooltip title={t('builder.rightSidebar.sections.layout.tooltip.reset-layout') as string}>
|
||||
<Tooltip title={t<string>('builder.rightSidebar.sections.layout.tooltip.reset-layout')}>
|
||||
<IconButton onClick={handleResetLayout}>
|
||||
<Restore />
|
||||
</IconButton>
|
||||
@ -76,12 +76,16 @@ const Layout = () => {
|
||||
<div key={pageIndex} className={styles.page}>
|
||||
<div className="flex items-center justify-between pr-3">
|
||||
<p className={styles.heading}>
|
||||
{t('builder.common.glossary.page')} {pageIndex + 1}
|
||||
{t<string>('builder.common.glossary.page')} {pageIndex + 1}
|
||||
</p>
|
||||
|
||||
<div className={clsx(styles.delete, { hidden: pageIndex === 0 })}>
|
||||
<Tooltip
|
||||
title={t('builder.common.actions.delete', { token: t('builder.common.glossary.page') }) as string}
|
||||
title={
|
||||
t<string>('builder.common.actions.delete', {
|
||||
token: t<string>('builder.common.glossary.page'),
|
||||
}) as string
|
||||
}
|
||||
>
|
||||
<IconButton size="small" onClick={() => handleDeletePage(pageIndex)}>
|
||||
<Close fontSize="small" />
|
||||
@ -113,7 +117,7 @@ const Layout = () => {
|
||||
[styles.disabled]: !get(resumeSections, `${sectionId}.visible`, true),
|
||||
})}
|
||||
>
|
||||
{get(resumeSections, `${sectionId}.name`)}
|
||||
{get(resumeSections, `${sectionId}.name`, '') as string}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -132,7 +136,7 @@ const Layout = () => {
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<Button variant="outlined" startIcon={<Add />} onClick={handleAddPage}>
|
||||
{t('builder.common.actions.add', { token: t('builder.common.glossary.page') })}
|
||||
{t<string>('builder.common.actions.add', { token: t<string>('builder.common.glossary.page') })}
|
||||
</Button>
|
||||
</div>
|
||||
</DragDropContext>
|
||||
|
||||
@ -12,39 +12,41 @@ const Links = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading path="metadata.links" name={t('builder.rightSidebar.sections.links.heading')} />
|
||||
<Heading path="metadata.links" name={t<string>('builder.rightSidebar.sections.links.heading')} />
|
||||
|
||||
<div className={styles.container}>
|
||||
<div className={styles.section}>
|
||||
<h2>
|
||||
<Savings fontSize="small" />
|
||||
{t('builder.rightSidebar.sections.links.donate.heading')}
|
||||
{t<string>('builder.rightSidebar.sections.links.donate.heading')}
|
||||
</h2>
|
||||
|
||||
<p>{t('builder.rightSidebar.sections.links.donate.body')}</p>
|
||||
<p>{t<string>('builder.rightSidebar.sections.links.donate.body')}</p>
|
||||
|
||||
<a href={DONATION_URL} target="_blank" rel="noreferrer">
|
||||
<Button startIcon={<Coffee />}>{t('builder.rightSidebar.sections.links.donate.button')}</Button>
|
||||
<Button startIcon={<Coffee />}>{t<string>('builder.rightSidebar.sections.links.donate.button')}</Button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<h2>
|
||||
<BugReport fontSize="small" />
|
||||
{t('builder.rightSidebar.sections.links.bugs-features.heading')}
|
||||
{t<string>('builder.rightSidebar.sections.links.bugs-features.heading')}
|
||||
</h2>
|
||||
|
||||
<p>{t('builder.rightSidebar.sections.links.bugs-features.body')}</p>
|
||||
<p>{t<string>('builder.rightSidebar.sections.links.bugs-features.body')}</p>
|
||||
|
||||
<a href={GITHUB_ISSUES_URL} target="_blank" rel="noreferrer">
|
||||
<Button startIcon={<GitHub />}>{t('builder.rightSidebar.sections.links.bugs-features.button')}</Button>
|
||||
<Button startIcon={<GitHub />}>
|
||||
{t<string>('builder.rightSidebar.sections.links.bugs-features.button')}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a href={GITHUB_URL} target="_blank" rel="noreferrer">
|
||||
<Button variant="text" startIcon={<Link />}>
|
||||
{t('builder.rightSidebar.sections.links.github')}
|
||||
{t<string>('builder.rightSidebar.sections.links.github')}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -15,7 +15,7 @@ import dayjs from 'dayjs';
|
||||
import get from 'lodash/get';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import Heading from '@/components/shared/Heading';
|
||||
@ -36,9 +36,11 @@ const Settings = () => {
|
||||
|
||||
const { locale, ...router } = useRouter();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const [confirmReset, setConfirmReset] = useState(false);
|
||||
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const theme = useAppSelector((state) => state.build.theme);
|
||||
const pages = useAppSelector((state) => state.resume.metadata.layout);
|
||||
const pages = useAppSelector((state) => state.resume.present.metadata.layout);
|
||||
const breakLine = useAppSelector((state) => state.build.page.breakLine);
|
||||
const orientation = useAppSelector((state) => state.build.page.orientation);
|
||||
|
||||
@ -48,7 +50,7 @@ const Settings = () => {
|
||||
const dateConfig: DateConfig = useMemo(() => get(resume, 'metadata.date'), [resume]);
|
||||
|
||||
const isDarkMode = useMemo(() => theme === 'dark', [theme]);
|
||||
const exampleString = useMemo(() => `Eg. ${dayjs().format(dateConfig.format)}`, [dateConfig.format]);
|
||||
const exampleString = useMemo(() => `Eg. ${dayjs().utc().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>(
|
||||
@ -78,20 +80,25 @@ const Settings = () => {
|
||||
};
|
||||
|
||||
const handleResetResume = async () => {
|
||||
await resetResumeMutation({ id });
|
||||
if (!confirmReset) {
|
||||
return setConfirmReset(true);
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries(`resume/${username}/${slug}`);
|
||||
await resetResumeMutation({ id });
|
||||
await queryClient.invalidateQueries(`resume/${username}/${slug}`);
|
||||
|
||||
setConfirmReset(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading path="metadata.settings" name={t('builder.rightSidebar.sections.settings.heading')} />
|
||||
<Heading path="metadata.settings" name={t<string>('builder.rightSidebar.sections.settings.heading')} />
|
||||
|
||||
<List sx={{ padding: 0 }}>
|
||||
{/* Global Settings */}
|
||||
<>
|
||||
<ListSubheader className="rounded">
|
||||
{t('builder.rightSidebar.sections.settings.global.heading')}
|
||||
<ListSubheader disableSticky className="rounded">
|
||||
{t<string>('builder.rightSidebar.sections.settings.global.heading')}
|
||||
</ListSubheader>
|
||||
|
||||
<ListItem>
|
||||
@ -99,7 +106,7 @@ const Settings = () => {
|
||||
<Palette />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t('builder.rightSidebar.sections.settings.global.theme.primary')}
|
||||
primary={t<string>('builder.rightSidebar.sections.settings.global.theme.primary')}
|
||||
secondary={themeString}
|
||||
/>
|
||||
<ThemeSwitch checked={isDarkMode} onChange={(_, value: boolean) => handleSetTheme(value)} />
|
||||
@ -108,8 +115,8 @@ const Settings = () => {
|
||||
<ListItem className="flex-col">
|
||||
<ListItemText
|
||||
className="w-full"
|
||||
primary={t('builder.rightSidebar.sections.settings.global.date.primary')}
|
||||
secondary={t('builder.rightSidebar.sections.settings.global.date.secondary')}
|
||||
primary={t<string>('builder.rightSidebar.sections.settings.global.date.primary')}
|
||||
secondary={t<string>('builder.rightSidebar.sections.settings.global.date.secondary')}
|
||||
/>
|
||||
<Autocomplete<string, false, boolean, false>
|
||||
disableClearable
|
||||
@ -124,8 +131,8 @@ const Settings = () => {
|
||||
<ListItem className="flex-col">
|
||||
<ListItemText
|
||||
className="w-full"
|
||||
primary={t('builder.rightSidebar.sections.settings.global.language.primary')}
|
||||
secondary={t('builder.rightSidebar.sections.settings.global.language.secondary')}
|
||||
primary={t<string>('builder.rightSidebar.sections.settings.global.language.primary')}
|
||||
secondary={t<string>('builder.rightSidebar.sections.settings.global.language.secondary')}
|
||||
/>
|
||||
<Autocomplete<Language, false, boolean, false>
|
||||
disableClearable
|
||||
@ -148,15 +155,17 @@ const Settings = () => {
|
||||
|
||||
{/* Page Settings */}
|
||||
<>
|
||||
<ListSubheader className="rounded">{t('builder.rightSidebar.sections.settings.page.heading')}</ListSubheader>
|
||||
<ListSubheader disableSticky className="rounded">
|
||||
{t<string>('builder.rightSidebar.sections.settings.page.heading')}
|
||||
</ListSubheader>
|
||||
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={t('builder.rightSidebar.sections.settings.page.orientation.primary')}
|
||||
primary={t<string>('builder.rightSidebar.sections.settings.page.orientation.primary')}
|
||||
secondary={
|
||||
pages.length === 1
|
||||
? t('builder.rightSidebar.sections.settings.page.orientation.disabled')
|
||||
: t('builder.rightSidebar.sections.settings.page.orientation.secondary')
|
||||
? t<string>('builder.rightSidebar.sections.settings.page.orientation.disabled')
|
||||
: t<string>('builder.rightSidebar.sections.settings.page.orientation.secondary')
|
||||
}
|
||||
/>
|
||||
<Switch
|
||||
@ -169,8 +178,8 @@ const Settings = () => {
|
||||
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={t('builder.rightSidebar.sections.settings.page.break-line.primary')}
|
||||
secondary={t('builder.rightSidebar.sections.settings.page.break-line.secondary')}
|
||||
primary={t<string>('builder.rightSidebar.sections.settings.page.break-line.primary')}
|
||||
secondary={t<string>('builder.rightSidebar.sections.settings.page.break-line.secondary')}
|
||||
/>
|
||||
<Switch color="secondary" checked={breakLine} onChange={() => dispatch(togglePageBreakLine())} />
|
||||
</ListItem>
|
||||
@ -178,8 +187,8 @@ const Settings = () => {
|
||||
|
||||
{/* Resume Settings */}
|
||||
<>
|
||||
<ListSubheader className="rounded">
|
||||
{t('builder.rightSidebar.sections.settings.resume.heading')}
|
||||
<ListSubheader disableSticky className="rounded">
|
||||
{t<string>('builder.rightSidebar.sections.settings.resume.heading')}
|
||||
</ListSubheader>
|
||||
|
||||
<ListItem>
|
||||
@ -188,8 +197,8 @@ const Settings = () => {
|
||||
<Anchor />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t('builder.rightSidebar.sections.settings.resume.sample.primary')}
|
||||
secondary={t('builder.rightSidebar.sections.settings.resume.sample.secondary')}
|
||||
primary={t<string>('builder.rightSidebar.sections.settings.resume.sample.primary')}
|
||||
secondary={t<string>('builder.rightSidebar.sections.settings.resume.sample.secondary')}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
@ -200,8 +209,12 @@ const Settings = () => {
|
||||
<DeleteForever />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t('builder.rightSidebar.sections.settings.resume.reset.primary')}
|
||||
secondary={t('builder.rightSidebar.sections.settings.resume.reset.secondary')}
|
||||
primary={
|
||||
confirmReset
|
||||
? 'Are you sure?'
|
||||
: t<string>('builder.rightSidebar.sections.settings.resume.reset.primary')
|
||||
}
|
||||
secondary={t<string>('builder.rightSidebar.sections.settings.resume.reset.secondary')}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
@ -17,7 +17,7 @@ const Sharing = () => {
|
||||
|
||||
const [showShortUrl, setShowShortUrl] = useState(false);
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const isPublic = useMemo(() => get(resume, 'public'), [resume]);
|
||||
const url = useMemo(() => getResumeUrl(resume, { withHost: true }), [resume]);
|
||||
const shortUrl = useMemo(() => getResumeUrl(resume, { withHost: true, shortUrl: true }), [resume]);
|
||||
@ -29,19 +29,19 @@ const Sharing = () => {
|
||||
|
||||
await navigator.clipboard.writeText(text);
|
||||
|
||||
toast.success(t('common.toast.success.resume-link-copied'));
|
||||
toast.success(t<string>('common.toast.success.resume-link-copied'));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading path="metadata.sharing" name={t('builder.rightSidebar.sections.sharing.heading')} />
|
||||
<Heading path="metadata.sharing" name={t<string>('builder.rightSidebar.sections.sharing.heading')} />
|
||||
|
||||
<List sx={{ padding: 0 }}>
|
||||
<ListItem className="flex flex-col" sx={{ padding: 0 }}>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<ListItemText
|
||||
primary={t('builder.rightSidebar.sections.sharing.visibility.title')}
|
||||
secondary={t('builder.rightSidebar.sections.sharing.visibility.subtitle')}
|
||||
primary={t<string>('builder.rightSidebar.sections.sharing.visibility.title')}
|
||||
secondary={t<string>('builder.rightSidebar.sections.sharing.visibility.subtitle')}
|
||||
/>
|
||||
<Switch color="secondary" checked={isPublic} onChange={(_, value) => handleSetVisibility(value)} />
|
||||
</div>
|
||||
@ -63,7 +63,7 @@ const Sharing = () => {
|
||||
|
||||
<div className="mt-1 flex w-full">
|
||||
<FormControlLabel
|
||||
label={t('builder.rightSidebar.sections.sharing.short-url.label') as string}
|
||||
label={t<string>('builder.rightSidebar.sections.sharing.short-url.label')}
|
||||
control={
|
||||
<Checkbox className="mr-1" checked={showShortUrl} onChange={(_, value) => setShowShortUrl(value)} />
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ const Templates = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const currentTemplate: string = useAppSelector((state) => get(state.resume, 'metadata.template'));
|
||||
const currentTemplate: string = useAppSelector((state) => get(state.resume.present, 'metadata.template'));
|
||||
|
||||
const handleChange = (template: TemplateMeta) => {
|
||||
dispatch(setResumeState({ path: 'metadata.template', value: template.id }));
|
||||
@ -24,14 +24,14 @@ const Templates = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading path="metadata.templates" name={t('builder.rightSidebar.sections.templates.heading')} />
|
||||
<Heading path="metadata.templates" name={t<string>('builder.rightSidebar.sections.templates.heading')} />
|
||||
|
||||
<div className={styles.container}>
|
||||
{Object.values(templateMap).map((template) => (
|
||||
<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" />
|
||||
<Image src={template.preview} alt={template.name} className="rounded-sm" layout="fill" priority />
|
||||
</ButtonBase>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
.container {
|
||||
@apply grid sm:grid-cols-2 gap-4;
|
||||
@apply grid gap-4 sm:grid-cols-2;
|
||||
}
|
||||
|
||||
.colorOptions {
|
||||
@apply col-span-2 mb-4;
|
||||
@apply grid grid-cols-8 gap-y-2 justify-items-center;
|
||||
@apply grid grid-cols-8 justify-items-center gap-y-2;
|
||||
}
|
||||
|
||||
@ -16,7 +16,9 @@ const Theme = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { background, text, primary } = useAppSelector<ThemeType>((state) => get(state.resume, 'metadata.theme'));
|
||||
const { background, text, primary } = useAppSelector<ThemeType>((state) =>
|
||||
get(state.resume.present, 'metadata.theme')
|
||||
);
|
||||
|
||||
const handleChange = (property: string, color: string) => {
|
||||
dispatch(setResumeState({ path: `metadata.theme.${property}`, value: color[0] !== '#' ? `#${color}` : color }));
|
||||
@ -24,7 +26,7 @@ const Theme = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading path="metadata.theme" name={t('builder.rightSidebar.sections.theme.heading')} />
|
||||
<Heading path="metadata.theme" name={t<string>('builder.rightSidebar.sections.theme.heading')} />
|
||||
|
||||
<div className={styles.container}>
|
||||
<div className={styles.colorOptions}>
|
||||
@ -34,18 +36,18 @@ const Theme = () => {
|
||||
</div>
|
||||
|
||||
<ColorPicker
|
||||
label={t('builder.rightSidebar.sections.theme.form.primary.label')}
|
||||
label={t<string>('builder.rightSidebar.sections.theme.form.primary.label')}
|
||||
color={primary}
|
||||
className="col-span-2"
|
||||
onChange={(color) => handleChange('primary', color)}
|
||||
/>
|
||||
<ColorPicker
|
||||
label={t('builder.rightSidebar.sections.theme.form.background.label')}
|
||||
label={t<string>('builder.rightSidebar.sections.theme.form.background.label')}
|
||||
color={background}
|
||||
onChange={(color) => handleChange('background', color)}
|
||||
/>
|
||||
<ColorPicker
|
||||
label={t('builder.rightSidebar.sections.theme.form.text.label')}
|
||||
label={t<string>('builder.rightSidebar.sections.theme.form.text.label')}
|
||||
color={text}
|
||||
onChange={(color) => handleChange('text', color)}
|
||||
/>
|
||||
|
||||
@ -33,7 +33,7 @@ const Widgets: React.FC<WidgetProps> = ({ label, category }) => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { family, size } = useAppSelector<TypographyType>((state) => get(state.resume, 'metadata.typography'));
|
||||
const { family, size } = useAppSelector<TypographyType>((state) => get(state.resume.present, 'metadata.typography'));
|
||||
|
||||
const { data: fonts } = useQuery(FONTS_QUERY, fetchFonts, {
|
||||
select: (fonts) => fonts.sort((a, b) => a.category.localeCompare(b.category)),
|
||||
@ -64,7 +64,7 @@ const Widgets: React.FC<WidgetProps> = ({ label, category }) => {
|
||||
step={1}
|
||||
marks={[
|
||||
{ value: 12, label: '12px' },
|
||||
{ value: 24, label: t('builder.rightSidebar.sections.typography.form.font-size.label') },
|
||||
{ value: 24, label: t<string>('builder.rightSidebar.sections.typography.form.font-size.label') },
|
||||
{ value: 36, label: '36px' },
|
||||
]}
|
||||
valueLabelDisplay="auto"
|
||||
@ -82,7 +82,10 @@ const Widgets: React.FC<WidgetProps> = ({ label, category }) => {
|
||||
value={fonts.find((font) => font.family === family[category])}
|
||||
onChange={(_, font: Font | null) => handleChange('family', font)}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label={t('builder.rightSidebar.sections.typography.form.font-family.label')} />
|
||||
<TextField
|
||||
{...params}
|
||||
label={t<string>('builder.rightSidebar.sections.typography.form.font-family.label')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@ -95,10 +98,13 @@ const Typography = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading path="metadata.typography" name={t('builder.rightSidebar.sections.typography.heading')} />
|
||||
<Heading path="metadata.typography" name={t<string>('builder.rightSidebar.sections.typography.heading')} />
|
||||
|
||||
<Widgets label={t('builder.rightSidebar.sections.typography.widgets.headings.label')} category="heading" />
|
||||
<Widgets label={t('builder.rightSidebar.sections.typography.widgets.body.label')} category="body" />
|
||||
<Widgets
|
||||
label={t<string>('builder.rightSidebar.sections.typography.widgets.headings.label')}
|
||||
category="heading"
|
||||
/>
|
||||
<Widgets label={t<string>('builder.rightSidebar.sections.typography.widgets.body.label')} category="body" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -94,7 +94,7 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
||||
const url = getResumeUrl(resume, { withHost: true });
|
||||
await navigator.clipboard.writeText(url);
|
||||
|
||||
toast.success(t('common.toast.success.resume-link-copied'));
|
||||
toast.success(t<string>('common.toast.success.resume-link-copied'));
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
@ -124,7 +124,7 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
||||
<footer>
|
||||
<div className={styles.meta}>
|
||||
<p>{resume.name}</p>
|
||||
<p>{t('dashboard.resume.timestamp', { timestamp: getRelativeTime(resume.updatedAt) })}</p>
|
||||
<p>{t<string>('dashboard.resume.timestamp', { timestamp: getRelativeTime(resume.updatedAt) })}</p>
|
||||
</div>
|
||||
|
||||
<ButtonBase className={styles.menu} onClick={handleOpenMenu}>
|
||||
@ -136,21 +136,21 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
||||
<ListItemIcon>
|
||||
<OpenInNew className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('dashboard.resume.menu.open')}</ListItemText>
|
||||
<ListItemText>{t<string>('dashboard.resume.menu.open')}</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={handleRename}>
|
||||
<ListItemIcon>
|
||||
<DriveFileRenameOutline className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('dashboard.resume.menu.rename')}</ListItemText>
|
||||
<ListItemText>{t<string>('dashboard.resume.menu.rename')}</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={handleDuplicate}>
|
||||
<ListItemIcon>
|
||||
<ContentCopy className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('dashboard.resume.menu.duplicate')}</ListItemText>
|
||||
<ListItemText>{t<string>('dashboard.resume.menu.duplicate')}</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
{resume.public ? (
|
||||
@ -158,27 +158,27 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
||||
<ListItemIcon>
|
||||
<LinkIcon className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('dashboard.resume.menu.share-link')}</ListItemText>
|
||||
<ListItemText>{t<string>('dashboard.resume.menu.share-link')}</ListItemText>
|
||||
</MenuItem>
|
||||
) : (
|
||||
<Tooltip arrow placement="right" title={t('dashboard.resume.menu.tooltips.share-link') as string}>
|
||||
<Tooltip arrow placement="right" title={t<string>('dashboard.resume.menu.tooltips.share-link')}>
|
||||
<div>
|
||||
<MenuItem>
|
||||
<ListItemIcon>
|
||||
<LinkIcon className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('dashboard.resume.menu.share-link')}</ListItemText>
|
||||
<ListItemText>{t<string>('dashboard.resume.menu.share-link')}</ListItemText>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip arrow placement="right" title={t('dashboard.resume.menu.tooltips.delete') as string}>
|
||||
<Tooltip arrow placement="right" title={t<string>('dashboard.resume.menu.tooltips.delete')}>
|
||||
<MenuItem onClick={handleDelete}>
|
||||
<ListItemIcon>
|
||||
<DeleteOutline className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('dashboard.resume.menu.delete')}</ListItemText>
|
||||
<ListItemText>{t<string>('dashboard.resume.menu.delete')}</ListItemText>
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
</Menu>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
.testimony {
|
||||
@apply grid gap-2;
|
||||
@apply border-2 rounded p-4 dark:border-neutral-800;
|
||||
@apply rounded border-2 p-4 dark:border-neutral-800;
|
||||
|
||||
blockquote {
|
||||
@apply text-xs leading-normal text-justify opacity-90;
|
||||
@apply text-justify text-xs leading-normal opacity-90;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
|
||||
@ -56,12 +56,12 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
||||
<Menu anchorEl={anchorEl} onClose={handleClose} open={Boolean(anchorEl)}>
|
||||
<MenuItem>
|
||||
<div>
|
||||
<span className="text-xs opacity-50">{t('common.avatar.menu.greeting')}</span>
|
||||
<span className="text-xs opacity-50">{t<string>('common.avatar.menu.greeting')}</span>
|
||||
<p>{user?.name}</p>
|
||||
</div>
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem onClick={handleLogout}>{t('common.avatar.menu.logout')}</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>{t<string>('common.avatar.menu.logout')}</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
.content {
|
||||
@apply rounded p-6 text-sm shadow lg:w-1/2 xl:w-2/5;
|
||||
@apply rounded px-6 text-sm shadow lg:w-1/2 xl:w-2/5;
|
||||
@apply absolute inset-4 sm:inset-x-4 sm:inset-y-auto lg:inset-auto;
|
||||
@apply overflow-scroll bg-neutral-50 dark:bg-neutral-900 lg:overflow-auto;
|
||||
@apply max-h-[90vh] min-h-fit;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply sticky top-0 left-0 right-0 z-50 bg-neutral-50 pt-6 dark:bg-neutral-900;
|
||||
@apply flex items-center justify-between;
|
||||
@apply w-full border-b pb-5 dark:border-white/10;
|
||||
|
||||
@ -27,6 +33,7 @@
|
||||
}
|
||||
|
||||
.footer {
|
||||
@apply sticky bottom-0 left-0 right-0 z-50 bg-neutral-50 pb-6 dark:bg-neutral-900;
|
||||
@apply flex items-center justify-end gap-x-4;
|
||||
@apply w-full border-t pt-5 dark:border-white/10;
|
||||
}
|
||||
|
||||
@ -5,11 +5,12 @@ import { useRouter } from 'next/router';
|
||||
import styles from './BaseModal.module.scss';
|
||||
|
||||
type Props = {
|
||||
icon?: React.ReactNode;
|
||||
isOpen: boolean;
|
||||
heading: string;
|
||||
handleClose: () => void;
|
||||
icon?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
footerChildren?: React.ReactNode;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
const BaseModal: React.FC<Props> = ({ icon, isOpen, heading, children, handleClose, footerChildren }) => {
|
||||
|
||||
@ -10,7 +10,7 @@ const Footer: React.FC<Props> = ({ className }) => {
|
||||
|
||||
return (
|
||||
<div className={clsx('text-xs', className)}>
|
||||
<p>{t('common.footer.license')}</p>
|
||||
<p>{t<string>('common.footer.license')}</p>
|
||||
|
||||
<p>
|
||||
<Trans t={t} i18nKey="common.footer.credit">
|
||||
|
||||
@ -32,8 +32,8 @@ const Heading: React.FC<Props> = ({
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`, name));
|
||||
const visibility = useAppSelector((state) => get(state.resume, `${path}.visible`, true));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`, name));
|
||||
const visibility = useAppSelector((state) => get(state.resume.present, `${path}.visible`, true));
|
||||
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
|
||||
@ -72,19 +72,19 @@ const Heading: React.FC<Props> = ({
|
||||
})}
|
||||
>
|
||||
{isEditable && (
|
||||
<Tooltip title={t('builder.common.tooltip.rename-section') as string}>
|
||||
<Tooltip title={t<string>('builder.common.tooltip.rename-section')}>
|
||||
<IconButton onClick={toggleEditMode}>{editMode ? <Check /> : <DriveFileRenameOutline />}</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isHideable && (
|
||||
<Tooltip title={t('builder.common.tooltip.toggle-visibility') as string}>
|
||||
<Tooltip title={t<string>('builder.common.tooltip.toggle-visibility')}>
|
||||
<IconButton onClick={toggleVisibility}>{visibility ? <Visibility /> : <VisibilityOff />}</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isDeletable && (
|
||||
<Tooltip title={t('builder.common.tooltip.delete-section') as string}>
|
||||
<Tooltip title={t<string>('builder.common.tooltip.delete-section')}>
|
||||
<IconButton onClick={handleDelete}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
|
||||
@ -9,5 +9,5 @@
|
||||
}
|
||||
|
||||
.language {
|
||||
@apply py-2 px-4 cursor-pointer text-center hover:underline;
|
||||
@apply cursor-pointer py-2 px-4 text-center hover:underline;
|
||||
}
|
||||
|
||||
@ -1,70 +1,51 @@
|
||||
import { Language } from '@mui/icons-material';
|
||||
import { IconButton, Popover } from '@mui/material';
|
||||
import { IconButton, Menu, MenuItem } from '@mui/material';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { MouseEvent, useState } from 'react';
|
||||
|
||||
import { languages } from '@/config/languages';
|
||||
import { useAppDispatch } from '@/store/hooks';
|
||||
import { setResumeState } from '@/store/resume/resumeSlice';
|
||||
|
||||
import styles from './LanguageSwitcher.module.scss';
|
||||
import { TRANSLATE_URL } from '@/constants/index';
|
||||
|
||||
const LanguageSwitcher = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const handleClick = (event: MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget);
|
||||
|
||||
const handleClose = () => setAnchorEl(null);
|
||||
|
||||
const handleChangeLanguage = (locale: string) => {
|
||||
const handleChange = (locale: string) => {
|
||||
const { pathname, asPath, query } = router;
|
||||
|
||||
handleClose();
|
||||
|
||||
document.cookie = `NEXT_LOCALE=${locale}; path=/; expires=2147483647`;
|
||||
dispatch(setResumeState({ path: 'metadata.locale', value: locale }));
|
||||
|
||||
router.push({ pathname, query }, asPath, { locale });
|
||||
};
|
||||
|
||||
const handleAddLanguage = () => window.open(TRANSLATE_URL, '_blank');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IconButton onClick={handleClick}>
|
||||
<Language />
|
||||
</IconButton>
|
||||
|
||||
<Popover
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<div className={styles.popover}>
|
||||
<div className={styles.container}>
|
||||
{languages.map(({ code, name, localName }) => (
|
||||
<p key={code} className={styles.language} onClick={() => handleChangeLanguage(code)}>
|
||||
{name} {localName && `(${localName})`}
|
||||
</p>
|
||||
))}
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleClose}>
|
||||
{languages.map(({ code, name, localName }) => (
|
||||
<MenuItem key={code} onClick={() => handleChange(code)}>
|
||||
{name} {localName && `(${localName})`}
|
||||
</MenuItem>
|
||||
))}
|
||||
|
||||
<a href="https://translate.rxresu.me" target="_blank" rel="noreferrer" className={styles.language}>
|
||||
{t('common.footer.language.missing')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
<MenuItem>
|
||||
<span className="font-bold" onClick={handleAddLanguage}>
|
||||
Add your language
|
||||
</span>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -36,7 +36,7 @@ const List: React.FC<Props> = ({
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const list: Array<ListItemType> = useAppSelector((state) => get(state.resume, path, []));
|
||||
const list: Array<ListItemType> = useAppSelector((state) => get(state.resume.present, path, []));
|
||||
|
||||
const handleEdit = (item: ListItemType) => {
|
||||
isFunction(onEdit) && onEdit(item);
|
||||
@ -66,7 +66,7 @@ const List: React.FC<Props> = ({
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<div className={clsx(styles.container, className)}>
|
||||
{isEmpty(list) && <div className={styles.empty}>{t('builder.common.list.empty-text')}</div>}
|
||||
{isEmpty(list) && <div className={styles.empty}>{t<string>('builder.common.list.empty-text')}</div>}
|
||||
|
||||
{list.map((item, index) => {
|
||||
const title = get(item, titleKey, '');
|
||||
@ -76,6 +76,7 @@ const List: React.FC<Props> = ({
|
||||
return (
|
||||
<ListItem
|
||||
key={item.id}
|
||||
path={path}
|
||||
item={item}
|
||||
index={index}
|
||||
title={title}
|
||||
|
||||
@ -17,6 +17,7 @@ interface DragItem {
|
||||
|
||||
type Props = {
|
||||
item: ListItemType;
|
||||
path: string;
|
||||
index: number;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
@ -26,14 +27,14 @@ type Props = {
|
||||
onDuplicate?: (item: ListItemType) => void;
|
||||
};
|
||||
|
||||
const ListItem: React.FC<Props> = ({ item, index, title, subtitle, onMove, onEdit, onDelete, onDuplicate }) => {
|
||||
const ListItem: React.FC<Props> = ({ item, path, index, title, subtitle, onMove, onEdit, onDelete, onDuplicate }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
|
||||
|
||||
const [{ handlerId }, drop] = useDrop<DragItem, any, any>({
|
||||
accept: 'ListItem',
|
||||
accept: path,
|
||||
collect(monitor) {
|
||||
return { handlerId: monitor.getHandlerId() };
|
||||
},
|
||||
@ -68,7 +69,7 @@ const ListItem: React.FC<Props> = ({ item, index, title, subtitle, onMove, onEdi
|
||||
});
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: 'ListItem',
|
||||
type: path,
|
||||
item: () => {
|
||||
return { id: item.id, index };
|
||||
},
|
||||
@ -125,25 +126,25 @@ const ListItem: React.FC<Props> = ({ item, index, title, subtitle, onMove, onEdi
|
||||
<ListItemIcon>
|
||||
<DriveFileRenameOutline className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('builder.common.list.actions.edit')}</ListItemText>
|
||||
<ListItemText>{t<string>('builder.common.list.actions.edit')}</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={() => handleDuplicate(item)}>
|
||||
<ListItemIcon>
|
||||
<FileCopy className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('builder.common.list.actions.duplicate')}</ListItemText>
|
||||
<ListItemText>{t<string>('builder.common.list.actions.duplicate')}</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Tooltip arrow placement="right" title={t('builder.common.tooltip.delete-item') as string}>
|
||||
<Tooltip arrow placement="right" title={t<string>('builder.common.tooltip.delete-item')}>
|
||||
<div>
|
||||
<MenuItem onClick={() => handleDelete(item)}>
|
||||
<ListItemIcon>
|
||||
<DeleteOutline className="scale-90" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('builder.common.list.actions.delete')}</ListItemText>
|
||||
<ListItemText>{t<string>('builder.common.list.actions.delete')}</ListItemText>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
@ -5,6 +5,7 @@ import styles from './Loading.module.scss';
|
||||
|
||||
const Loading: React.FC = () => {
|
||||
const { isReady } = useRouter();
|
||||
|
||||
const isFetching = useIsFetching();
|
||||
const isMutating = useIsMutating();
|
||||
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const NoSSR: React.FC = ({ children }) => <>{children}</>;
|
||||
|
||||
export default dynamic(() => Promise.resolve(NoSSR), { ssr: false });
|
||||
@ -1,4 +1,7 @@
|
||||
import { TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import dayjs from 'dayjs';
|
||||
import { isEmpty } from 'lodash';
|
||||
import get from 'lodash/get';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
@ -8,7 +11,7 @@ import { setResumeState } from '@/store/resume/resumeSlice';
|
||||
import MarkdownSupported from './MarkdownSupported';
|
||||
|
||||
interface Props {
|
||||
type?: 'text' | 'textarea';
|
||||
type?: 'text' | 'textarea' | 'date';
|
||||
label: string;
|
||||
path: string;
|
||||
className?: string;
|
||||
@ -18,7 +21,7 @@ interface Props {
|
||||
const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, markdownSupported = false }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const stateValue = useAppSelector((state) => get(state.resume, path, ''));
|
||||
const stateValue = useAppSelector((state) => get(state.resume.present, path, ''));
|
||||
|
||||
useEffect(() => {
|
||||
setValue(stateValue);
|
||||
@ -31,6 +34,11 @@ const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, m
|
||||
dispatch(setResumeState({ path, value: event.target.value }));
|
||||
};
|
||||
|
||||
const onChangeValue = (value: string) => {
|
||||
setValue(value);
|
||||
dispatch(setResumeState({ path, value }));
|
||||
};
|
||||
|
||||
if (type === 'textarea') {
|
||||
return (
|
||||
<TextField
|
||||
@ -45,6 +53,22 @@ const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, m
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'date') {
|
||||
return (
|
||||
<DatePicker
|
||||
openTo="year"
|
||||
label={label}
|
||||
value={value}
|
||||
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());
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <TextField type={type} label={label} value={value} onChange={onChange} className={className} />;
|
||||
};
|
||||
|
||||
|
||||
@ -6,19 +6,33 @@ export type Language = {
|
||||
|
||||
export const languages: Language[] = [
|
||||
{ code: 'ar', name: 'Arabic', localName: 'اَلْعَرَبِيَّةُ' },
|
||||
{ code: 'bg', name: 'Bulgarian', localName: 'български' },
|
||||
{ code: 'bn', name: 'Bengali', localName: 'বাংলা' },
|
||||
{ code: 'cs', name: 'Czech', localName: 'čeština' },
|
||||
{ code: 'da', name: 'Danish', localName: 'Dansk' },
|
||||
{ code: 'de', name: 'German', localName: 'Deutsch' },
|
||||
{ code: 'el', name: 'Greek', localName: 'Ελληνικά' },
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'es', name: 'Spanish', localName: 'Español' },
|
||||
{ code: 'fi', name: 'Finnish', localName: 'Suomi' },
|
||||
{ code: 'fr', name: 'French', localName: 'Français' },
|
||||
{ code: 'he', name: 'Hebrew', localName: 'Ivrit' },
|
||||
{ code: 'hi', name: 'Hindi', localName: 'हिन्दी' },
|
||||
{ code: 'hu', name: 'Hungarian', localName: 'Magyar' },
|
||||
{ code: 'id', name: 'Indonesian', localName: 'Bahasa Indonesia' },
|
||||
{ code: 'it', name: 'Italian', localName: 'Italiano' },
|
||||
{ code: 'kn', name: 'Kannada', localName: 'ಕನ್ನಡ' },
|
||||
{ code: 'ml', name: 'Malayalam', localName: 'മലയാളം' },
|
||||
{ code: 'nl', name: 'Dutch', localName: 'Nederlands' },
|
||||
{ code: 'or', name: 'Odia', localName: 'ଓଡ଼ିଆ' },
|
||||
{ code: 'fa', name: 'Persian', localName: 'Farsi' },
|
||||
{ code: 'pl', name: 'Polish', localName: 'Polski' },
|
||||
{ code: 'pt', name: 'Portuguese', localName: 'Português' },
|
||||
{ code: 'ru', name: 'Russian', localName: 'русский' },
|
||||
{ code: 'sv', name: 'Swedish', localName: 'Svenska' },
|
||||
{ code: 'ta', name: 'Tamil', localName: 'தமிழ்' },
|
||||
{ code: 'tr', name: 'Turkish', localName: 'Türkçe' },
|
||||
{ code: 'vi', name: 'Vietnamese', localName: 'Tiếng Việt' },
|
||||
{ code: 'zh', name: 'Chinese', localName: '中文' },
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createTheme } from '@mui/material';
|
||||
import { createTheme, ThemeOptions } from '@mui/material/styles';
|
||||
|
||||
const theme = createTheme({
|
||||
const theme: ThemeOptions = {
|
||||
typography: {
|
||||
fontSize: 12,
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
@ -49,7 +49,7 @@ const theme = createTheme({
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const lightTheme = createTheme({
|
||||
...theme,
|
||||
|
||||
3
client/constants/flags.ts
Normal file
3
client/constants/flags.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import env from '@beam-australia/react-env';
|
||||
|
||||
export const FLAG_DISABLE_SIGNUPS = env('FLAG_DISABLE_SIGNUPS') === 'true';
|
||||
@ -10,6 +10,8 @@ export const FILENAME_TIMESTAMP = 'DDMMYYYYHHmmss';
|
||||
|
||||
// Links
|
||||
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 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';
|
||||
|
||||
@ -54,16 +54,16 @@ const ForgotPasswordModal: React.FC = () => {
|
||||
<BaseModal
|
||||
icon={<Password />}
|
||||
isOpen={isOpen}
|
||||
heading={t('modals.auth.forgot-password.heading')}
|
||||
heading={t<string>('modals.auth.forgot-password.heading')}
|
||||
handleClose={handleClose}
|
||||
footerChildren={
|
||||
<Button type="submit" disabled={isLoading} onClick={handleSubmit(onSubmit)}>
|
||||
{t('modals.auth.forgot-password.actions.send-email')}
|
||||
{t<string>('modals.auth.forgot-password.actions.send-email')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="grid gap-4">
|
||||
<p>{t('modals.auth.forgot-password.body')}</p>
|
||||
<p>{t<string>('modals.auth.forgot-password.body')}</p>
|
||||
|
||||
<form className="grid gap-4 xl:w-2/3">
|
||||
<Controller
|
||||
@ -72,7 +72,7 @@ const ForgotPasswordModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
autoFocus
|
||||
label={t('modals.auth.forgot-password.form.email.label')}
|
||||
label={t<string>('modals.auth.forgot-password.form.email.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -81,7 +81,7 @@ const ForgotPasswordModal: React.FC = () => {
|
||||
/>
|
||||
</form>
|
||||
|
||||
<p className="text-xs">{t('modals.auth.forgot-password.help-text')}</p>
|
||||
<p className="text-xs">{t<string>('modals.auth.forgot-password.help-text')}</p>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</>
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
import env from '@beam-australia/react-env';
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Google, Login, Visibility, VisibilityOff } from '@mui/icons-material';
|
||||
import { Login, Visibility, VisibilityOff } from '@mui/icons-material';
|
||||
import { Button, IconButton, InputAdornment, TextField } from '@mui/material';
|
||||
import { CredentialResponse, GoogleLogin } from '@react-oauth/google';
|
||||
import Joi from 'joi';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { GoogleLoginResponse, GoogleLoginResponseOffline, useGoogleLogin } from 'react-google-login';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useIsMutating, useMutation } from 'react-query';
|
||||
|
||||
import BaseModal from '@/components/shared/BaseModal';
|
||||
import { FLAG_DISABLE_SIGNUPS } from '@/constants/flags';
|
||||
import { login, LoginParams, loginWithGoogle, LoginWithGoogleParams } from '@/services/auth';
|
||||
import { ServerError } from '@/services/axios';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
@ -55,15 +56,6 @@ const LoginModal: React.FC = () => {
|
||||
loginWithGoogle
|
||||
);
|
||||
|
||||
const { signIn } = useGoogleLogin({
|
||||
clientId: env('GOOGLE_CLIENT_ID'),
|
||||
onSuccess: async (response: GoogleLoginResponse | GoogleLoginResponseOffline) => {
|
||||
await loginWithGoogleMutation({ accessToken: (response as GoogleLoginResponse).accessToken });
|
||||
|
||||
handleClose();
|
||||
},
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(setModalState({ modal: 'auth.login', state: { open: false } }));
|
||||
reset();
|
||||
@ -92,8 +84,16 @@ const LoginModal: React.FC = () => {
|
||||
dispatch(setModalState({ modal: 'auth.forgot', state: { open: true } }));
|
||||
};
|
||||
|
||||
const handleLoginWithGoogle = () => {
|
||||
signIn();
|
||||
const handleLoginWithGoogle = async (response: CredentialResponse) => {
|
||||
if (response.credential) {
|
||||
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleLoginWithGoogleError });
|
||||
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoginWithGoogleError = () => {
|
||||
toast("Please try logging in using email/password, or use another browser that supports Google's One Tap API.");
|
||||
};
|
||||
|
||||
const PasswordVisibility = (): React.ReactElement => {
|
||||
@ -112,29 +112,21 @@ const LoginModal: React.FC = () => {
|
||||
<BaseModal
|
||||
icon={<Login />}
|
||||
isOpen={isOpen}
|
||||
heading={t('modals.auth.login.heading')}
|
||||
heading={t<string>('modals.auth.login.heading')}
|
||||
handleClose={handleClose}
|
||||
footerChildren={
|
||||
<div className="flex gap-4">
|
||||
{!isEmpty(env('GOOGLE_CLIENT_ID')) && (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outlined"
|
||||
disabled={isLoading}
|
||||
startIcon={<Google />}
|
||||
onClick={handleLoginWithGoogle}
|
||||
>
|
||||
{t('modals.auth.login.actions.google')}
|
||||
</Button>
|
||||
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleLoginWithGoogleError} />
|
||||
)}
|
||||
|
||||
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
||||
{t('modals.auth.login.actions.login')}
|
||||
{t<string>('modals.auth.login.actions.login')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p>{t('modals.auth.login.body')}</p>
|
||||
<p>{t<string>('modals.auth.login.body')}</p>
|
||||
|
||||
<form className="grid gap-4 xl:w-2/3">
|
||||
<Controller
|
||||
@ -143,9 +135,9 @@ const LoginModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
autoFocus
|
||||
label={t('modals.auth.login.form.username.label')}
|
||||
label={t<string>('modals.auth.login.form.username.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || t('modals.auth.login.form.username.help-text')}
|
||||
helperText={fieldState.error?.message || t<string>('modals.auth.login.form.username.help-text')}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
@ -157,7 +149,7 @@ const LoginModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
label={t('modals.auth.login.form.password.label')}
|
||||
label={t<string>('modals.auth.login.form.password.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
InputProps={{ endAdornment: <PasswordVisibility /> }}
|
||||
@ -167,11 +159,13 @@ const LoginModal: React.FC = () => {
|
||||
/>
|
||||
</form>
|
||||
|
||||
<p className="text-xs">
|
||||
<Trans t={t} i18nKey="modals.auth.login.register-text">
|
||||
If you don't have one, you can <a onClick={handleCreateAccount}>create an account</a> here.
|
||||
</Trans>
|
||||
</p>
|
||||
{!FLAG_DISABLE_SIGNUPS && (
|
||||
<p className="text-xs">
|
||||
<Trans t={t} i18nKey="modals.auth.login.register-text">
|
||||
If you don't have one, you can <a onClick={handleCreateAccount}>create an account</a> here.
|
||||
</Trans>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="text-xs">
|
||||
<Trans t={t} i18nKey="modals.auth.login.recover-text">
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import env from '@beam-australia/react-env';
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Google, HowToReg } from '@mui/icons-material';
|
||||
import { HowToReg } from '@mui/icons-material';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { CredentialResponse, GoogleLogin } from '@react-oauth/google';
|
||||
import Joi from 'joi';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { GoogleLoginResponse, GoogleLoginResponseOffline, useGoogleLogin } from 'react-google-login';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import BaseModal from '@/components/shared/BaseModal';
|
||||
@ -63,15 +64,6 @@ const RegisterModal: React.FC = () => {
|
||||
loginWithGoogle
|
||||
);
|
||||
|
||||
const { signIn } = useGoogleLogin({
|
||||
clientId: env('GOOGLE_CLIENT_ID'),
|
||||
onSuccess: async (response: GoogleLoginResponse | GoogleLoginResponseOffline) => {
|
||||
await loginWithGoogleMutation({ accessToken: (response as GoogleLoginResponse).accessToken });
|
||||
|
||||
handleClose();
|
||||
},
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(setModalState({ modal: 'auth.register', state: { open: false } }));
|
||||
reset();
|
||||
@ -87,37 +79,37 @@ const RegisterModal: React.FC = () => {
|
||||
dispatch(setModalState({ modal: 'auth.login', state: { open: true } }));
|
||||
};
|
||||
|
||||
const handleLoginWithGoogle = () => {
|
||||
signIn();
|
||||
const handleLoginWithGoogle = async (response: CredentialResponse) => {
|
||||
if (response.credential) {
|
||||
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleLoginWithGoogleError });
|
||||
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoginWithGoogleError = () => {
|
||||
toast("Please try logging in using email/password, or use another browser that supports Google's One Tap API.");
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
icon={<HowToReg />}
|
||||
isOpen={isOpen}
|
||||
heading={t('modals.auth.register.heading')}
|
||||
heading={t<string>('modals.auth.register.heading')}
|
||||
handleClose={handleClose}
|
||||
footerChildren={
|
||||
<div className="flex gap-4">
|
||||
{!isEmpty(env('GOOGLE_CLIENT_ID')) && (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outlined"
|
||||
disabled={isLoading}
|
||||
startIcon={<Google />}
|
||||
onClick={handleLoginWithGoogle}
|
||||
>
|
||||
{t('modals.auth.register.actions.google')}
|
||||
</Button>
|
||||
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleLoginWithGoogleError} />
|
||||
)}
|
||||
|
||||
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
||||
{t('modals.auth.register.actions.register')}
|
||||
{t<string>('modals.auth.register.actions.register')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p>{t('modals.auth.register.body')}</p>
|
||||
<p>{t<string>('modals.auth.register.body')}</p>
|
||||
|
||||
<form className="grid gap-4 md:grid-cols-2">
|
||||
<Controller
|
||||
@ -126,7 +118,7 @@ const RegisterModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
autoFocus
|
||||
label={t('modals.auth.register.form.name.label')}
|
||||
label={t<string>('modals.auth.register.form.name.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -139,7 +131,7 @@ const RegisterModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('modals.auth.register.form.username.label')}
|
||||
label={t<string>('modals.auth.register.form.username.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -153,7 +145,7 @@ const RegisterModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
type="email"
|
||||
label={t('modals.auth.register.form.email.label')}
|
||||
label={t<string>('modals.auth.register.form.email.label')}
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
@ -168,7 +160,7 @@ const RegisterModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
type="password"
|
||||
label={t('modals.auth.register.form.password.label')}
|
||||
label={t<string>('modals.auth.register.form.password.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -182,7 +174,7 @@ const RegisterModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
type="password"
|
||||
label={t('modals.auth.register.form.confirm-password.label')}
|
||||
label={t<string>('modals.auth.register.form.confirm-password.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
|
||||
@ -65,15 +65,15 @@ const ResetPasswordModal: React.FC = () => {
|
||||
<BaseModal
|
||||
icon={<LockReset />}
|
||||
isOpen={isOpen}
|
||||
heading={t('modals.auth.reset-password.heading')}
|
||||
heading={t<string>('modals.auth.reset-password.heading')}
|
||||
handleClose={handleClose}
|
||||
footerChildren={
|
||||
<Button type="submit" disabled={isLoading} onClick={handleSubmit(onSubmit)}>
|
||||
{t('modals.auth.reset-password.actions.set-password')}
|
||||
{t<string>('modals.auth.reset-password.actions.set-password')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<p>{t('modals.auth.reset-password.body')}</p>
|
||||
<p>{t<string>('modals.auth.reset-password.body')}</p>
|
||||
|
||||
<form className="grid gap-4 md:grid-cols-2">
|
||||
<Controller
|
||||
@ -83,7 +83,7 @@ const ResetPasswordModal: React.FC = () => {
|
||||
<TextField
|
||||
autoFocus
|
||||
type="password"
|
||||
label={t('modals.auth.reset-password.form.password.label')}
|
||||
label={t<string>('modals.auth.reset-password.form.password.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -97,7 +97,7 @@ const ResetPasswordModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
type="password"
|
||||
label={t('modals.auth.reset-password.form.confirm-password.label')}
|
||||
label={t<string>('modals.auth.reset-password.form.confirm-password.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import DatePicker from '@mui/lab/DatePicker';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { Award, SectionPath } from '@reactive-resume/schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
@ -44,14 +44,14 @@ const AwardModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
const isEditMode = useMemo(() => !!item, [item]);
|
||||
|
||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||
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]);
|
||||
|
||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||
defaultValues: defaultState,
|
||||
@ -101,7 +101,7 @@ const AwardModal: React.FC = () => {
|
||||
<TextField
|
||||
required
|
||||
autoFocus
|
||||
label={t('builder.common.form.title.label')}
|
||||
label={t<string>('builder.common.form.title.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -115,7 +115,7 @@ const AwardModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
required
|
||||
label={t('builder.leftSidebar.sections.awards.form.awarder.label')}
|
||||
label={t<string>('builder.leftSidebar.sections.awards.form.awarder.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -130,11 +130,11 @@ const AwardModal: React.FC = () => {
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
label={t('builder.common.form.date.label')}
|
||||
label={t<string>('builder.common.form.date.label')}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -152,7 +152,7 @@ const AwardModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('builder.common.form.url.label')}
|
||||
label={t<string>('builder.common.form.url.label')}
|
||||
placeholder="https://"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
@ -169,7 +169,7 @@ const AwardModal: React.FC = () => {
|
||||
multiline
|
||||
minRows={3}
|
||||
maxRows={6}
|
||||
label={t('builder.common.form.summary.label')}
|
||||
label={t<string>('builder.common.form.summary.label')}
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import DatePicker from '@mui/lab/DatePicker';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { Certificate, SectionPath } from '@reactive-resume/schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
@ -44,14 +44,14 @@ const CertificateModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
|
||||
const isEditMode = useMemo(() => !!item, [item]);
|
||||
|
||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||
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]);
|
||||
|
||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||
defaultValues: defaultState,
|
||||
@ -101,7 +101,7 @@ const CertificateModal: React.FC = () => {
|
||||
<TextField
|
||||
required
|
||||
autoFocus
|
||||
label={t('builder.common.form.name.label')}
|
||||
label={t<string>('builder.common.form.name.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -115,7 +115,7 @@ const CertificateModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
required
|
||||
label={t('builder.leftSidebar.sections.certifications.form.issuer.label')}
|
||||
label={t<string>('builder.leftSidebar.sections.certifications.form.issuer.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -130,11 +130,11 @@ const CertificateModal: React.FC = () => {
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
label={t('builder.common.form.date.label')}
|
||||
label={t<string>('builder.common.form.date.label')}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -152,7 +152,7 @@ const CertificateModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('builder.common.form.url.label')}
|
||||
label={t<string>('builder.common.form.url.label')}
|
||||
placeholder="https://"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
@ -169,7 +169,7 @@ const CertificateModal: React.FC = () => {
|
||||
multiline
|
||||
minRows={3}
|
||||
maxRows={6}
|
||||
label={t('builder.common.form.summary.label')}
|
||||
label={t<string>('builder.common.form.summary.label')}
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import DatePicker from '@mui/lab/DatePicker';
|
||||
import { Button, Slider, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { Custom } from '@reactive-resume/schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
@ -60,15 +60,15 @@ const CustomModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
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 item: FormData = get(payload, 'item', null);
|
||||
const isEditMode = useMemo(() => !!item, [item]);
|
||||
|
||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||
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]);
|
||||
|
||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||
defaultValues: defaultState,
|
||||
@ -118,7 +118,7 @@ const CustomModal: React.FC = () => {
|
||||
<TextField
|
||||
required
|
||||
autoFocus
|
||||
label={t('builder.common.form.title.label')}
|
||||
label={t<string>('builder.common.form.title.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -131,7 +131,7 @@ const CustomModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('builder.common.form.subtitle.label')}
|
||||
label={t<string>('builder.common.form.subtitle.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -146,11 +146,11 @@ const CustomModal: React.FC = () => {
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
label={t('builder.common.form.start-date.label')}
|
||||
label={t<string>('builder.common.form.start-date.label')}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -170,11 +170,11 @@ const CustomModal: React.FC = () => {
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
label={t('builder.common.form.end-date.label')}
|
||||
label={t<string>('builder.common.form.end-date.label')}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -192,7 +192,7 @@ const CustomModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('builder.common.form.url.label')}
|
||||
label={t<string>('builder.common.form.url.label')}
|
||||
placeholder="https://"
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
@ -207,7 +207,7 @@ const CustomModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('builder.common.form.level.label')}
|
||||
label={t<string>('builder.common.form.level.label')}
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
@ -221,7 +221,7 @@ const CustomModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="col-span-2">
|
||||
<h4 className="mb-3 font-semibold">{t('builder.common.form.levelNum.label')}</h4>
|
||||
<h4 className="mb-3 font-semibold">{t<string>('builder.common.form.levelNum.label')}</h4>
|
||||
|
||||
<div className="px-10">
|
||||
<Slider
|
||||
@ -245,7 +245,7 @@ const CustomModal: React.FC = () => {
|
||||
defaultValue={0}
|
||||
color="secondary"
|
||||
valueLabelDisplay="auto"
|
||||
aria-label={t('builder.common.form.levelNum.label')}
|
||||
aria-label={t<string>('builder.common.form.levelNum.label')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -260,7 +260,7 @@ const CustomModal: React.FC = () => {
|
||||
multiline
|
||||
minRows={3}
|
||||
maxRows={6}
|
||||
label={t('builder.common.form.summary.label')}
|
||||
label={t<string>('builder.common.form.summary.label')}
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||
@ -274,7 +274,7 @@ const CustomModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<ArrayInput
|
||||
label={t('builder.common.form.keywords.label')}
|
||||
label={t<string>('builder.common.form.keywords.label')}
|
||||
value={field.value as string[]}
|
||||
onChange={field.onChange}
|
||||
errors={fieldState.error}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import DatePicker from '@mui/lab/DatePicker';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { Education, SectionPath } from '@reactive-resume/schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
@ -57,14 +57,14 @@ const EducationModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
const isEditMode = useMemo(() => !!item, [item]);
|
||||
|
||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||
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]);
|
||||
|
||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||
defaultValues: defaultState,
|
||||
@ -114,7 +114,7 @@ const EducationModal: React.FC = () => {
|
||||
<TextField
|
||||
required
|
||||
autoFocus
|
||||
label={t('builder.leftSidebar.sections.education.form.institution.label')}
|
||||
label={t<string>('builder.leftSidebar.sections.education.form.institution.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -128,7 +128,7 @@ const EducationModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
required
|
||||
label={t('builder.leftSidebar.sections.education.form.degree.label')}
|
||||
label={t<string>('builder.leftSidebar.sections.education.form.degree.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -141,7 +141,7 @@ const EducationModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('builder.leftSidebar.sections.education.form.area-study.label')}
|
||||
label={t<string>('builder.leftSidebar.sections.education.form.area-study.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -154,7 +154,7 @@ const EducationModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('builder.leftSidebar.sections.education.form.grade.label')}
|
||||
label={t<string>('builder.leftSidebar.sections.education.form.grade.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -169,11 +169,11 @@ const EducationModal: React.FC = () => {
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
label={t('builder.common.form.start-date.label')}
|
||||
label={t<string>('builder.common.form.start-date.label')}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -193,17 +193,17 @@ const EducationModal: React.FC = () => {
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
label={t('builder.common.form.end-date.label')}
|
||||
label={t<string>('builder.common.form.end-date.label')}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || t('builder.common.form.end-date.help-text')}
|
||||
helperText={fieldState.error?.message || t<string>('builder.common.form.end-date.help-text')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -215,7 +215,7 @@ const EducationModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('builder.common.form.url.label')}
|
||||
label={t<string>('builder.common.form.url.label')}
|
||||
placeholder="https://"
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
@ -233,7 +233,7 @@ const EducationModal: React.FC = () => {
|
||||
multiline
|
||||
minRows={3}
|
||||
maxRows={6}
|
||||
label={t('builder.common.form.summary.label')}
|
||||
label={t<string>('builder.common.form.summary.label')}
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||
@ -247,7 +247,7 @@ const EducationModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<ArrayInput
|
||||
label={t('builder.leftSidebar.sections.education.form.courses.label')}
|
||||
label={t<string>('builder.leftSidebar.sections.education.form.courses.label')}
|
||||
value={field.value as string[]}
|
||||
onChange={field.onChange}
|
||||
errors={fieldState.error}
|
||||
|
||||
@ -35,14 +35,14 @@ const InterestModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
const isEditMode = useMemo(() => !!item, [item]);
|
||||
|
||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||
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]);
|
||||
|
||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||
defaultValues: defaultState,
|
||||
@ -92,7 +92,7 @@ const InterestModal: React.FC = () => {
|
||||
<TextField
|
||||
required
|
||||
autoFocus
|
||||
label={t('builder.common.form.name.label')}
|
||||
label={t<string>('builder.common.form.name.label')}
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
@ -106,7 +106,7 @@ const InterestModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<ArrayInput
|
||||
label={t('builder.common.form.keywords.label')}
|
||||
label={t<string>('builder.common.form.keywords.label')}
|
||||
value={field.value as string[]}
|
||||
onChange={field.onChange}
|
||||
errors={fieldState.error}
|
||||
|
||||
@ -36,14 +36,14 @@ const LanguageModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
const isEditMode = useMemo(() => !!item, [item]);
|
||||
|
||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||
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]);
|
||||
|
||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||
defaultValues: defaultState,
|
||||
@ -93,7 +93,7 @@ const LanguageModal: React.FC = () => {
|
||||
<TextField
|
||||
required
|
||||
autoFocus
|
||||
label={t('builder.common.form.name.label')}
|
||||
label={t<string>('builder.common.form.name.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -107,7 +107,7 @@ const LanguageModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
required
|
||||
label={t('builder.common.form.level.label')}
|
||||
label={t<string>('builder.common.form.level.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -120,7 +120,7 @@ const LanguageModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="col-span-2">
|
||||
<h4 className="mb-3 font-semibold">{t('builder.common.form.levelNum.label')}</h4>
|
||||
<h4 className="mb-3 font-semibold">{t<string>('builder.common.form.levelNum.label')}</h4>
|
||||
|
||||
<div className="px-10">
|
||||
<Slider
|
||||
@ -144,7 +144,7 @@ const LanguageModal: React.FC = () => {
|
||||
defaultValue={0}
|
||||
color="secondary"
|
||||
valueLabelDisplay="auto"
|
||||
aria-label={t('builder.common.form.levelNum.label')}
|
||||
aria-label={t<string>('builder.common.form.levelNum.label')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -42,11 +42,11 @@ const ProfileModal: React.FC = () => {
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
const isEditMode = useMemo(() => !!item, [item]);
|
||||
|
||||
const addText = t('builder.common.actions.add', {
|
||||
token: t('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
||||
const addText = t<string>('builder.common.actions.add', {
|
||||
token: t<string>('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
||||
});
|
||||
const editText = t('builder.common.actions.edit', {
|
||||
token: t('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
||||
const editText = t<string>('builder.common.actions.edit', {
|
||||
token: t<string>('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
||||
});
|
||||
|
||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||
@ -97,7 +97,7 @@ const ProfileModal: React.FC = () => {
|
||||
<TextField
|
||||
required
|
||||
autoFocus
|
||||
label={t('builder.leftSidebar.sections.profiles.form.network.label')}
|
||||
label={t<string>('builder.leftSidebar.sections.profiles.form.network.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -111,7 +111,7 @@ const ProfileModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
required
|
||||
label={t('builder.leftSidebar.sections.profiles.form.username.label')}
|
||||
label={t<string>('builder.leftSidebar.sections.profiles.form.username.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
InputProps={{
|
||||
@ -127,7 +127,7 @@ const ProfileModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('builder.common.form.url.label')}
|
||||
label={t<string>('builder.common.form.url.label')}
|
||||
className="col-span-2"
|
||||
placeholder="https://"
|
||||
error={!!fieldState.error}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import DatePicker from '@mui/lab/DatePicker';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { Project, SectionPath } from '@reactive-resume/schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
@ -53,14 +53,14 @@ const ProjectModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
const isEditMode = useMemo(() => !!item, [item]);
|
||||
|
||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||
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]);
|
||||
|
||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||
defaultValues: defaultState,
|
||||
@ -110,7 +110,7 @@ const ProjectModal: React.FC = () => {
|
||||
<TextField
|
||||
required
|
||||
autoFocus
|
||||
label={t('builder.common.form.name.label')}
|
||||
label={t<string>('builder.common.form.name.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -124,7 +124,7 @@ const ProjectModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
required
|
||||
label={t('builder.common.form.description.label')}
|
||||
label={t<string>('builder.common.form.description.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -139,11 +139,11 @@ const ProjectModal: React.FC = () => {
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
label={t('builder.common.form.start-date.label')}
|
||||
label={t<string>('builder.common.form.start-date.label')}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -163,11 +163,11 @@ const ProjectModal: React.FC = () => {
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
label={t('builder.common.form.end-date.label')}
|
||||
label={t<string>('builder.common.form.end-date.label')}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -185,7 +185,7 @@ const ProjectModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('builder.common.form.url.label')}
|
||||
label={t<string>('builder.common.form.url.label')}
|
||||
placeholder="https://"
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
@ -203,7 +203,7 @@ const ProjectModal: React.FC = () => {
|
||||
multiline
|
||||
minRows={3}
|
||||
maxRows={6}
|
||||
label={t('builder.common.form.summary.label')}
|
||||
label={t<string>('builder.common.form.summary.label')}
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||
@ -217,7 +217,7 @@ const ProjectModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<ArrayInput
|
||||
label={t('builder.common.form.keywords.label')}
|
||||
label={t<string>('builder.common.form.keywords.label')}
|
||||
value={field.value as string[]}
|
||||
onChange={field.onChange}
|
||||
errors={fieldState.error}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import DatePicker from '@mui/lab/DatePicker';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { Publication, SectionPath } from '@reactive-resume/schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
@ -44,14 +44,14 @@ const PublicationModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
const isEditMode = useMemo(() => !!item, [item]);
|
||||
|
||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||
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]);
|
||||
|
||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||
defaultValues: defaultState,
|
||||
@ -101,7 +101,7 @@ const PublicationModal: React.FC = () => {
|
||||
<TextField
|
||||
required
|
||||
autoFocus
|
||||
label={t('builder.common.form.name.label')}
|
||||
label={t<string>('builder.common.form.name.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -115,7 +115,7 @@ const PublicationModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
required
|
||||
label={t('builder.leftSidebar.sections.publications.form.publisher.label')}
|
||||
label={t<string>('builder.leftSidebar.sections.publications.form.publisher.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -130,11 +130,11 @@ const PublicationModal: React.FC = () => {
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
label={t('builder.common.form.date.label')}
|
||||
label={t<string>('builder.common.form.date.label')}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -152,7 +152,7 @@ const PublicationModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('builder.common.form.url.label')}
|
||||
label={t<string>('builder.common.form.url.label')}
|
||||
placeholder="https://"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
@ -169,7 +169,7 @@ const PublicationModal: React.FC = () => {
|
||||
multiline
|
||||
minRows={3}
|
||||
maxRows={6}
|
||||
label={t('builder.common.form.summary.label')}
|
||||
label={t<string>('builder.common.form.summary.label')}
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||
|
||||
@ -41,14 +41,14 @@ const ReferenceModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
const isEditMode = useMemo(() => !!item, [item]);
|
||||
|
||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||
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]);
|
||||
|
||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||
defaultValues: defaultState,
|
||||
@ -98,7 +98,7 @@ const ReferenceModal: React.FC = () => {
|
||||
<TextField
|
||||
required
|
||||
autoFocus
|
||||
label={t('builder.common.form.name.label')}
|
||||
label={t<string>('builder.common.form.name.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -112,7 +112,7 @@ const ReferenceModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
required
|
||||
label={t('builder.leftSidebar.sections.references.form.relationship.label')}
|
||||
label={t<string>('builder.leftSidebar.sections.references.form.relationship.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -125,7 +125,7 @@ const ReferenceModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('builder.common.form.phone.label')}
|
||||
label={t<string>('builder.common.form.phone.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -138,7 +138,7 @@ const ReferenceModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('builder.common.form.email.label')}
|
||||
label={t<string>('builder.common.form.email.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -154,7 +154,7 @@ const ReferenceModal: React.FC = () => {
|
||||
multiline
|
||||
minRows={3}
|
||||
maxRows={6}
|
||||
label={t('builder.common.form.summary.label')}
|
||||
label={t<string>('builder.common.form.summary.label')}
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||
|
||||
@ -39,14 +39,14 @@ const SkillModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
const isEditMode = useMemo(() => !!item, [item]);
|
||||
|
||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||
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]);
|
||||
|
||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||
defaultValues: defaultState,
|
||||
@ -96,7 +96,7 @@ const SkillModal: React.FC = () => {
|
||||
<TextField
|
||||
required
|
||||
autoFocus
|
||||
label={t('builder.common.form.name.label')}
|
||||
label={t<string>('builder.common.form.name.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -109,7 +109,7 @@ const SkillModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('builder.common.form.level.label')}
|
||||
label={t<string>('builder.common.form.level.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -122,7 +122,7 @@ const SkillModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="col-span-2">
|
||||
<h4 className="mb-3 font-semibold">{t('builder.common.form.levelNum.label')}</h4>
|
||||
<h4 className="mb-3 font-semibold">{t<string>('builder.common.form.levelNum.label')}</h4>
|
||||
|
||||
<div className="px-3">
|
||||
<Slider
|
||||
@ -146,7 +146,7 @@ const SkillModal: React.FC = () => {
|
||||
defaultValue={0}
|
||||
color="secondary"
|
||||
valueLabelDisplay="auto"
|
||||
aria-label={t('builder.common.form.levelNum.label')}
|
||||
aria-label={t<string>('builder.common.form.levelNum.label')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -158,7 +158,7 @@ const SkillModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<ArrayInput
|
||||
label={t('builder.common.form.keywords.label')}
|
||||
label={t<string>('builder.common.form.keywords.label')}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
errors={fieldState.error}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import DatePicker from '@mui/lab/DatePicker';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { SectionPath, Volunteer } from '@reactive-resume/schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
@ -50,14 +50,14 @@ const VolunteerModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
const isEditMode = useMemo(() => !!item, [item]);
|
||||
|
||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||
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]);
|
||||
|
||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||
defaultValues: defaultState,
|
||||
@ -107,7 +107,7 @@ const VolunteerModal: React.FC = () => {
|
||||
<TextField
|
||||
required
|
||||
autoFocus
|
||||
label={t('builder.leftSidebar.sections.volunteer.form.organization.label')}
|
||||
label={t<string>('builder.leftSidebar.sections.volunteer.form.organization.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -121,7 +121,7 @@ const VolunteerModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
required
|
||||
label={t('builder.common.form.position.label')}
|
||||
label={t<string>('builder.common.form.position.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -136,11 +136,11 @@ const VolunteerModal: React.FC = () => {
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
label={t('builder.common.form.start-date.label')}
|
||||
label={t<string>('builder.common.form.start-date.label')}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -160,11 +160,11 @@ const VolunteerModal: React.FC = () => {
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
label={t('builder.common.form.end-date.label')}
|
||||
label={t<string>('builder.common.form.end-date.label')}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -182,7 +182,7 @@ const VolunteerModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('builder.common.form.url.label')}
|
||||
label={t<string>('builder.common.form.url.label')}
|
||||
placeholder="https://"
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
@ -200,7 +200,7 @@ const VolunteerModal: React.FC = () => {
|
||||
multiline
|
||||
minRows={3}
|
||||
maxRows={6}
|
||||
label={t('builder.common.form.summary.label')}
|
||||
label={t<string>('builder.common.form.summary.label')}
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import DatePicker from '@mui/lab/DatePicker';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { SectionPath, WorkExperience } from '@reactive-resume/schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
@ -50,14 +50,14 @@ const WorkModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
const isEditMode = useMemo(() => !!item, [item]);
|
||||
|
||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||
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]);
|
||||
|
||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||
defaultValues: defaultState,
|
||||
@ -107,7 +107,7 @@ const WorkModal: React.FC = () => {
|
||||
<TextField
|
||||
required
|
||||
autoFocus
|
||||
label={t('builder.common.form.name.label')}
|
||||
label={t<string>('builder.common.form.name.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -121,7 +121,7 @@ const WorkModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
required
|
||||
label={t('builder.common.form.position.label')}
|
||||
label={t<string>('builder.common.form.position.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -136,11 +136,11 @@ const WorkModal: React.FC = () => {
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
label={t('builder.common.form.start-date.label')}
|
||||
label={t<string>('builder.common.form.start-date.label')}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
@ -160,17 +160,17 @@ const WorkModal: React.FC = () => {
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
label={t('builder.common.form.end-date.label')}
|
||||
label={t<string>('builder.common.form.end-date.label')}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || t('builder.common.form.end-date.help-text')}
|
||||
helperText={fieldState.error?.message || t<string>('builder.common.form.end-date.help-text')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -182,7 +182,7 @@ const WorkModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('builder.common.form.url.label')}
|
||||
label={t<string>('builder.common.form.url.label')}
|
||||
placeholder="https://"
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
@ -200,7 +200,7 @@ const WorkModal: React.FC = () => {
|
||||
multiline
|
||||
minRows={3}
|
||||
maxRows={6}
|
||||
label={t('builder.common.form.summary.label')}
|
||||
label={t<string>('builder.common.form.summary.label')}
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||
|
||||
@ -58,7 +58,7 @@ const CreateResumeModal: React.FC = () => {
|
||||
const slug = name
|
||||
? name
|
||||
.toLowerCase()
|
||||
.replace(/[`~!@#$%^&*()_|+=?;:'",.<>{}[]\\\/]/gi, '')
|
||||
.replace(/[^\w\s]/gi, '')
|
||||
.replace(/[ ]/gi, '-')
|
||||
: '';
|
||||
|
||||
@ -69,7 +69,8 @@ const CreateResumeModal: React.FC = () => {
|
||||
try {
|
||||
await mutateAsync({ name, slug, public: isPublic });
|
||||
|
||||
queryClient.invalidateQueries(RESUMES_QUERY);
|
||||
await queryClient.invalidateQueries(RESUMES_QUERY);
|
||||
|
||||
handleClose();
|
||||
} catch (error: any) {
|
||||
toast.error(error.message);
|
||||
@ -85,15 +86,15 @@ const CreateResumeModal: React.FC = () => {
|
||||
<BaseModal
|
||||
isOpen={isOpen}
|
||||
icon={<Add />}
|
||||
heading={t('modals.dashboard.create-resume.heading')}
|
||||
heading={t<string>('modals.dashboard.create-resume.heading')}
|
||||
handleClose={handleClose}
|
||||
footerChildren={
|
||||
<Button type="submit" disabled={isLoading} onClick={handleSubmit(onSubmit)}>
|
||||
{t('modals.dashboard.create-resume.actions.create-resume')}
|
||||
{t<string>('modals.dashboard.create-resume.actions.create-resume')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<p>{t('modals.dashboard.create-resume.body')}</p>
|
||||
<p>{t<string>('modals.dashboard.create-resume.body')}</p>
|
||||
|
||||
<form className="grid gap-4">
|
||||
<Controller
|
||||
@ -102,7 +103,7 @@ const CreateResumeModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
autoFocus
|
||||
label={t('modals.dashboard.create-resume.form.name.label')}
|
||||
label={t<string>('modals.dashboard.create-resume.form.name.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -115,7 +116,7 @@ const CreateResumeModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('modals.dashboard.create-resume.form.slug.label')}
|
||||
label={t<string>('modals.dashboard.create-resume.form.slug.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -125,7 +126,7 @@ const CreateResumeModal: React.FC = () => {
|
||||
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
label={t('modals.dashboard.create-resume.form.public.label') as string}
|
||||
label={t<string>('modals.dashboard.create-resume.form.public.label')}
|
||||
control={
|
||||
<Controller
|
||||
name="isPublic"
|
||||
|
||||
@ -63,7 +63,7 @@ const ImportExternalModal: React.FC = () => {
|
||||
const file = event.target.files[0];
|
||||
|
||||
if (file.size > FILE_UPLOAD_MAX_SIZE) {
|
||||
toast.error(t('common.toast.error.upload-file-size'));
|
||||
toast.error(t<string>('common.toast.error.upload-file-size'));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -78,13 +78,13 @@ const ImportExternalModal: React.FC = () => {
|
||||
<BaseModal
|
||||
isOpen={isOpen}
|
||||
icon={<ImportExport />}
|
||||
heading={t('modals.dashboard.import-external.heading')}
|
||||
heading={t<string>('modals.dashboard.import-external.heading')}
|
||||
handleClose={handleClose}
|
||||
>
|
||||
<div className="grid gap-5">
|
||||
<h2 className="inline-flex items-center gap-2 text-lg font-medium">
|
||||
<LinkedIn />
|
||||
{t('modals.dashboard.import-external.linkedin.heading')}
|
||||
{t<string>('modals.dashboard.import-external.linkedin.heading')}
|
||||
</h2>
|
||||
|
||||
<p className="mb-2">
|
||||
@ -110,7 +110,7 @@ const ImportExternalModal: React.FC = () => {
|
||||
startIcon={<UploadFile />}
|
||||
onClick={() => handleClick('linkedin')}
|
||||
>
|
||||
{t('modals.dashboard.import-external.linkedin.actions.upload-archive')}
|
||||
{t<string>('modals.dashboard.import-external.linkedin.actions.upload-archive')}
|
||||
</Button>
|
||||
|
||||
<input
|
||||
@ -128,7 +128,7 @@ const ImportExternalModal: React.FC = () => {
|
||||
<div className="grid gap-5">
|
||||
<h2 className="inline-flex items-center gap-2 text-lg font-medium">
|
||||
<Code />
|
||||
{t('modals.dashboard.import-external.json-resume.heading')}
|
||||
{t<string>('modals.dashboard.import-external.json-resume.heading')}
|
||||
</h2>
|
||||
|
||||
<p className="mb-2">
|
||||
@ -154,7 +154,7 @@ const ImportExternalModal: React.FC = () => {
|
||||
startIcon={<UploadFile />}
|
||||
onClick={() => handleClick('json-resume')}
|
||||
>
|
||||
{t('modals.dashboard.import-external.json-resume.actions.upload-json')}
|
||||
{t<string>('modals.dashboard.import-external.json-resume.actions.upload-json')}
|
||||
</Button>
|
||||
|
||||
<input
|
||||
@ -172,10 +172,10 @@ const ImportExternalModal: React.FC = () => {
|
||||
<div className="grid gap-5">
|
||||
<h2 className="inline-flex items-center gap-2 text-lg font-medium">
|
||||
<TrackChanges />
|
||||
{t('modals.dashboard.import-external.reactive-resume.heading')}
|
||||
{t<string>('modals.dashboard.import-external.reactive-resume.heading')}
|
||||
</h2>
|
||||
|
||||
<p className="mb-2">{t('modals.dashboard.import-external.reactive-resume.body')}</p>
|
||||
<p className="mb-2">{t<string>('modals.dashboard.import-external.reactive-resume.body')}</p>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
@ -184,7 +184,7 @@ const ImportExternalModal: React.FC = () => {
|
||||
startIcon={<UploadFile />}
|
||||
onClick={() => handleClick('reactive-resume')}
|
||||
>
|
||||
{t('modals.dashboard.import-external.reactive-resume.actions.upload-json')}
|
||||
{t<string>('modals.dashboard.import-external.reactive-resume.actions.upload-json')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@ -193,7 +193,7 @@ const ImportExternalModal: React.FC = () => {
|
||||
startIcon={<UploadFile />}
|
||||
onClick={() => handleClick('reactive-resume-v2')}
|
||||
>
|
||||
{t('modals.dashboard.import-external.reactive-resume.actions.upload-json-v2')}
|
||||
{t<string>('modals.dashboard.import-external.reactive-resume.actions.upload-json-v2')}
|
||||
</Button>
|
||||
|
||||
<input
|
||||
|
||||
@ -56,7 +56,7 @@ const RenameResumeModal: React.FC = () => {
|
||||
const slug = name
|
||||
? name
|
||||
.toLowerCase()
|
||||
.replace(/[`~!@#$%^&*()_|+=?;:'",.<>{}[]\\\/]/gi, '')
|
||||
.replace(/[^\w\s]/gi, '')
|
||||
.replace(/[ ]/gi, '-')
|
||||
: '';
|
||||
|
||||
@ -92,11 +92,11 @@ const RenameResumeModal: React.FC = () => {
|
||||
<BaseModal
|
||||
icon={<DriveFileRenameOutline />}
|
||||
isOpen={isOpen}
|
||||
heading={t('modals.dashboard.rename-resume.heading')}
|
||||
heading={t<string>('modals.dashboard.rename-resume.heading')}
|
||||
handleClose={handleClose}
|
||||
footerChildren={
|
||||
<Button type="submit" disabled={isLoading} onClick={handleSubmit(onSubmit)}>
|
||||
{t('modals.dashboard.rename-resume.actions.rename-resume')}
|
||||
{t<string>('modals.dashboard.rename-resume.actions.rename-resume')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
@ -107,7 +107,7 @@ const RenameResumeModal: React.FC = () => {
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
autoFocus
|
||||
label={t('modals.dashboard.rename-resume.form.name.label')}
|
||||
label={t<string>('modals.dashboard.rename-resume.form.name.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -120,7 +120,7 @@ const RenameResumeModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
label={t('modals.dashboard.rename-resume.form.slug.label')}
|
||||
label={t<string>('modals.dashboard.rename-resume.form.slug.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
|
||||
@ -3,7 +3,37 @@ const path = require('path');
|
||||
const i18nConfig = {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['ar', 'bn', 'da', 'de', 'en', 'es', 'fr', 'hi', 'it', 'kn', 'ml', 'pl', 'ta', 'tr', 'zh'],
|
||||
locales: [
|
||||
'ar',
|
||||
'bg',
|
||||
'bn',
|
||||
'cs',
|
||||
'da',
|
||||
'de',
|
||||
'el',
|
||||
'en',
|
||||
'es',
|
||||
'fa',
|
||||
'fi',
|
||||
'fr',
|
||||
'he',
|
||||
'hi',
|
||||
'hu',
|
||||
'id',
|
||||
'it',
|
||||
'kn',
|
||||
'ml',
|
||||
'nl',
|
||||
'or',
|
||||
'pl',
|
||||
'pt',
|
||||
'ru',
|
||||
'sv',
|
||||
'ta',
|
||||
'tr',
|
||||
'vi',
|
||||
'zh',
|
||||
],
|
||||
},
|
||||
nsSeparator: '.',
|
||||
localePath: path.resolve('./public/locales'),
|
||||
|
||||
@ -12,7 +12,7 @@ const nextConfig = {
|
||||
},
|
||||
|
||||
images: {
|
||||
domains: ['www.gravatar.com'],
|
||||
domains: ['cdn.rxresu.me', 'www.gravatar.com'],
|
||||
},
|
||||
|
||||
async rewrites() {
|
||||
|
||||
@ -2,77 +2,82 @@
|
||||
"name": "@reactive-resume/client",
|
||||
"scripts": {
|
||||
"dev": "react-env --prefix PUBLIC -- next dev",
|
||||
"lint": "next lint --fix",
|
||||
"build": "next build && npm run sitemap",
|
||||
"start": "react-env --prefix PUBLIC -- next start",
|
||||
"lint": "next lint --fix",
|
||||
"sitemap": "next-sitemap --config next-sitemap.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@beam-australia/react-env": "^3.1.1",
|
||||
"@emotion/css": "^11.7.1",
|
||||
"@emotion/react": "^11.8.2",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@hookform/resolvers": "2.8.8",
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@mui/icons-material": "^5.5.1",
|
||||
"@mui/lab": "^5.0.0-alpha.73",
|
||||
"@mui/material": "^5.5.1",
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
"axios": "^0.26.1",
|
||||
"clsx": "^1.1.1",
|
||||
"dayjs": "^1.11.0",
|
||||
"@date-io/dayjs": "^2.15.0",
|
||||
"@emotion/css": "^11.10.0",
|
||||
"@emotion/react": "^11.10.0",
|
||||
"@emotion/styled": "^11.10.0",
|
||||
"@hello-pangea/dnd": "^16.0.0",
|
||||
"@hookform/resolvers": "2.9.7",
|
||||
"@monaco-editor/react": "^4.4.5",
|
||||
"@mui/icons-material": "^5.10.2",
|
||||
"@mui/lab": "^5.0.0-alpha.96",
|
||||
"@mui/material": "^5.10.2",
|
||||
"@mui/system": "^5.10.2",
|
||||
"@mui/x-date-pickers": "5.0.0-beta.7",
|
||||
"@next/env": "^12.2.5",
|
||||
"@react-oauth/google": "^0.2.6",
|
||||
"@reduxjs/toolkit": "^1.8.5",
|
||||
"axios": "^0.27.2",
|
||||
"clsx": "^1.2.1",
|
||||
"dayjs": "^1.11.5",
|
||||
"downloadjs": "^1.4.7",
|
||||
"joi": "^17.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"md5-hex": "^4.0.0",
|
||||
"monaco-editor": "^0.33.0",
|
||||
"nanoid": "^3.3.1",
|
||||
"next": "12.1.0",
|
||||
"next-i18next": "^10.5.0",
|
||||
"react": ">=17",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-colorful": "^5.5.1",
|
||||
"react-dnd": "^15.1.1",
|
||||
"react-dnd-html5-backend": "^15.1.2",
|
||||
"react-dom": ">=17",
|
||||
"react-google-login": "^5.2.2",
|
||||
"react-hook-form": "^7.28.0",
|
||||
"react-hot-toast": "2.2.0",
|
||||
"react-hotkeys-hook": "^3.4.4",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-markdown": "^8.0.1",
|
||||
"react-query": "^3.34.16",
|
||||
"react-redux": "^7.2.6",
|
||||
"monaco-editor": "^0.34.0",
|
||||
"nanoid": "^3.3.4",
|
||||
"next": "12.2.5",
|
||||
"next-i18next": "^12.0.0",
|
||||
"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.34.2",
|
||||
"react-hot-toast": "2.3.0",
|
||||
"react-hotkeys-hook": "^3.4.7",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-query": "^3.39.2",
|
||||
"react-redux": "^8.0.2",
|
||||
"react-zoom-pan-pinch": "^2.1.3",
|
||||
"redux": "^4.1.2",
|
||||
"redux": "^4.2.0",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.1.3",
|
||||
"redux-saga": "^1.2.1",
|
||||
"redux-undo": "^1.0.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sharp": "^0.30.3",
|
||||
"sharp": "^0.30.7",
|
||||
"uuid": "^8.3.2",
|
||||
"webfontloader": "^1.6.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.7",
|
||||
"@babel/core": "^7.18.13",
|
||||
"@reactive-resume/schema": "workspace:*",
|
||||
"@tailwindcss/typography": "^0.5.2",
|
||||
"@tailwindcss/typography": "^0.5.4",
|
||||
"@types/downloadjs": "^1.4.3",
|
||||
"@types/lodash": "^4.14.180",
|
||||
"@types/node": "17.0.21",
|
||||
"@types/react": "17.0.40",
|
||||
"@types/react-beautiful-dnd": "^13.1.2",
|
||||
"@types/react-redux": "^7.1.23",
|
||||
"@types/tailwindcss": "^3.0.9",
|
||||
"@types/lodash": "^4.14.184",
|
||||
"@types/node": "^18.7.13",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-redux": "^7.1.24",
|
||||
"@types/tailwindcss": "^3.0.11",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/webfontloader": "^1.6.34",
|
||||
"autoprefixer": "^10.4.3",
|
||||
"eslint": "^8.11.0",
|
||||
"eslint-config-next": "12.1.0",
|
||||
"next-sitemap": "^2.5.10",
|
||||
"postcss": "^8.4.11",
|
||||
"prettier": "^2.6.0",
|
||||
"sass": "^1.49.9",
|
||||
"tailwindcss": "^3.0.23",
|
||||
"typescript": "<4.6.0"
|
||||
"autoprefixer": "^10.4.8",
|
||||
"csstype": "^3.1.0",
|
||||
"eslint-config-next": "^12.2.5",
|
||||
"eslint-plugin-tailwindcss": "^3.6.0",
|
||||
"next-sitemap": "^3.1.21",
|
||||
"postcss": "^8.4.16",
|
||||
"sass": "^1.54.5",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"typescript": "^4.7.4"
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,7 +62,7 @@ const Build: NextPage<Props> = ({ username, slug }) => {
|
||||
<div className={styles.container}>
|
||||
<Head>
|
||||
<title>
|
||||
{resume.name} | {t('common.title')}
|
||||
{resume.name} | {t<string>('common.title')}
|
||||
</title>
|
||||
</Head>
|
||||
|
||||
|
||||
@ -51,7 +51,7 @@ const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData && !isEmpty(initialData)) {
|
||||
@ -59,6 +59,14 @@ const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
|
||||
}
|
||||
}, [dispatch, initialData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(resume) && router.locale !== resume.metadata.locale) {
|
||||
const { pathname, asPath, query } = router;
|
||||
|
||||
router.push({ pathname, query }, asPath, { locale: resume.metadata.locale });
|
||||
}
|
||||
}, [resume, router]);
|
||||
|
||||
useQuery<Resume>(`resume/${username}/${slug}`, () => fetchResumeByIdentifier({ username, slug }), {
|
||||
initialData,
|
||||
retry: false,
|
||||
|
||||
@ -3,7 +3,6 @@ import clsx from 'clsx';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { GetServerSideProps, NextPage } from 'next';
|
||||
import { useRouter } from 'next/router';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
@ -55,22 +54,14 @@ export const getServerSideProps: GetServerSideProps<Props | Promise<Props>, Quer
|
||||
};
|
||||
|
||||
const Printer: NextPage<Props> = ({ resume: initialData, locale }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData) dispatch(setResume(initialData));
|
||||
}, [dispatch, initialData]);
|
||||
|
||||
useEffect(() => {
|
||||
const { pathname, asPath, query } = router;
|
||||
|
||||
router.push({ pathname, query }, asPath, { locale });
|
||||
}, [router, locale]);
|
||||
|
||||
if (!resume || isEmpty(resume)) return null;
|
||||
|
||||
const layout: string[][][] = get(resume, 'metadata.layout', []);
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import '@/styles/globals.scss';
|
||||
|
||||
import DateAdapter from '@mui/lab/AdapterDayjs';
|
||||
import LocalizationProvider from '@mui/lab/LocalizationProvider';
|
||||
import env from '@beam-australia/react-env';
|
||||
import DayjsAdapter from '@date-io/dayjs';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers';
|
||||
import { GoogleOAuthProvider } from '@react-oauth/google';
|
||||
import type { AppProps } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
import { appWithTranslation } from 'next-i18next';
|
||||
@ -31,24 +33,26 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
</Head>
|
||||
|
||||
<ReduxProvider store={store}>
|
||||
<LocalizationProvider dateAdapter={DateAdapter}>
|
||||
<LocalizationProvider dateAdapter={DayjsAdapter}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<WrapperRegistry>
|
||||
<Loading />
|
||||
<GoogleOAuthProvider clientId={env('GOOGLE_CLIENT_ID')}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<WrapperRegistry>
|
||||
<Loading />
|
||||
|
||||
<Component {...pageProps} />
|
||||
<Component {...pageProps} />
|
||||
|
||||
<ModalWrapper />
|
||||
<Toaster
|
||||
position="bottom-right"
|
||||
toastOptions={{
|
||||
duration: 4000,
|
||||
className: 'toast',
|
||||
}}
|
||||
/>
|
||||
</WrapperRegistry>
|
||||
</QueryClientProvider>
|
||||
<ModalWrapper />
|
||||
<Toaster
|
||||
position="bottom-right"
|
||||
toastOptions={{
|
||||
duration: 4000,
|
||||
className: 'toast',
|
||||
}}
|
||||
/>
|
||||
</WrapperRegistry>
|
||||
</QueryClientProvider>
|
||||
</GoogleOAuthProvider>
|
||||
</PersistGate>
|
||||
</LocalizationProvider>
|
||||
</ReduxProvider>
|
||||
|
||||
@ -4,7 +4,9 @@ import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useEffect } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { ActionCreators } from 'redux-undo';
|
||||
|
||||
import ResumeCard from '@/components/dashboard/ResumeCard';
|
||||
import ResumePreview from '@/components/dashboard/ResumePreview';
|
||||
@ -12,6 +14,7 @@ import Avatar from '@/components/shared/Avatar';
|
||||
import Logo from '@/components/shared/Logo';
|
||||
import { RESUMES_QUERY } from '@/constants/index';
|
||||
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' }) => {
|
||||
@ -25,15 +28,21 @@ export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => {
|
||||
const Dashboard: NextPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { data } = useQuery(RESUMES_QUERY, fetchResumes);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(ActionCreators.clearHistory());
|
||||
}, []);
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Head>
|
||||
<title>
|
||||
{t('dashboard.title')} | {t('common.title')}
|
||||
{t<string>('dashboard.title')} | {t<string>('common.title')}
|
||||
</title>
|
||||
</Head>
|
||||
|
||||
@ -51,15 +60,15 @@ const Dashboard: NextPage = () => {
|
||||
<ResumeCard
|
||||
modal="dashboard.create-resume"
|
||||
icon={Add}
|
||||
title={t('dashboard.create-resume.title')}
|
||||
subtitle={t('dashboard.create-resume.subtitle')}
|
||||
title={t<string>('dashboard.create-resume.title')}
|
||||
subtitle={t<string>('dashboard.create-resume.subtitle')}
|
||||
/>
|
||||
|
||||
<ResumeCard
|
||||
modal="dashboard.import-external"
|
||||
icon={ImportExport}
|
||||
title={t('dashboard.import-external.title')}
|
||||
subtitle={t('dashboard.import-external.subtitle')}
|
||||
title={t<string>('dashboard.import-external.title')}
|
||||
subtitle={t<string>('dashboard.import-external.subtitle')}
|
||||
/>
|
||||
|
||||
{data.map((resume) => (
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Link as LinkIcon } from '@mui/icons-material';
|
||||
import { DarkMode, LightMode, Link as LinkIcon } from '@mui/icons-material';
|
||||
import { Masonry } from '@mui/lab';
|
||||
import { Button } from '@mui/material';
|
||||
import { Button, IconButton, NoSsr } from '@mui/material';
|
||||
import type { GetStaticProps, NextPage } from 'next';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
@ -11,15 +11,16 @@ import Testimony from '@/components/landing/Testimony';
|
||||
import Footer from '@/components/shared/Footer';
|
||||
import LanguageSwitcher from '@/components/shared/LanguageSwitcher';
|
||||
import Logo from '@/components/shared/Logo';
|
||||
import NoSSR from '@/components/shared/NoSSR';
|
||||
import { screenshots } from '@/config/screenshots';
|
||||
import { FLAG_DISABLE_SIGNUPS } from '@/constants/flags';
|
||||
import testimonials from '@/data/testimonials';
|
||||
import { logout } from '@/store/auth/authSlice';
|
||||
import { setTheme } from '@/store/build/buildSlice';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setModalState } from '@/store/modal/modalSlice';
|
||||
import styles from '@/styles/pages/Home.module.scss';
|
||||
|
||||
import { DONATION_URL, GITHUB_URL } from '../constants';
|
||||
import { DIGITALOCEAN_URL, DONATION_URL, GITHUB_URL } from '../constants';
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => {
|
||||
return {
|
||||
@ -34,12 +35,15 @@ const Home: NextPage = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const theme = useAppSelector((state) => state.build.theme);
|
||||
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 (
|
||||
@ -50,52 +54,52 @@ const Home: NextPage = () => {
|
||||
</div>
|
||||
|
||||
<div className={styles.main}>
|
||||
<h1>{t('common.title')}</h1>
|
||||
<h1>{t<string>('common.title')}</h1>
|
||||
|
||||
<h2>{t('common.subtitle')}</h2>
|
||||
<h2>{t<string>('common.subtitle')}</h2>
|
||||
|
||||
<NoSSR>
|
||||
<NoSsr>
|
||||
<div className={styles.buttonWrapper}>
|
||||
{isLoggedIn ? (
|
||||
<>
|
||||
<Link href="/dashboard" passHref>
|
||||
<Button>{t('landing.actions.app')}</Button>
|
||||
<Button>{t<string>('landing.actions.app')}</Button>
|
||||
</Link>
|
||||
|
||||
<Button variant="outlined" onClick={handleLogout}>
|
||||
{t('landing.actions.logout')}
|
||||
{t<string>('landing.actions.logout')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button onClick={handleLogin}>{t('landing.actions.login')}</Button>
|
||||
<Button onClick={handleLogin}>{t<string>('landing.actions.login')}</Button>
|
||||
|
||||
<Button variant="outlined" onClick={handleRegister}>
|
||||
{t('landing.actions.register')}
|
||||
<Button variant="outlined" onClick={handleRegister} disabled={FLAG_DISABLE_SIGNUPS}>
|
||||
{t<string>('landing.actions.register')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</NoSSR>
|
||||
</NoSsr>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h6>{t('landing.summary.heading')}</h6>
|
||||
<h6>{t<string>('landing.summary.heading')}</h6>
|
||||
|
||||
<p>{t('landing.summary.body')}</p>
|
||||
<p>{t<string>('landing.summary.body')}</p>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h6>{t('landing.features.heading')}</h6>
|
||||
<h6>{t<string>('landing.features.heading')}</h6>
|
||||
|
||||
<ul className="list-inside list-disc leading-loose">
|
||||
<li>{t('landing.features.list.free')}</li>
|
||||
<li>{t('landing.features.list.ads')}</li>
|
||||
<li>{t('landing.features.list.tracking')}</li>
|
||||
<li>{t('landing.features.list.languages')}</li>
|
||||
<li>{t('landing.features.list.import')}</li>
|
||||
<li>{t('landing.features.list.export')}</li>
|
||||
<li>{t<string>('landing.features.list.free')}</li>
|
||||
<li>{t<string>('landing.features.list.ads')}</li>
|
||||
<li>{t<string>('landing.features.list.tracking')}</li>
|
||||
<li>{t<string>('landing.features.list.languages')}</li>
|
||||
<li>{t<string>('landing.features.list.import')}</li>
|
||||
<li>{t<string>('landing.features.list.export')}</li>
|
||||
<li>
|
||||
<Trans t={t} i18nKey="landing.features.list.more">
|
||||
And a lot of exciting features,
|
||||
@ -108,7 +112,7 @@ const Home: NextPage = () => {
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h6>{t('landing.screenshots.heading')}</h6>
|
||||
<h6>{t<string>('landing.screenshots.heading')}</h6>
|
||||
|
||||
<div className={styles.screenshots}>
|
||||
{screenshots.map(({ src, alt }) => (
|
||||
@ -120,7 +124,7 @@ const Home: NextPage = () => {
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h6>{t('landing.testimonials.heading')}</h6>
|
||||
<h6>{t<string>('landing.testimonials.heading')}</h6>
|
||||
|
||||
<p className="my-3">
|
||||
<Trans t={t} i18nKey="landing.testimonials.body">
|
||||
@ -145,37 +149,37 @@ const Home: NextPage = () => {
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h6>{t('landing.links.heading')}</h6>
|
||||
<h6>{t<string>('landing.links.heading')}</h6>
|
||||
|
||||
<div>
|
||||
<Link href="/meta/privacy" passHref>
|
||||
<Button variant="text" startIcon={<LinkIcon />}>
|
||||
{t('landing.links.links.privacy')}
|
||||
{t<string>('landing.links.links.privacy')}
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link href="/meta/service" passHref>
|
||||
<Button variant="text" startIcon={<LinkIcon />}>
|
||||
{t('landing.links.links.service')}
|
||||
{t<string>('landing.links.links.service')}
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<a href={GITHUB_URL} target="_blank" rel="noreferrer">
|
||||
<Button variant="text" startIcon={<LinkIcon />}>
|
||||
{t('landing.links.links.github')}
|
||||
{t<string>('landing.links.links.github')}
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<a href={DONATION_URL} target="_blank" rel="noreferrer">
|
||||
<Button variant="text" startIcon={<LinkIcon />}>
|
||||
{t('landing.links.links.donate')}
|
||||
{t<string>('landing.links.links.donate')}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<a href="https://pillai.xyz/digitalocean" target="_blank" rel="noreferrer">
|
||||
<a href={DIGITALOCEAN_URL} target="_blank" rel="noreferrer">
|
||||
<Image src="/images/sponsors/digitalocean.svg" alt="Powered By DigitalOcean" width={200} height={40} />
|
||||
</a>
|
||||
</section>
|
||||
@ -187,7 +191,11 @@ const Home: NextPage = () => {
|
||||
<div>v{process.env.appVersion}</div>
|
||||
</div>
|
||||
|
||||
<LanguageSwitcher />
|
||||
<div className={styles.actions}>
|
||||
<IconButton onClick={handleToggle}>{theme === 'dark' ? <DarkMode /> : <LightMode />}</IconButton>
|
||||
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { NextPage } from 'next';
|
||||
|
||||
const PrivacyPolicy: NextPage = () => (
|
||||
<div className="mx-auto my-12 prose dark:prose-invert">
|
||||
<div className="prose mx-auto my-12 dark:prose-invert">
|
||||
<h1>Privacy Policy</h1>
|
||||
|
||||
<p>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { NextPage } from 'next';
|
||||
|
||||
const TermsOfService: NextPage = () => (
|
||||
<div className="mx-auto my-12 prose dark:prose-invert">
|
||||
<div className="prose mx-auto my-12 dark:prose-invert">
|
||||
<h1>Terms of Service</h1>
|
||||
|
||||
<p>
|
||||
|
||||
@ -7,6 +7,7 @@ import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { GetServerSideProps, NextPage } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
@ -35,6 +36,8 @@ export const getServerSideProps: GetServerSideProps<Props> = async ({ query, loc
|
||||
};
|
||||
|
||||
const Preview: NextPage<Props> = ({ shortId }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { data: resume } = useQuery<Resume>(`resume/${shortId}`, () => fetchResumeByShortId({ shortId }), {
|
||||
@ -52,6 +55,14 @@ const Preview: NextPage<Props> = ({ shortId }) => {
|
||||
if (resume) dispatch(setResume(resume));
|
||||
}, [resume, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resume && !isEmpty(resume) && router.locale !== resume.metadata.locale) {
|
||||
const { pathname, asPath, query } = router;
|
||||
|
||||
router.push({ pathname, query }, asPath, { locale: resume.metadata.locale });
|
||||
}
|
||||
}, [resume, router]);
|
||||
|
||||
if (!resume || isEmpty(resume)) return null;
|
||||
|
||||
const layout: string[][][] = get(resume, 'metadata.layout', []);
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 59 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user