Compare commits
266 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 0e1e2bbe4e | |||
| 3a2e62be4c | |||
| 697ceef8f2 | |||
| c8e81a456d | |||
| 2b334e5c5a | |||
| 90321e1284 | |||
| 9bcddb4b5c | |||
| 72fdc05f69 | |||
| e1d6540500 | |||
| 4b17719c69 | |||
| da056307dd | |||
| e4950728d8 | |||
| dac4e862b8 |
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
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
41
.env.example
@ -1,30 +1,33 @@
|
|||||||
# App
|
# Server + Client
|
||||||
TZ=UTC
|
TZ=UTC
|
||||||
SECRET_KEY=
|
PUBLIC_URL=http://localhost:3000
|
||||||
|
PUBLIC_SERVER_URL=http://localhost:3000/api
|
||||||
|
PUBLIC_GOOGLE_CLIENT_ID=
|
||||||
|
|
||||||
# URLs
|
# Server + Database
|
||||||
PUBLIC_URL=http://<SERVER-IP>
|
POSTGRES_DB=postgres
|
||||||
PUBLIC_SERVER_URL=http://<SERVER-IP>/api
|
POSTGRES_USER=postgres
|
||||||
|
|
||||||
# Database
|
|
||||||
POSTGRES_HOST=localhost
|
|
||||||
POSTGRES_PORT=5432
|
|
||||||
POSTGRES_USERNAME=postgres
|
|
||||||
POSTGRES_PASSWORD=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_SECRET=
|
||||||
JWT_EXPIRY_TIME=604800
|
JWT_EXPIRY_TIME=604800
|
||||||
|
|
||||||
# Google
|
|
||||||
PUBLIC_GOOGLE_CLIENT_ID=
|
|
||||||
GOOGLE_CLIENT_SECRET=
|
GOOGLE_CLIENT_SECRET=
|
||||||
GOOGLE_API_KEY=
|
GOOGLE_API_KEY=
|
||||||
|
|
||||||
# SendGrid (Optional)
|
|
||||||
SENDGRID_API_KEY=
|
SENDGRID_API_KEY=
|
||||||
SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID=
|
SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID=
|
||||||
SENDGRID_FROM_NAME=
|
SENDGRID_FROM_NAME=
|
||||||
SENDGRID_FROM_EMAIL=
|
SENDGRID_FROM_EMAIL=
|
||||||
|
STORAGE_BUCKET=
|
||||||
|
STORAGE_REGION=
|
||||||
|
STORAGE_ENDPOINT=
|
||||||
|
STORAGE_URL_PREFIX=
|
||||||
|
STORAGE_ACCESS_KEY=
|
||||||
|
STORAGE_SECRET_KEY=
|
||||||
|
|
||||||
|
# Flags (Client)
|
||||||
|
PUBLIC_FLAG_DISABLE_SIGNUPS=false
|
||||||
16
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@ -8,29 +8,29 @@ assignees: ''
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Describe the bug**
|
**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**
|
**Product Flavor**
|
||||||
- [ ] Managed (https://rxresu.me)
|
- [ ] Managed (https://rxresu.me)
|
||||||
- [ ] Self Hosted
|
- [ ] Self Hosted
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
<!-- Steps to reproduce the behavior: -->
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
4. See error
|
4. See error
|
||||||
|
|
||||||
**Expected behavior**
|
**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**
|
**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):**
|
**Desktop (please complete the following information):**
|
||||||
- OS: [e.g. iOS]
|
- OS: <!--[e.g. iOS]-->
|
||||||
- Browser [e.g. chrome, safari]
|
- Browser <!--[e.g. chrome, safari]-->
|
||||||
- Version [e.g. 22]
|
- Version <!--[e.g. 22]-->
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
<!-- Add any other context about the problem here. -->
|
||||||
|
|||||||
8
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@ -8,13 +8,13 @@ assignees: ''
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
**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**
|
**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**
|
**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**
|
**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
@ -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
@ -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
@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install DigitalOcean CLI
|
- name: Install DigitalOcean CLI
|
||||||
uses: digitalocean/action-doctl@v2.1.0
|
uses: digitalocean/action-doctl@v2.1.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.DIGITALOCEAN_TOKEN }}
|
token: ${{ secrets.DIGITALOCEAN_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
96
.github/workflows/docker-build-push.yml
vendored
@ -5,26 +5,33 @@ on:
|
|||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker_client:
|
client:
|
||||||
name: Docker (Client)
|
name: Client
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v3.0.0
|
uses: actions/checkout@v3.0.2
|
||||||
|
|
||||||
- id: version
|
- id: version
|
||||||
name: Get Version
|
name: Get Version
|
||||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||||
|
|
||||||
- name: Login to Docker
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v1.14.1
|
uses: docker/login-action@v2.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
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
|
- name: Build and Push Client Image
|
||||||
uses: docker/build-push-action@v2.9.0
|
uses: docker/build-push-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
@ -32,27 +39,36 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
amruthpillai/reactive-resume:client-latest
|
amruthpillai/reactive-resume:client-latest
|
||||||
amruthpillai/reactive-resume:client-${{ steps.version.outputs.tag }}
|
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:
|
server:
|
||||||
name: Docker (Server)
|
name: Server
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v3.0.0
|
uses: actions/checkout@v3.0.2
|
||||||
|
|
||||||
- id: version
|
- id: version
|
||||||
name: Get Version
|
name: Get Version
|
||||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||||
|
|
||||||
- name: Login to Docker
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v1.14.1
|
uses: docker/login-action@v2.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
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
|
- name: Build and Push Server Image
|
||||||
uses: docker/build-push-action@v2.9.0
|
uses: docker/build-push-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
@ -60,61 +76,5 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
amruthpillai/reactive-resume:server-latest
|
amruthpillai/reactive-resume:server-latest
|
||||||
amruthpillai/reactive-resume:server-${{ steps.version.outputs.tag }}
|
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-latest
|
||||||
ghcr.io/amruthpillai/reactive-resume:server-${{ steps.version.outputs.tag }}
|
ghcr.io/amruthpillai/reactive-resume:server-${{ steps.version.outputs.tag }}
|
||||||
|
|||||||
136
CHANGELOG.md
@ -2,6 +2,142 @@
|
|||||||
|
|
||||||
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.
|
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.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)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **i18n:** add Malayalam (മലയാളം) language to i18n locales ([3a2e62b](https://github.com/AmruthPillai/Reactive-Resume/commit/3a2e62be4c9acc14f17277c060cc9ea2c417a478))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **printer/i18n:** fix dates not showing up in resume language when printing ([90321e1](https://github.com/AmruthPillai/Reactive-Resume/commit/90321e1284409ab9442883c04a9b4c591d36f95d)), closes [#729](https://github.com/AmruthPillai/Reactive-Resume/issues/729)
|
||||||
|
|
||||||
### [3.2.6](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.5...v3.2.6) (2022-03-17)
|
### [3.2.6](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.5...v3.2.6) (2022-03-17)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
39
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
|
# Reactive Resume
|
||||||
|
|
||||||

|
[](https://github.com/AmruthPillai/Reactive-Resume/releases)
|
||||||

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

|
||||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2FAmruthPillai%2FReactive-Resume?ref=badge_shield)
|
[](https://app.fossa.com/projects/git%2Bgithub.com%2FAmruthPillai%2FReactive-Resume?ref=badge_shield)
|
||||||
|
|
||||||
## [Go to App](https://rxresu.me) | [Docs](https://docs.rxresu.me)
|
## [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.
|
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
|
## Features
|
||||||
|
|
||||||
- Free, forever
|
- Free, forever
|
||||||
@ -39,17 +57,30 @@ You have complete control over what goes into your resume, how it looks, what co
|
|||||||
- Arabic (اَلْعَرَبِيَّةُ)
|
- Arabic (اَلْعَرَبِيَّةُ)
|
||||||
- Bengali (বাংলা)
|
- Bengali (বাংলা)
|
||||||
- Chinese (中文)
|
- Chinese (中文)
|
||||||
|
- Czech (čeština)
|
||||||
- Danish (Dansk)
|
- Danish (Dansk)
|
||||||
|
- Dutch (Nederlands)
|
||||||
- English
|
- English
|
||||||
- French (Français)
|
- French (Français)
|
||||||
- German (Deutsch)
|
- German (Deutsch)
|
||||||
|
- Greek (Ελληνικά)
|
||||||
|
- Hebrew (Ivrit)
|
||||||
- Hindi (हिन्दी)
|
- Hindi (हिन्दी)
|
||||||
|
- Hungarian (Magyar)
|
||||||
|
- Indonesian (Bahasa Indonesia)
|
||||||
- Italian (Italiano)
|
- Italian (Italiano)
|
||||||
- Kannada (ಕನ್ನಡ)
|
- Kannada (ಕನ್ನಡ)
|
||||||
|
- Malayalam (മലയാളം)
|
||||||
|
- Odia (ଓଡ଼ିଆ)
|
||||||
|
- Persian (Farsi)
|
||||||
- Polish (Polski)
|
- Polish (Polski)
|
||||||
|
- Portuguese (Português)
|
||||||
|
- Russian (русский)
|
||||||
- Spanish (Español)
|
- Spanish (Español)
|
||||||
|
- Swedish (Svenska)
|
||||||
- Tamil (தமிழ்)
|
- Tamil (தமிழ்)
|
||||||
- Turkish (Türkçe)
|
- Turkish (Türkçe)
|
||||||
|
- Vietnamese (Tiếng Việt)
|
||||||
|
|
||||||
Help by [translating Reactive Resume](https://translate.rxresu.me) to your language!
|
Help by [translating Reactive Resume](https://translate.rxresu.me) to your language!
|
||||||
|
|
||||||
@ -73,7 +104,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!
|
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.
|
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
@ -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 {
|
plugins {
|
||||||
id 'com.android.application' version '7.1.2' apply false
|
id 'com.android.application' version '7.1.2' apply false
|
||||||
id 'com.android.library' 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.0' apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
task clean(type: Delete) {
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
"ignorePatterns": [".next", "__ENV.js"],
|
"ignorePatterns": [".next", "__ENV.js"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@next/next/no-img-element": "off",
|
"@next/next/no-img-element": "off",
|
||||||
"@next/next/no-sync-scripts": "off"
|
"@next/next/no-sync-scripts": "off",
|
||||||
|
"@next/next/no-html-link-for-pages": ["error", "pages"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
client/.gitignore
vendored
@ -36,4 +36,7 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
# react-env
|
# react-env
|
||||||
__ENV.js
|
__ENV.js
|
||||||
|
|
||||||
|
# next-sitemap
|
||||||
|
sitemap*.xml
|
||||||
@ -1,4 +1,4 @@
|
|||||||
FROM node:16-alpine as dependencies
|
FROM node:lts-alpine as dependencies
|
||||||
|
|
||||||
RUN apk add --no-cache curl g++ make python3 \
|
RUN apk add --no-cache curl g++ make python3 \
|
||||||
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||||
@ -11,7 +11,7 @@ COPY ./client/package.json ./client/package.json
|
|||||||
|
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
FROM node:16-alpine as builder
|
FROM node:lts-alpine as builder
|
||||||
|
|
||||||
RUN apk add --no-cache curl g++ make python3 \
|
RUN apk add --no-cache curl g++ make python3 \
|
||||||
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||||
@ -27,7 +27,7 @@ COPY --from=dependencies /app/client/node_modules ./client/node_modules
|
|||||||
RUN pnpm run build:schema
|
RUN pnpm run build:schema
|
||||||
RUN pnpm run build:client
|
RUN pnpm run build:client
|
||||||
|
|
||||||
FROM node:16-alpine as production
|
FROM node:lts-alpine as production
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@ -49,4 +49,7 @@ EXPOSE 3000
|
|||||||
|
|
||||||
ENV PORT 3000
|
ENV PORT 3000
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=20s --retries=3 --start-period=15s \
|
||||||
|
CMD curl -fSs 127.0.0.1:3000 || exit 1
|
||||||
|
|
||||||
CMD [ "pnpm", "run", "start:client" ]
|
CMD [ "pnpm", "run", "start:client" ]
|
||||||
@ -52,7 +52,7 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
|||||||
const url = getResumeUrl(resume, { withHost: true });
|
const url = getResumeUrl(resume, { withHost: true });
|
||||||
await navigator.clipboard.writeText(url);
|
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 () => {
|
const handleExportPDF = async () => {
|
||||||
@ -75,19 +75,19 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className={styles.controller}>
|
<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.zoom-in')}>
|
||||||
<ButtonBase onClick={() => zoomIn(0.25)}>
|
<ButtonBase onClick={() => zoomIn(0.25)}>
|
||||||
<ZoomIn fontSize="medium" />
|
<ZoomIn fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
</Tooltip>
|
</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)}>
|
<ButtonBase onClick={() => zoomOut(0.25)}>
|
||||||
<ZoomOut fontSize="medium" />
|
<ZoomOut fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
</Tooltip>
|
</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)}>
|
<ButtonBase onClick={() => centerView(0.95)}>
|
||||||
<FilterCenterFocus fontSize="medium" />
|
<FilterCenterFocus fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
@ -98,7 +98,7 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
|||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
<>
|
<>
|
||||||
{pages.length > 1 && (
|
{pages.length > 1 && (
|
||||||
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.toggle-orientation') as string}>
|
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-orientation')}>
|
||||||
<ButtonBase onClick={handleTogglePageOrientation}>
|
<ButtonBase onClick={handleTogglePageOrientation}>
|
||||||
{orientation === 'vertical' ? (
|
{orientation === 'vertical' ? (
|
||||||
<AlignHorizontalCenter fontSize="medium" />
|
<AlignHorizontalCenter fontSize="medium" />
|
||||||
@ -109,13 +109,13 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
|||||||
</Tooltip>
|
</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}>
|
<ButtonBase onClick={handleTogglePageBreakLine}>
|
||||||
<InsertPageBreak fontSize="medium" />
|
<InsertPageBreak fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
</Tooltip>
|
</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}>
|
<ButtonBase onClick={handleToggleSidebar}>
|
||||||
<ViewSidebar fontSize="medium" />
|
<ViewSidebar fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
@ -125,13 +125,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}>
|
<ButtonBase onClick={handleCopyLink}>
|
||||||
<Link fontSize="medium" />
|
<Link fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
</Tooltip>
|
</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}>
|
<ButtonBase onClick={handleExportPDF} disabled={isLoading}>
|
||||||
<Download fontSize="medium" />
|
<Download fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
|
|||||||
@ -133,7 +133,7 @@ const Header = () => {
|
|||||||
const url = getResumeUrl(resume, { withHost: true });
|
const url = getResumeUrl(resume, { withHost: true });
|
||||||
await navigator.clipboard.writeText(url);
|
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 (
|
return (
|
||||||
@ -166,14 +166,14 @@ const Header = () => {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DriveFileRenameOutline className="scale-90" />
|
<DriveFileRenameOutline className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('builder.header.menu.rename')}</ListItemText>
|
<ListItemText>{t<string>('builder.header.menu.rename')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem onClick={handleDuplicate}>
|
<MenuItem onClick={handleDuplicate}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<CopyAll className="scale-90" />
|
<CopyAll className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('builder.header.menu.duplicate')}</ListItemText>
|
<ListItemText>{t<string>('builder.header.menu.duplicate')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
{resume.public ? (
|
{resume.public ? (
|
||||||
@ -181,27 +181,27 @@ const Header = () => {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LinkIcon className="scale-90" />
|
<LinkIcon className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('builder.header.menu.share-link')}</ListItemText>
|
<ListItemText>{t<string>('builder.header.menu.share-link')}</ListItemText>
|
||||||
</MenuItem>
|
</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>
|
<div>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LinkIcon className="scale-90" />
|
<LinkIcon className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('builder.header.menu.share-link')}</ListItemText>
|
<ListItemText>{t<string>('builder.header.menu.share-link')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</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}>
|
<MenuItem onClick={handleDelete}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Delete className="scale-90" />
|
<Delete className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('builder.header.menu.delete')}</ListItemText>
|
<ListItemText>{t<string>('builder.header.menu.delete')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@ -48,9 +48,7 @@ const Page: React.FC<Props> = ({ page, showPageNumbers = false }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showPageNumbers && (
|
{showPageNumbers && (
|
||||||
<h4 className={styles.pageNumber}>
|
<h4 className={styles.pageNumber}>{`${t<string>('builder.common.glossary.page')} ${page + 1}`}</h4>
|
||||||
{t('builder.common.glossary.page')} {page + 1}
|
|
||||||
</h4>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -81,14 +81,14 @@ const LeftSidebar = () => {
|
|||||||
arrow
|
arrow
|
||||||
key={id}
|
key={id}
|
||||||
placement="right"
|
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>
|
<IconButton onClick={() => handleClick(id)}>{icon}</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{customSections.map(({ id }) => (
|
{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)}>
|
<IconButton onClick={() => handleClick(id)}>
|
||||||
<Star />
|
<Star />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@ -114,7 +114,9 @@ const LeftSidebar = () => {
|
|||||||
|
|
||||||
<div className="py-6 text-right">
|
<div className="py-6 text-right">
|
||||||
<Button fullWidth variant="outlined" startIcon={<Add />} onClick={handleAddSection}>
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const Basics = () => {
|
|||||||
|
|
||||||
return (
|
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 grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
<div className="grid items-center gap-4 sm:col-span-2 sm:grid-cols-3">
|
<div className="grid items-center gap-4 sm:col-span-2 sm:grid-cols-3">
|
||||||
@ -33,10 +33,10 @@ const Basics = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2 w-full sm:col-span-2">
|
<div className="grid gap-2 w-full sm:col-span-2">
|
||||||
<ResumeInput label={t('builder.leftSidebar.sections.basics.name.label')} path="basics.name" />
|
<ResumeInput label={t<string>('builder.leftSidebar.sections.basics.name.label')} path="basics.name" />
|
||||||
|
|
||||||
<Button variant="outlined" startIcon={<PhotoFilter />} onClick={handleClick}>
|
<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>
|
</Button>
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
@ -57,20 +57,30 @@ const Basics = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ResumeInput label={t('builder.common.form.email.label')} path="basics.email" className="sm:col-span-2" />
|
<ResumeInput
|
||||||
<ResumeInput label={t('builder.common.form.phone.label')} path="basics.phone" />
|
type="date"
|
||||||
<ResumeInput label={t('builder.common.form.url.label')} path="basics.website" />
|
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" />
|
<Divider className="sm:col-span-2" />
|
||||||
|
|
||||||
<ResumeInput
|
<ResumeInput
|
||||||
label={t('builder.leftSidebar.sections.basics.headline.label')}
|
label={t<string>('builder.leftSidebar.sections.basics.headline.label')}
|
||||||
path="basics.headline"
|
path="basics.headline"
|
||||||
className="sm:col-span-2"
|
className="sm:col-span-2"
|
||||||
/>
|
/>
|
||||||
<ResumeInput
|
<ResumeInput
|
||||||
type="textarea"
|
type="textarea"
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
path="basics.summary"
|
path="basics.summary"
|
||||||
className="sm:col-span-2"
|
className="sm:col-span-2"
|
||||||
markdownSupported
|
markdownSupported
|
||||||
|
|||||||
@ -8,19 +8,28 @@ const Location = () => {
|
|||||||
|
|
||||||
return (
|
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">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
<ResumeInput
|
<ResumeInput
|
||||||
label={t('builder.leftSidebar.sections.location.address.label')}
|
label={t<string>('builder.leftSidebar.sections.location.address.label')}
|
||||||
path="basics.location.address"
|
path="basics.location.address"
|
||||||
className="sm:col-span-2"
|
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
|
<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"
|
path="basics.location.postalCode"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -32,7 +32,7 @@ const PhotoFilters = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 p-5 dark:bg-neutral-800">
|
<div className="flex flex-col gap-2 p-5 dark:bg-neutral-800">
|
||||||
<div>
|
<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">
|
<div className="mx-2">
|
||||||
<Slider
|
<Slider
|
||||||
@ -54,18 +54,20 @@ const PhotoFilters = () => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<div>
|
<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">
|
<div className="flex items-center">
|
||||||
<FormControlLabel
|
<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={
|
control={
|
||||||
<Checkbox color="secondary" checked={grayscale} onChange={(_, value) => handleSetGrayscale(value)} />
|
<Checkbox color="secondary" checked={grayscale} onChange={(_, value) => handleSetGrayscale(value)} />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControlLabel
|
<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)} />}
|
control={<Checkbox color="secondary" checked={border} onChange={(_, value) => handleSetBorder(value)} />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -74,7 +76,7 @@ const PhotoFilters = () => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<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)}>
|
<ToggleButtonGroup exclusive value={shape} onChange={(_, value) => handleChangeShape(value)}>
|
||||||
<ToggleButton size="small" value="square" className="w-14">
|
<ToggleButton size="small" value="square" className="w-14">
|
||||||
|
|||||||
@ -49,7 +49,7 @@ const PhotoUpload: React.FC = () => {
|
|||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
|
|
||||||
if (file.size > FILE_UPLOAD_MAX_SIZE) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,8 +67,8 @@ const PhotoUpload: React.FC = () => {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
isEmpty(photo.url)
|
isEmpty(photo.url)
|
||||||
? (t('builder.leftSidebar.sections.basics.photo-upload.tooltip.upload') as string)
|
? (t<string>('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.remove') as string)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Avatar sx={{ width: 96, height: 96 }} src={photo.url} />
|
<Avatar sx={{ width: 96, height: 96 }} src={photo.url} />
|
||||||
|
|||||||
@ -28,7 +28,7 @@ const Profiles = () => {
|
|||||||
|
|
||||||
return (
|
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
|
<List
|
||||||
path="basics.profiles"
|
path="basics.profiles"
|
||||||
@ -40,8 +40,8 @@ const Profiles = () => {
|
|||||||
|
|
||||||
<footer className="flex justify-end">
|
<footer className="flex justify-end">
|
||||||
<Button variant="outlined" startIcon={<Add />} onClick={handleAdd}>
|
<Button variant="outlined" startIcon={<Add />} onClick={handleAdd}>
|
||||||
{t('builder.common.actions.add', {
|
{t<string>('builder.common.actions.add', {
|
||||||
token: t('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
token: t<string>('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
||||||
})}
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -74,7 +74,7 @@ const Section: React.FC<Props> = ({
|
|||||||
<SectionSettings path={path} />
|
<SectionSettings path={path} />
|
||||||
|
|
||||||
<Button variant="outlined" startIcon={<Add />} onClick={handleAdd}>
|
<Button variant="outlined" startIcon={<Add />} onClick={handleAdd}>
|
||||||
{t('builder.common.actions.add', { token: heading })}
|
{t<string>('builder.common.actions.add', { token: heading })}
|
||||||
</Button>
|
</Button>
|
||||||
</footer>
|
</footer>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -32,7 +32,7 @@ const SectionSettings: React.FC<Props> = ({ path }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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">
|
<ButtonBase onClick={handleClick} sx={{ padding: 1, borderRadius: 1 }} className="opacity-50 hover:opacity-75">
|
||||||
<ViewWeek /> <span className="ml-1.5 text-xs">{columns}</span>
|
<ViewWeek /> <span className="ml-1.5 text-xs">{columns}</span>
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
@ -48,7 +48,7 @@ const SectionSettings: React.FC<Props> = ({ path }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="p-5 dark:bg-neutral-800">
|
<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)}>
|
<ToggleButtonGroup exclusive value={columns} onChange={(_, value: number) => handleSetColumns(value)}>
|
||||||
{[1, 2, 3, 4].map((index) => (
|
{[1, 2, 3, 4].map((index) => (
|
||||||
|
|||||||
@ -25,7 +25,7 @@ const CustomCSS = () => {
|
|||||||
|
|
||||||
return (
|
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
|
<Editor
|
||||||
height="200px"
|
height="200px"
|
||||||
|
|||||||
@ -19,12 +19,12 @@ const Export = () => {
|
|||||||
|
|
||||||
const pdfListItemText = {
|
const pdfListItemText = {
|
||||||
normal: {
|
normal: {
|
||||||
primary: t('builder.rightSidebar.sections.export.pdf.normal.primary'),
|
primary: t<string>('builder.rightSidebar.sections.export.pdf.normal.primary'),
|
||||||
secondary: t('builder.rightSidebar.sections.export.pdf.normal.secondary'),
|
secondary: t<string>('builder.rightSidebar.sections.export.pdf.normal.secondary'),
|
||||||
},
|
},
|
||||||
loading: {
|
loading: {
|
||||||
primary: t('builder.rightSidebar.sections.export.pdf.loading.primary'),
|
primary: t<string>('builder.rightSidebar.sections.export.pdf.loading.primary'),
|
||||||
secondary: t('builder.rightSidebar.sections.export.pdf.loading.secondary'),
|
secondary: t<string>('builder.rightSidebar.sections.export.pdf.loading.secondary'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ const Export = () => {
|
|||||||
|
|
||||||
return (
|
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 }}>
|
<List sx={{ padding: 0 }}>
|
||||||
<ListItem sx={{ padding: 0 }}>
|
<ListItem sx={{ padding: 0 }}>
|
||||||
@ -61,8 +61,8 @@ const Export = () => {
|
|||||||
<Schema />
|
<Schema />
|
||||||
|
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('builder.rightSidebar.sections.export.json.primary')}
|
primary={t<string>('builder.rightSidebar.sections.export.json.primary')}
|
||||||
secondary={t('builder.rightSidebar.sections.export.json.secondary')}
|
secondary={t<string>('builder.rightSidebar.sections.export.json.secondary')}
|
||||||
/>
|
/>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|||||||
@ -60,9 +60,9 @@ const Layout = () => {
|
|||||||
<>
|
<>
|
||||||
<Heading
|
<Heading
|
||||||
path="metadata.layout"
|
path="metadata.layout"
|
||||||
name={t('builder.rightSidebar.sections.layout.heading')}
|
name={t<string>('builder.rightSidebar.sections.layout.heading')}
|
||||||
action={
|
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}>
|
<IconButton onClick={handleResetLayout}>
|
||||||
<Restore />
|
<Restore />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@ -76,12 +76,16 @@ const Layout = () => {
|
|||||||
<div key={pageIndex} className={styles.page}>
|
<div key={pageIndex} className={styles.page}>
|
||||||
<div className="flex items-center justify-between pr-3">
|
<div className="flex items-center justify-between pr-3">
|
||||||
<p className={styles.heading}>
|
<p className={styles.heading}>
|
||||||
{t('builder.common.glossary.page')} {pageIndex + 1}
|
{t<string>('builder.common.glossary.page')} {pageIndex + 1}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className={clsx(styles.delete, { hidden: pageIndex === 0 })}>
|
<div className={clsx(styles.delete, { hidden: pageIndex === 0 })}>
|
||||||
<Tooltip
|
<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)}>
|
<IconButton size="small" onClick={() => handleDeletePage(pageIndex)}>
|
||||||
<Close fontSize="small" />
|
<Close fontSize="small" />
|
||||||
@ -113,7 +117,7 @@ const Layout = () => {
|
|||||||
[styles.disabled]: !get(resumeSections, `${sectionId}.visible`, true),
|
[styles.disabled]: !get(resumeSections, `${sectionId}.visible`, true),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{get(resumeSections, `${sectionId}.name`)}
|
{get(resumeSections, `${sectionId}.name`, '') as string}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -132,7 +136,7 @@ const Layout = () => {
|
|||||||
|
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<Button variant="outlined" startIcon={<Add />} onClick={handleAddPage}>
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
|
|||||||
@ -12,39 +12,41 @@ const Links = () => {
|
|||||||
|
|
||||||
return (
|
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.container}>
|
||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<h2>
|
<h2>
|
||||||
<Savings fontSize="small" />
|
<Savings fontSize="small" />
|
||||||
{t('builder.rightSidebar.sections.links.donate.heading')}
|
{t<string>('builder.rightSidebar.sections.links.donate.heading')}
|
||||||
</h2>
|
</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">
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<h2>
|
<h2>
|
||||||
<BugReport fontSize="small" />
|
<BugReport fontSize="small" />
|
||||||
{t('builder.rightSidebar.sections.links.bugs-features.heading')}
|
{t<string>('builder.rightSidebar.sections.links.bugs-features.heading')}
|
||||||
</h2>
|
</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">
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<a href={GITHUB_URL} target="_blank" rel="noreferrer">
|
<a href={GITHUB_URL} target="_blank" rel="noreferrer">
|
||||||
<Button variant="text" startIcon={<Link />}>
|
<Button variant="text" startIcon={<Link />}>
|
||||||
{t('builder.rightSidebar.sections.links.github')}
|
{t<string>('builder.rightSidebar.sections.links.github')}
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -85,13 +85,13 @@ const Settings = () => {
|
|||||||
|
|
||||||
return (
|
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 }}>
|
<List sx={{ padding: 0 }}>
|
||||||
{/* Global Settings */}
|
{/* Global Settings */}
|
||||||
<>
|
<>
|
||||||
<ListSubheader className="rounded">
|
<ListSubheader className="rounded">
|
||||||
{t('builder.rightSidebar.sections.settings.global.heading')}
|
{t<string>('builder.rightSidebar.sections.settings.global.heading')}
|
||||||
</ListSubheader>
|
</ListSubheader>
|
||||||
|
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@ -99,7 +99,7 @@ const Settings = () => {
|
|||||||
<Palette />
|
<Palette />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('builder.rightSidebar.sections.settings.global.theme.primary')}
|
primary={t<string>('builder.rightSidebar.sections.settings.global.theme.primary')}
|
||||||
secondary={themeString}
|
secondary={themeString}
|
||||||
/>
|
/>
|
||||||
<ThemeSwitch checked={isDarkMode} onChange={(_, value: boolean) => handleSetTheme(value)} />
|
<ThemeSwitch checked={isDarkMode} onChange={(_, value: boolean) => handleSetTheme(value)} />
|
||||||
@ -108,8 +108,8 @@ const Settings = () => {
|
|||||||
<ListItem className="flex-col">
|
<ListItem className="flex-col">
|
||||||
<ListItemText
|
<ListItemText
|
||||||
className="w-full"
|
className="w-full"
|
||||||
primary={t('builder.rightSidebar.sections.settings.global.date.primary')}
|
primary={t<string>('builder.rightSidebar.sections.settings.global.date.primary')}
|
||||||
secondary={t('builder.rightSidebar.sections.settings.global.date.secondary')}
|
secondary={t<string>('builder.rightSidebar.sections.settings.global.date.secondary')}
|
||||||
/>
|
/>
|
||||||
<Autocomplete<string, false, boolean, false>
|
<Autocomplete<string, false, boolean, false>
|
||||||
disableClearable
|
disableClearable
|
||||||
@ -124,8 +124,8 @@ const Settings = () => {
|
|||||||
<ListItem className="flex-col">
|
<ListItem className="flex-col">
|
||||||
<ListItemText
|
<ListItemText
|
||||||
className="w-full"
|
className="w-full"
|
||||||
primary={t('builder.rightSidebar.sections.settings.global.language.primary')}
|
primary={t<string>('builder.rightSidebar.sections.settings.global.language.primary')}
|
||||||
secondary={t('builder.rightSidebar.sections.settings.global.language.secondary')}
|
secondary={t<string>('builder.rightSidebar.sections.settings.global.language.secondary')}
|
||||||
/>
|
/>
|
||||||
<Autocomplete<Language, false, boolean, false>
|
<Autocomplete<Language, false, boolean, false>
|
||||||
disableClearable
|
disableClearable
|
||||||
@ -148,15 +148,17 @@ const Settings = () => {
|
|||||||
|
|
||||||
{/* Page Settings */}
|
{/* Page Settings */}
|
||||||
<>
|
<>
|
||||||
<ListSubheader className="rounded">{t('builder.rightSidebar.sections.settings.page.heading')}</ListSubheader>
|
<ListSubheader className="rounded">
|
||||||
|
{t<string>('builder.rightSidebar.sections.settings.page.heading')}
|
||||||
|
</ListSubheader>
|
||||||
|
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('builder.rightSidebar.sections.settings.page.orientation.primary')}
|
primary={t<string>('builder.rightSidebar.sections.settings.page.orientation.primary')}
|
||||||
secondary={
|
secondary={
|
||||||
pages.length === 1
|
pages.length === 1
|
||||||
? t('builder.rightSidebar.sections.settings.page.orientation.disabled')
|
? t<string>('builder.rightSidebar.sections.settings.page.orientation.disabled')
|
||||||
: t('builder.rightSidebar.sections.settings.page.orientation.secondary')
|
: t<string>('builder.rightSidebar.sections.settings.page.orientation.secondary')
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
@ -169,8 +171,8 @@ const Settings = () => {
|
|||||||
|
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('builder.rightSidebar.sections.settings.page.break-line.primary')}
|
primary={t<string>('builder.rightSidebar.sections.settings.page.break-line.primary')}
|
||||||
secondary={t('builder.rightSidebar.sections.settings.page.break-line.secondary')}
|
secondary={t<string>('builder.rightSidebar.sections.settings.page.break-line.secondary')}
|
||||||
/>
|
/>
|
||||||
<Switch color="secondary" checked={breakLine} onChange={() => dispatch(togglePageBreakLine())} />
|
<Switch color="secondary" checked={breakLine} onChange={() => dispatch(togglePageBreakLine())} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@ -179,7 +181,7 @@ const Settings = () => {
|
|||||||
{/* Resume Settings */}
|
{/* Resume Settings */}
|
||||||
<>
|
<>
|
||||||
<ListSubheader className="rounded">
|
<ListSubheader className="rounded">
|
||||||
{t('builder.rightSidebar.sections.settings.resume.heading')}
|
{t<string>('builder.rightSidebar.sections.settings.resume.heading')}
|
||||||
</ListSubheader>
|
</ListSubheader>
|
||||||
|
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@ -188,8 +190,8 @@ const Settings = () => {
|
|||||||
<Anchor />
|
<Anchor />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('builder.rightSidebar.sections.settings.resume.sample.primary')}
|
primary={t<string>('builder.rightSidebar.sections.settings.resume.sample.primary')}
|
||||||
secondary={t('builder.rightSidebar.sections.settings.resume.sample.secondary')}
|
secondary={t<string>('builder.rightSidebar.sections.settings.resume.sample.secondary')}
|
||||||
/>
|
/>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@ -200,8 +202,8 @@ const Settings = () => {
|
|||||||
<DeleteForever />
|
<DeleteForever />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('builder.rightSidebar.sections.settings.resume.reset.primary')}
|
primary={t<string>('builder.rightSidebar.sections.settings.resume.reset.primary')}
|
||||||
secondary={t('builder.rightSidebar.sections.settings.resume.reset.secondary')}
|
secondary={t<string>('builder.rightSidebar.sections.settings.resume.reset.secondary')}
|
||||||
/>
|
/>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|||||||
@ -29,19 +29,19 @@ const Sharing = () => {
|
|||||||
|
|
||||||
await navigator.clipboard.writeText(text);
|
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 (
|
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 }}>
|
<List sx={{ padding: 0 }}>
|
||||||
<ListItem className="flex flex-col" sx={{ padding: 0 }}>
|
<ListItem className="flex flex-col" sx={{ padding: 0 }}>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('builder.rightSidebar.sections.sharing.visibility.title')}
|
primary={t<string>('builder.rightSidebar.sections.sharing.visibility.title')}
|
||||||
secondary={t('builder.rightSidebar.sections.sharing.visibility.subtitle')}
|
secondary={t<string>('builder.rightSidebar.sections.sharing.visibility.subtitle')}
|
||||||
/>
|
/>
|
||||||
<Switch color="secondary" checked={isPublic} onChange={(_, value) => handleSetVisibility(value)} />
|
<Switch color="secondary" checked={isPublic} onChange={(_, value) => handleSetVisibility(value)} />
|
||||||
</div>
|
</div>
|
||||||
@ -63,7 +63,7 @@ const Sharing = () => {
|
|||||||
|
|
||||||
<div className="mt-1 flex w-full">
|
<div className="mt-1 flex w-full">
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label={t('builder.rightSidebar.sections.sharing.short-url.label') as string}
|
label={t<string>('builder.rightSidebar.sections.sharing.short-url.label')}
|
||||||
control={
|
control={
|
||||||
<Checkbox className="mr-1" checked={showShortUrl} onChange={(_, value) => setShowShortUrl(value)} />
|
<Checkbox className="mr-1" checked={showShortUrl} onChange={(_, value) => setShowShortUrl(value)} />
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const Templates = () => {
|
|||||||
|
|
||||||
return (
|
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}>
|
<div className={styles.container}>
|
||||||
{Object.values(templateMap).map((template) => (
|
{Object.values(templateMap).map((template) => (
|
||||||
|
|||||||
@ -19,12 +19,12 @@ const Theme = () => {
|
|||||||
const { background, text, primary } = useAppSelector<ThemeType>((state) => get(state.resume, 'metadata.theme'));
|
const { background, text, primary } = useAppSelector<ThemeType>((state) => get(state.resume, 'metadata.theme'));
|
||||||
|
|
||||||
const handleChange = (property: string, color: string) => {
|
const handleChange = (property: string, color: string) => {
|
||||||
dispatch(setResumeState({ path: `metadata.theme.${property}`, value: color }));
|
dispatch(setResumeState({ path: `metadata.theme.${property}`, value: color[0] !== '#' ? `#${color}` : color }));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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.container}>
|
||||||
<div className={styles.colorOptions}>
|
<div className={styles.colorOptions}>
|
||||||
@ -34,18 +34,18 @@ const Theme = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label={t('builder.rightSidebar.sections.theme.form.primary.label')}
|
label={t<string>('builder.rightSidebar.sections.theme.form.primary.label')}
|
||||||
color={primary}
|
color={primary}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
onChange={(color) => handleChange('primary', color)}
|
onChange={(color) => handleChange('primary', color)}
|
||||||
/>
|
/>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label={t('builder.rightSidebar.sections.theme.form.background.label')}
|
label={t<string>('builder.rightSidebar.sections.theme.form.background.label')}
|
||||||
color={background}
|
color={background}
|
||||||
onChange={(color) => handleChange('background', color)}
|
onChange={(color) => handleChange('background', color)}
|
||||||
/>
|
/>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label={t('builder.rightSidebar.sections.theme.form.text.label')}
|
label={t<string>('builder.rightSidebar.sections.theme.form.text.label')}
|
||||||
color={text}
|
color={text}
|
||||||
onChange={(color) => handleChange('text', color)}
|
onChange={(color) => handleChange('text', color)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -64,7 +64,7 @@ const Widgets: React.FC<WidgetProps> = ({ label, category }) => {
|
|||||||
step={1}
|
step={1}
|
||||||
marks={[
|
marks={[
|
||||||
{ value: 12, label: '12px' },
|
{ 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' },
|
{ value: 36, label: '36px' },
|
||||||
]}
|
]}
|
||||||
valueLabelDisplay="auto"
|
valueLabelDisplay="auto"
|
||||||
@ -82,7 +82,10 @@ const Widgets: React.FC<WidgetProps> = ({ label, category }) => {
|
|||||||
value={fonts.find((font) => font.family === family[category])}
|
value={fonts.find((font) => font.family === family[category])}
|
||||||
onChange={(_, font: Font | null) => handleChange('family', font)}
|
onChange={(_, font: Font | null) => handleChange('family', font)}
|
||||||
renderInput={(params) => (
|
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>
|
</div>
|
||||||
@ -95,10 +98,13 @@ const Typography = () => {
|
|||||||
|
|
||||||
return (
|
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
|
||||||
<Widgets label={t('builder.rightSidebar.sections.typography.widgets.body.label')} category="body" />
|
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 });
|
const url = getResumeUrl(resume, { withHost: true });
|
||||||
await navigator.clipboard.writeText(url);
|
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 () => {
|
const handleDelete = async () => {
|
||||||
@ -124,7 +124,7 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
|||||||
<footer>
|
<footer>
|
||||||
<div className={styles.meta}>
|
<div className={styles.meta}>
|
||||||
<p>{resume.name}</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<ButtonBase className={styles.menu} onClick={handleOpenMenu}>
|
<ButtonBase className={styles.menu} onClick={handleOpenMenu}>
|
||||||
@ -136,21 +136,21 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<OpenInNew className="scale-90" />
|
<OpenInNew className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('dashboard.resume.menu.open')}</ListItemText>
|
<ListItemText>{t<string>('dashboard.resume.menu.open')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem onClick={handleRename}>
|
<MenuItem onClick={handleRename}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DriveFileRenameOutline className="scale-90" />
|
<DriveFileRenameOutline className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('dashboard.resume.menu.rename')}</ListItemText>
|
<ListItemText>{t<string>('dashboard.resume.menu.rename')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem onClick={handleDuplicate}>
|
<MenuItem onClick={handleDuplicate}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<ContentCopy className="scale-90" />
|
<ContentCopy className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('dashboard.resume.menu.duplicate')}</ListItemText>
|
<ListItemText>{t<string>('dashboard.resume.menu.duplicate')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
{resume.public ? (
|
{resume.public ? (
|
||||||
@ -158,27 +158,27 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LinkIcon className="scale-90" />
|
<LinkIcon className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('dashboard.resume.menu.share-link')}</ListItemText>
|
<ListItemText>{t<string>('dashboard.resume.menu.share-link')}</ListItemText>
|
||||||
</MenuItem>
|
</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>
|
<div>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LinkIcon className="scale-90" />
|
<LinkIcon className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('dashboard.resume.menu.share-link')}</ListItemText>
|
<ListItemText>{t<string>('dashboard.resume.menu.share-link')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</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}>
|
<MenuItem onClick={handleDelete}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DeleteOutline className="scale-90" />
|
<DeleteOutline className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('dashboard.resume.menu.delete')}</ListItemText>
|
<ListItemText>{t<string>('dashboard.resume.menu.delete')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@ -56,12 +56,12 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
|||||||
<Menu anchorEl={anchorEl} onClose={handleClose} open={Boolean(anchorEl)}>
|
<Menu anchorEl={anchorEl} onClose={handleClose} open={Boolean(anchorEl)}>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<div>
|
<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>
|
<p>{user?.name}</p>
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
<MenuItem onClick={handleLogout}>{t('common.avatar.menu.logout')}</MenuItem>
|
<MenuItem onClick={handleLogout}>{t<string>('common.avatar.menu.logout')}</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
.content {
|
.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 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 overflow-scroll bg-neutral-50 dark:bg-neutral-900 lg:overflow-auto;
|
||||||
|
@apply max-h-[90vh] min-h-fit;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
@apply sticky top-0 left-0 right-0 z-50 pt-6 bg-neutral-50 dark:bg-neutral-900;
|
||||||
@apply flex items-center justify-between;
|
@apply flex items-center justify-between;
|
||||||
@apply w-full border-b pb-5 dark:border-white/10;
|
@apply w-full border-b pb-5 dark:border-white/10;
|
||||||
|
|
||||||
@ -27,6 +33,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
|
@apply sticky bottom-0 left-0 right-0 z-50 pb-6 bg-neutral-50 dark:bg-neutral-900;
|
||||||
@apply flex items-center justify-end gap-x-4;
|
@apply flex items-center justify-end gap-x-4;
|
||||||
@apply w-full border-t pt-5 dark:border-white/10;
|
@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';
|
import styles from './BaseModal.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
icon?: React.ReactNode;
|
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
heading: string;
|
heading: string;
|
||||||
handleClose: () => void;
|
icon?: React.ReactNode;
|
||||||
|
children?: React.ReactNode;
|
||||||
footerChildren?: React.ReactNode;
|
footerChildren?: React.ReactNode;
|
||||||
|
handleClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BaseModal: React.FC<Props> = ({ icon, isOpen, heading, children, handleClose, footerChildren }) => {
|
const BaseModal: React.FC<Props> = ({ icon, isOpen, heading, children, handleClose, footerChildren }) => {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const Footer: React.FC<Props> = ({ className }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('text-xs', className)}>
|
<div className={clsx('text-xs', className)}>
|
||||||
<p>{t('common.footer.license')}</p>
|
<p>{t<string>('common.footer.license')}</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<Trans t={t} i18nKey="common.footer.credit">
|
<Trans t={t} i18nKey="common.footer.credit">
|
||||||
|
|||||||
@ -72,19 +72,19 @@ const Heading: React.FC<Props> = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isEditable && (
|
{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>
|
<IconButton onClick={toggleEditMode}>{editMode ? <Check /> : <DriveFileRenameOutline />}</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isHideable && (
|
{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>
|
<IconButton onClick={toggleVisibility}>{visibility ? <Visibility /> : <VisibilityOff />}</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isDeletable && (
|
{isDeletable && (
|
||||||
<Tooltip title={t('builder.common.tooltip.delete-section') as string}>
|
<Tooltip title={t<string>('builder.common.tooltip.delete-section')}>
|
||||||
<IconButton onClick={handleDelete}>
|
<IconButton onClick={handleDelete}>
|
||||||
<Delete />
|
<Delete />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@ -1,70 +1,51 @@
|
|||||||
import { Language } from '@mui/icons-material';
|
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 { useRouter } from 'next/router';
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { MouseEvent, useState } from 'react';
|
import { MouseEvent, useState } from 'react';
|
||||||
|
|
||||||
import { languages } from '@/config/languages';
|
import { languages } from '@/config/languages';
|
||||||
import { useAppDispatch } from '@/store/hooks';
|
import { TRANSLATE_URL } from '@/constants/index';
|
||||||
import { setResumeState } from '@/store/resume/resumeSlice';
|
|
||||||
|
|
||||||
import styles from './LanguageSwitcher.module.scss';
|
|
||||||
|
|
||||||
const LanguageSwitcher = () => {
|
const LanguageSwitcher = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
const handleClick = (event: MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget);
|
const handleClick = (event: MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget);
|
||||||
|
|
||||||
const handleClose = () => setAnchorEl(null);
|
const handleClose = () => setAnchorEl(null);
|
||||||
|
|
||||||
const handleChangeLanguage = (locale: string) => {
|
const handleChange = (locale: string) => {
|
||||||
const { pathname, asPath, query } = router;
|
const { pathname, asPath, query } = router;
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
|
||||||
document.cookie = `NEXT_LOCALE=${locale}; path=/; expires=2147483647`;
|
document.cookie = `NEXT_LOCALE=${locale}; path=/; expires=2147483647`;
|
||||||
dispatch(setResumeState({ path: 'metadata.locale', value: locale }));
|
|
||||||
|
|
||||||
router.push({ pathname, query }, asPath, { locale });
|
router.push({ pathname, query }, asPath, { locale });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddLanguage = () => window.open(TRANSLATE_URL, '_blank');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<IconButton onClick={handleClick}>
|
<IconButton onClick={handleClick}>
|
||||||
<Language />
|
<Language />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Popover
|
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleClose}>
|
||||||
anchorEl={anchorEl}
|
{languages.map(({ code, name, localName }) => (
|
||||||
open={Boolean(anchorEl)}
|
<MenuItem key={code} onClick={() => handleChange(code)}>
|
||||||
onClose={handleClose}
|
{name} {localName && `(${localName})`}
|
||||||
anchorOrigin={{
|
</MenuItem>
|
||||||
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>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<a href="https://translate.rxresu.me" target="_blank" rel="noreferrer" className={styles.language}>
|
<MenuItem>
|
||||||
{t('common.footer.language.missing')}
|
<span className="font-bold" onClick={handleAddLanguage}>
|
||||||
</a>
|
Add your language
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</MenuItem>
|
||||||
</Popover>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -66,7 +66,7 @@ const List: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<div className={clsx(styles.container, className)}>
|
<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) => {
|
{list.map((item, index) => {
|
||||||
const title = get(item, titleKey, '');
|
const title = get(item, titleKey, '');
|
||||||
@ -76,6 +76,7 @@ const List: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
|
path={path}
|
||||||
item={item}
|
item={item}
|
||||||
index={index}
|
index={index}
|
||||||
title={title}
|
title={title}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ interface DragItem {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
item: ListItemType;
|
item: ListItemType;
|
||||||
|
path: string;
|
||||||
index: number;
|
index: number;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
@ -26,14 +27,14 @@ type Props = {
|
|||||||
onDuplicate?: (item: ListItemType) => void;
|
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 { t } = useTranslation();
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
|
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
|
||||||
|
|
||||||
const [{ handlerId }, drop] = useDrop<DragItem, any, any>({
|
const [{ handlerId }, drop] = useDrop<DragItem, any, any>({
|
||||||
accept: 'ListItem',
|
accept: path,
|
||||||
collect(monitor) {
|
collect(monitor) {
|
||||||
return { handlerId: monitor.getHandlerId() };
|
return { handlerId: monitor.getHandlerId() };
|
||||||
},
|
},
|
||||||
@ -68,7 +69,7 @@ const ListItem: React.FC<Props> = ({ item, index, title, subtitle, onMove, onEdi
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [{ isDragging }, drag] = useDrag({
|
const [{ isDragging }, drag] = useDrag({
|
||||||
type: 'ListItem',
|
type: path,
|
||||||
item: () => {
|
item: () => {
|
||||||
return { id: item.id, index };
|
return { id: item.id, index };
|
||||||
},
|
},
|
||||||
@ -125,25 +126,25 @@ const ListItem: React.FC<Props> = ({ item, index, title, subtitle, onMove, onEdi
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DriveFileRenameOutline className="scale-90" />
|
<DriveFileRenameOutline className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('builder.common.list.actions.edit')}</ListItemText>
|
<ListItemText>{t<string>('builder.common.list.actions.edit')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem onClick={() => handleDuplicate(item)}>
|
<MenuItem onClick={() => handleDuplicate(item)}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<FileCopy className="scale-90" />
|
<FileCopy className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('builder.common.list.actions.duplicate')}</ListItemText>
|
<ListItemText>{t<string>('builder.common.list.actions.duplicate')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<Divider />
|
<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>
|
<div>
|
||||||
<MenuItem onClick={() => handleDelete(item)}>
|
<MenuItem onClick={() => handleDelete(item)}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DeleteOutline className="scale-90" />
|
<DeleteOutline className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('builder.common.list.actions.delete')}</ListItemText>
|
<ListItemText>{t<string>('builder.common.list.actions.delete')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -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 { DatePicker } from '@mui/lab';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@ -8,7 +11,7 @@ import { setResumeState } from '@/store/resume/resumeSlice';
|
|||||||
import MarkdownSupported from './MarkdownSupported';
|
import MarkdownSupported from './MarkdownSupported';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type?: 'text' | 'textarea';
|
type?: 'text' | 'textarea' | 'date';
|
||||||
label: string;
|
label: string;
|
||||||
path: string;
|
path: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -31,6 +34,11 @@ const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, m
|
|||||||
dispatch(setResumeState({ path, value: event.target.value }));
|
dispatch(setResumeState({ path, value: event.target.value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onChangeValue = (value: string) => {
|
||||||
|
setValue(value);
|
||||||
|
dispatch(setResumeState({ path, value }));
|
||||||
|
};
|
||||||
|
|
||||||
if (type === 'textarea') {
|
if (type === 'textarea') {
|
||||||
return (
|
return (
|
||||||
<TextField
|
<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).isValid() && onChangeValue(date.toISOString());
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <TextField type={type} label={label} value={value} onChange={onChange} className={className} />;
|
return <TextField type={type} label={label} value={value} onChange={onChange} className={className} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -7,17 +7,30 @@ export type Language = {
|
|||||||
export const languages: Language[] = [
|
export const languages: Language[] = [
|
||||||
{ code: 'ar', name: 'Arabic', localName: 'اَلْعَرَبِيَّةُ' },
|
{ code: 'ar', name: 'Arabic', localName: 'اَلْعَرَبِيَّةُ' },
|
||||||
{ code: 'bn', name: 'Bengali', localName: 'বাংলা' },
|
{ code: 'bn', name: 'Bengali', localName: 'বাংলা' },
|
||||||
|
{ code: 'cs', name: 'Czech', localName: 'čeština' },
|
||||||
{ code: 'da', name: 'Danish', localName: 'Dansk' },
|
{ code: 'da', name: 'Danish', localName: 'Dansk' },
|
||||||
{ code: 'de', name: 'German', localName: 'Deutsch' },
|
{ code: 'de', name: 'German', localName: 'Deutsch' },
|
||||||
|
{ code: 'el', name: 'Greek', localName: 'Ελληνικά' },
|
||||||
{ code: 'en', name: 'English' },
|
{ code: 'en', name: 'English' },
|
||||||
{ code: 'es', name: 'Spanish', localName: 'Español' },
|
{ code: 'es', name: 'Spanish', localName: 'Español' },
|
||||||
{ code: 'fr', name: 'French', localName: 'Français' },
|
{ code: 'fr', name: 'French', localName: 'Français' },
|
||||||
|
{ code: 'he', name: 'Hebrew', localName: 'Ivrit' },
|
||||||
{ code: 'hi', name: 'Hindi', localName: 'हिन्दी' },
|
{ 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: 'it', name: 'Italian', localName: 'Italiano' },
|
||||||
{ code: 'kn', name: 'Kannada', localName: 'ಕನ್ನಡ' },
|
{ 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: '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: 'ta', name: 'Tamil', localName: 'தமிழ்' },
|
||||||
{ code: 'tr', name: 'Turkish', localName: 'Türkçe' },
|
{ code: 'tr', name: 'Turkish', localName: 'Türkçe' },
|
||||||
|
{ code: 'vi', name: 'Vietnamese', localName: 'Tiếng Việt' },
|
||||||
{ code: 'zh', name: 'Chinese', localName: '中文' },
|
{ code: 'zh', name: 'Chinese', localName: '中文' },
|
||||||
].sort((a, b) => a.name.localeCompare(b.name));
|
].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
|||||||
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
|
// Links
|
||||||
export const DONATION_URL = 'https://paypal.me/RajaRajanA';
|
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 GITHUB_URL = 'https://github.com/AmruthPillai/Reactive-Resume';
|
||||||
export const PRODUCT_HUNT_URL = 'https://www.producthunt.com/posts/reactive-resume-v3';
|
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';
|
export const GITHUB_ISSUES_URL = 'https://github.com/AmruthPillai/Reactive-Resume/issues/new/choose';
|
||||||
|
|||||||
@ -54,16 +54,16 @@ const ForgotPasswordModal: React.FC = () => {
|
|||||||
<BaseModal
|
<BaseModal
|
||||||
icon={<Password />}
|
icon={<Password />}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
heading={t('modals.auth.forgot-password.heading')}
|
heading={t<string>('modals.auth.forgot-password.heading')}
|
||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
footerChildren={
|
footerChildren={
|
||||||
<Button type="submit" disabled={isLoading} onClick={handleSubmit(onSubmit)}>
|
<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>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="grid gap-4">
|
<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">
|
<form className="grid gap-4 xl:w-2/3">
|
||||||
<Controller
|
<Controller
|
||||||
@ -72,7 +72,7 @@ const ForgotPasswordModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('modals.auth.forgot-password.form.email.label')}
|
label={t<string>('modals.auth.forgot-password.form.email.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -81,7 +81,7 @@ const ForgotPasswordModal: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -6,17 +6,19 @@ import Joi from 'joi';
|
|||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import { Trans, useTranslation } from 'next-i18next';
|
import { Trans, useTranslation } from 'next-i18next';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { GoogleLoginResponse, GoogleLoginResponseOffline, useGoogleLogin } from 'react-google-login';
|
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useIsMutating, useMutation } from 'react-query';
|
import { useIsMutating, useMutation } from 'react-query';
|
||||||
|
|
||||||
import BaseModal from '@/components/shared/BaseModal';
|
import BaseModal from '@/components/shared/BaseModal';
|
||||||
|
import { FLAG_DISABLE_SIGNUPS } from '@/constants/flags';
|
||||||
import { login, LoginParams, loginWithGoogle, LoginWithGoogleParams } from '@/services/auth';
|
import { login, LoginParams, loginWithGoogle, LoginWithGoogleParams } from '@/services/auth';
|
||||||
import { ServerError } from '@/services/axios';
|
import { ServerError } from '@/services/axios';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
import { setModalState } from '@/store/modal/modalSlice';
|
import { setModalState } from '@/store/modal/modalSlice';
|
||||||
|
|
||||||
|
declare const google: any;
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
identifier: string;
|
identifier: string;
|
||||||
password: string;
|
password: string;
|
||||||
@ -55,15 +57,6 @@ const LoginModal: React.FC = () => {
|
|||||||
loginWithGoogle
|
loginWithGoogle
|
||||||
);
|
);
|
||||||
|
|
||||||
const { signIn } = useGoogleLogin({
|
|
||||||
clientId: env('GOOGLE_CLIENT_ID'),
|
|
||||||
onSuccess: async (response: GoogleLoginResponse | GoogleLoginResponseOffline) => {
|
|
||||||
await loginWithGoogleMutation({ accessToken: (response as GoogleLoginResponse).accessToken });
|
|
||||||
|
|
||||||
handleClose();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
dispatch(setModalState({ modal: 'auth.login', state: { open: false } }));
|
dispatch(setModalState({ modal: 'auth.login', state: { open: false } }));
|
||||||
reset();
|
reset();
|
||||||
@ -92,8 +85,18 @@ const LoginModal: React.FC = () => {
|
|||||||
dispatch(setModalState({ modal: 'auth.forgot', state: { open: true } }));
|
dispatch(setModalState({ modal: 'auth.forgot', state: { open: true } }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoginWithGoogle = () => {
|
const handleLoginWithGoogle = async () => {
|
||||||
signIn();
|
google.accounts.id.initialize({
|
||||||
|
client_id: env('GOOGLE_CLIENT_ID'),
|
||||||
|
callback: async (response: any) => {
|
||||||
|
await loginWithGoogleMutation({ credential: response.credential });
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
},
|
||||||
|
auto_select: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
google.accounts.id.prompt();
|
||||||
};
|
};
|
||||||
|
|
||||||
const PasswordVisibility = (): React.ReactElement => {
|
const PasswordVisibility = (): React.ReactElement => {
|
||||||
@ -112,7 +115,7 @@ const LoginModal: React.FC = () => {
|
|||||||
<BaseModal
|
<BaseModal
|
||||||
icon={<Login />}
|
icon={<Login />}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
heading={t('modals.auth.login.heading')}
|
heading={t<string>('modals.auth.login.heading')}
|
||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
footerChildren={
|
footerChildren={
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
@ -124,17 +127,17 @@ const LoginModal: React.FC = () => {
|
|||||||
startIcon={<Google />}
|
startIcon={<Google />}
|
||||||
onClick={handleLoginWithGoogle}
|
onClick={handleLoginWithGoogle}
|
||||||
>
|
>
|
||||||
{t('modals.auth.login.actions.google')}
|
{t<string>('modals.auth.login.actions.google')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
||||||
{t('modals.auth.login.actions.login')}
|
{t<string>('modals.auth.login.actions.login')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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">
|
<form className="grid gap-4 xl:w-2/3">
|
||||||
<Controller
|
<Controller
|
||||||
@ -143,9 +146,9 @@ const LoginModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('modals.auth.login.form.username.label')}
|
label={t<string>('modals.auth.login.form.username.label')}
|
||||||
error={!!fieldState.error}
|
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}
|
{...field}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -157,7 +160,7 @@ const LoginModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
label={t('modals.auth.login.form.password.label')}
|
label={t<string>('modals.auth.login.form.password.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
InputProps={{ endAdornment: <PasswordVisibility /> }}
|
InputProps={{ endAdornment: <PasswordVisibility /> }}
|
||||||
@ -167,11 +170,13 @@ const LoginModal: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p className="text-xs">
|
{!FLAG_DISABLE_SIGNUPS && (
|
||||||
<Trans t={t} i18nKey="modals.auth.login.register-text">
|
<p className="text-xs">
|
||||||
If you don't have one, you can <a onClick={handleCreateAccount}>create an account</a> here.
|
<Trans t={t} i18nKey="modals.auth.login.register-text">
|
||||||
</Trans>
|
If you don't have one, you can <a onClick={handleCreateAccount}>create an account</a> here.
|
||||||
</p>
|
</Trans>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<p className="text-xs">
|
<p className="text-xs">
|
||||||
<Trans t={t} i18nKey="modals.auth.login.recover-text">
|
<Trans t={t} i18nKey="modals.auth.login.recover-text">
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { Button, TextField } from '@mui/material';
|
|||||||
import Joi from 'joi';
|
import Joi from 'joi';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import { Trans, useTranslation } from 'next-i18next';
|
import { Trans, useTranslation } from 'next-i18next';
|
||||||
import { GoogleLoginResponse, GoogleLoginResponseOffline, useGoogleLogin } from 'react-google-login';
|
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { useMutation } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
|
|
||||||
@ -15,6 +14,8 @@ import { ServerError } from '@/services/axios';
|
|||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
import { setModalState } from '@/store/modal/modalSlice';
|
import { setModalState } from '@/store/modal/modalSlice';
|
||||||
|
|
||||||
|
declare const google: any;
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
name: string;
|
name: string;
|
||||||
username: string;
|
username: string;
|
||||||
@ -63,15 +64,6 @@ const RegisterModal: React.FC = () => {
|
|||||||
loginWithGoogle
|
loginWithGoogle
|
||||||
);
|
);
|
||||||
|
|
||||||
const { signIn } = useGoogleLogin({
|
|
||||||
clientId: env('GOOGLE_CLIENT_ID'),
|
|
||||||
onSuccess: async (response: GoogleLoginResponse | GoogleLoginResponseOffline) => {
|
|
||||||
await loginWithGoogleMutation({ accessToken: (response as GoogleLoginResponse).accessToken });
|
|
||||||
|
|
||||||
handleClose();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
dispatch(setModalState({ modal: 'auth.register', state: { open: false } }));
|
dispatch(setModalState({ modal: 'auth.register', state: { open: false } }));
|
||||||
reset();
|
reset();
|
||||||
@ -87,15 +79,25 @@ const RegisterModal: React.FC = () => {
|
|||||||
dispatch(setModalState({ modal: 'auth.login', state: { open: true } }));
|
dispatch(setModalState({ modal: 'auth.login', state: { open: true } }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoginWithGoogle = () => {
|
const handleLoginWithGoogle = async () => {
|
||||||
signIn();
|
google.accounts.id.initialize({
|
||||||
|
client_id: env('GOOGLE_CLIENT_ID'),
|
||||||
|
callback: async (response: any) => {
|
||||||
|
await loginWithGoogleMutation({ credential: response.credential });
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
},
|
||||||
|
auto_select: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
google.accounts.id.prompt();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseModal
|
<BaseModal
|
||||||
icon={<HowToReg />}
|
icon={<HowToReg />}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
heading={t('modals.auth.register.heading')}
|
heading={t<string>('modals.auth.register.heading')}
|
||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
footerChildren={
|
footerChildren={
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
@ -107,17 +109,17 @@ const RegisterModal: React.FC = () => {
|
|||||||
startIcon={<Google />}
|
startIcon={<Google />}
|
||||||
onClick={handleLoginWithGoogle}
|
onClick={handleLoginWithGoogle}
|
||||||
>
|
>
|
||||||
{t('modals.auth.register.actions.google')}
|
{t<string>('modals.auth.register.actions.google')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
||||||
{t('modals.auth.register.actions.register')}
|
{t<string>('modals.auth.register.actions.register')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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">
|
<form className="grid gap-4 md:grid-cols-2">
|
||||||
<Controller
|
<Controller
|
||||||
@ -126,7 +128,7 @@ const RegisterModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('modals.auth.register.form.name.label')}
|
label={t<string>('modals.auth.register.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -139,7 +141,7 @@ const RegisterModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('modals.auth.register.form.username.label')}
|
label={t<string>('modals.auth.register.form.username.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -153,7 +155,7 @@ const RegisterModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
type="email"
|
type="email"
|
||||||
label={t('modals.auth.register.form.email.label')}
|
label={t<string>('modals.auth.register.form.email.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
@ -168,7 +170,7 @@ const RegisterModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
type="password"
|
type="password"
|
||||||
label={t('modals.auth.register.form.password.label')}
|
label={t<string>('modals.auth.register.form.password.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -182,7 +184,7 @@ const RegisterModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
type="password"
|
type="password"
|
||||||
label={t('modals.auth.register.form.confirm-password.label')}
|
label={t<string>('modals.auth.register.form.confirm-password.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
|
|||||||
@ -65,15 +65,15 @@ const ResetPasswordModal: React.FC = () => {
|
|||||||
<BaseModal
|
<BaseModal
|
||||||
icon={<LockReset />}
|
icon={<LockReset />}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
heading={t('modals.auth.reset-password.heading')}
|
heading={t<string>('modals.auth.reset-password.heading')}
|
||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
footerChildren={
|
footerChildren={
|
||||||
<Button type="submit" disabled={isLoading} onClick={handleSubmit(onSubmit)}>
|
<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>
|
</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">
|
<form className="grid gap-4 md:grid-cols-2">
|
||||||
<Controller
|
<Controller
|
||||||
@ -83,7 +83,7 @@ const ResetPasswordModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
type="password"
|
type="password"
|
||||||
label={t('modals.auth.reset-password.form.password.label')}
|
label={t<string>('modals.auth.reset-password.form.password.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -97,7 +97,7 @@ const ResetPasswordModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
type="password"
|
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}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
|
|||||||
@ -50,8 +50,8 @@ const AwardModal: React.FC = () => {
|
|||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -101,7 +101,7 @@ const AwardModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.title.label')}
|
label={t<string>('builder.common.form.title.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -115,7 +115,7 @@ const AwardModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.leftSidebar.sections.awards.form.awarder.label')}
|
label={t<string>('builder.leftSidebar.sections.awards.form.awarder.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -130,7 +130,7 @@ const AwardModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.date.label')}
|
label={t<string>('builder.common.form.date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
@ -152,7 +152,7 @@ const AwardModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t<string>('builder.common.form.url.label')}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
@ -169,7 +169,7 @@ const AwardModal: React.FC = () => {
|
|||||||
multiline
|
multiline
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||||
|
|||||||
@ -50,8 +50,8 @@ const CertificateModal: React.FC = () => {
|
|||||||
|
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -101,7 +101,7 @@ const CertificateModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.name.label')}
|
label={t<string>('builder.common.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -115,7 +115,7 @@ const CertificateModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.leftSidebar.sections.certifications.form.issuer.label')}
|
label={t<string>('builder.leftSidebar.sections.certifications.form.issuer.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -130,7 +130,7 @@ const CertificateModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.date.label')}
|
label={t<string>('builder.common.form.date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
@ -152,7 +152,7 @@ const CertificateModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t<string>('builder.common.form.url.label')}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
@ -169,7 +169,7 @@ const CertificateModal: React.FC = () => {
|
|||||||
multiline
|
multiline
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||||
|
|||||||
@ -67,8 +67,8 @@ const CustomModal: React.FC = () => {
|
|||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -118,7 +118,7 @@ const CustomModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.title.label')}
|
label={t<string>('builder.common.form.title.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -131,7 +131,7 @@ const CustomModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.subtitle.label')}
|
label={t<string>('builder.common.form.subtitle.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -146,7 +146,7 @@ const CustomModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.start-date.label')}
|
label={t<string>('builder.common.form.start-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
@ -170,7 +170,7 @@ const CustomModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.end-date.label')}
|
label={t<string>('builder.common.form.end-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
@ -192,7 +192,7 @@ const CustomModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t<string>('builder.common.form.url.label')}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
@ -207,7 +207,7 @@ const CustomModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.level.label')}
|
label={t<string>('builder.common.form.level.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
@ -221,7 +221,7 @@ const CustomModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<div className="col-span-2">
|
<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">
|
<div className="px-10">
|
||||||
<Slider
|
<Slider
|
||||||
@ -245,7 +245,7 @@ const CustomModal: React.FC = () => {
|
|||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
valueLabelDisplay="auto"
|
valueLabelDisplay="auto"
|
||||||
aria-label={t('builder.common.form.levelNum.label')}
|
aria-label={t<string>('builder.common.form.levelNum.label')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -260,7 +260,7 @@ const CustomModal: React.FC = () => {
|
|||||||
multiline
|
multiline
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||||
@ -274,7 +274,7 @@ const CustomModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<ArrayInput
|
<ArrayInput
|
||||||
label={t('builder.common.form.keywords.label')}
|
label={t<string>('builder.common.form.keywords.label')}
|
||||||
value={field.value as string[]}
|
value={field.value as string[]}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
errors={fieldState.error}
|
errors={fieldState.error}
|
||||||
|
|||||||
@ -63,8 +63,8 @@ const EducationModal: React.FC = () => {
|
|||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -114,7 +114,7 @@ const EducationModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.leftSidebar.sections.education.form.institution.label')}
|
label={t<string>('builder.leftSidebar.sections.education.form.institution.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -128,7 +128,7 @@ const EducationModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.leftSidebar.sections.education.form.degree.label')}
|
label={t<string>('builder.leftSidebar.sections.education.form.degree.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -141,7 +141,7 @@ const EducationModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<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}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -154,7 +154,7 @@ const EducationModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.leftSidebar.sections.education.form.grade.label')}
|
label={t<string>('builder.leftSidebar.sections.education.form.grade.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -169,7 +169,7 @@ const EducationModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.start-date.label')}
|
label={t<string>('builder.common.form.start-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
@ -193,7 +193,7 @@ const EducationModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.end-date.label')}
|
label={t<string>('builder.common.form.end-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
@ -203,7 +203,7 @@ const EducationModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
error={!!fieldState.error}
|
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}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t<string>('builder.common.form.url.label')}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
@ -233,7 +233,7 @@ const EducationModal: React.FC = () => {
|
|||||||
multiline
|
multiline
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||||
@ -247,7 +247,7 @@ const EducationModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<ArrayInput
|
<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[]}
|
value={field.value as string[]}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
errors={fieldState.error}
|
errors={fieldState.error}
|
||||||
|
|||||||
@ -41,8 +41,8 @@ const InterestModal: React.FC = () => {
|
|||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -92,7 +92,7 @@ const InterestModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.name.label')}
|
label={t<string>('builder.common.form.name.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
@ -106,7 +106,7 @@ const InterestModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<ArrayInput
|
<ArrayInput
|
||||||
label={t('builder.common.form.keywords.label')}
|
label={t<string>('builder.common.form.keywords.label')}
|
||||||
value={field.value as string[]}
|
value={field.value as string[]}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
errors={fieldState.error}
|
errors={fieldState.error}
|
||||||
|
|||||||
@ -42,8 +42,8 @@ const LanguageModal: React.FC = () => {
|
|||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -93,7 +93,7 @@ const LanguageModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.name.label')}
|
label={t<string>('builder.common.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -107,7 +107,7 @@ const LanguageModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.common.form.level.label')}
|
label={t<string>('builder.common.form.level.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -120,7 +120,7 @@ const LanguageModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<div className="col-span-2">
|
<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">
|
<div className="px-10">
|
||||||
<Slider
|
<Slider
|
||||||
@ -144,7 +144,7 @@ const LanguageModal: React.FC = () => {
|
|||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
valueLabelDisplay="auto"
|
valueLabelDisplay="auto"
|
||||||
aria-label={t('builder.common.form.levelNum.label')}
|
aria-label={t<string>('builder.common.form.levelNum.label')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -42,11 +42,11 @@ const ProfileModal: React.FC = () => {
|
|||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = t('builder.common.actions.add', {
|
const addText = t<string>('builder.common.actions.add', {
|
||||||
token: t('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
token: t<string>('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
||||||
});
|
});
|
||||||
const editText = t('builder.common.actions.edit', {
|
const editText = t<string>('builder.common.actions.edit', {
|
||||||
token: t('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
token: t<string>('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
@ -97,7 +97,7 @@ const ProfileModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.leftSidebar.sections.profiles.form.network.label')}
|
label={t<string>('builder.leftSidebar.sections.profiles.form.network.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -111,7 +111,7 @@ const ProfileModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.leftSidebar.sections.profiles.form.username.label')}
|
label={t<string>('builder.leftSidebar.sections.profiles.form.username.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@ -127,7 +127,7 @@ const ProfileModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t<string>('builder.common.form.url.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
|
|||||||
@ -59,8 +59,8 @@ const ProjectModal: React.FC = () => {
|
|||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -110,7 +110,7 @@ const ProjectModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.name.label')}
|
label={t<string>('builder.common.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -124,7 +124,7 @@ const ProjectModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.common.form.description.label')}
|
label={t<string>('builder.common.form.description.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -139,7 +139,7 @@ const ProjectModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.start-date.label')}
|
label={t<string>('builder.common.form.start-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
@ -163,7 +163,7 @@ const ProjectModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.end-date.label')}
|
label={t<string>('builder.common.form.end-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
@ -185,7 +185,7 @@ const ProjectModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t<string>('builder.common.form.url.label')}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
@ -203,7 +203,7 @@ const ProjectModal: React.FC = () => {
|
|||||||
multiline
|
multiline
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||||
@ -217,7 +217,7 @@ const ProjectModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<ArrayInput
|
<ArrayInput
|
||||||
label={t('builder.common.form.keywords.label')}
|
label={t<string>('builder.common.form.keywords.label')}
|
||||||
value={field.value as string[]}
|
value={field.value as string[]}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
errors={fieldState.error}
|
errors={fieldState.error}
|
||||||
|
|||||||
@ -50,8 +50,8 @@ const PublicationModal: React.FC = () => {
|
|||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -101,7 +101,7 @@ const PublicationModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.name.label')}
|
label={t<string>('builder.common.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -115,7 +115,7 @@ const PublicationModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.leftSidebar.sections.publications.form.publisher.label')}
|
label={t<string>('builder.leftSidebar.sections.publications.form.publisher.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -130,7 +130,7 @@ const PublicationModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.date.label')}
|
label={t<string>('builder.common.form.date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
@ -152,7 +152,7 @@ const PublicationModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t<string>('builder.common.form.url.label')}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
@ -169,7 +169,7 @@ const PublicationModal: React.FC = () => {
|
|||||||
multiline
|
multiline
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||||
|
|||||||
@ -47,8 +47,8 @@ const ReferenceModal: React.FC = () => {
|
|||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -98,7 +98,7 @@ const ReferenceModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.name.label')}
|
label={t<string>('builder.common.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -112,7 +112,7 @@ const ReferenceModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.leftSidebar.sections.references.form.relationship.label')}
|
label={t<string>('builder.leftSidebar.sections.references.form.relationship.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -125,7 +125,7 @@ const ReferenceModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.phone.label')}
|
label={t<string>('builder.common.form.phone.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -138,7 +138,7 @@ const ReferenceModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.email.label')}
|
label={t<string>('builder.common.form.email.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -154,7 +154,7 @@ const ReferenceModal: React.FC = () => {
|
|||||||
multiline
|
multiline
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||||
|
|||||||
@ -45,8 +45,8 @@ const SkillModal: React.FC = () => {
|
|||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -96,7 +96,7 @@ const SkillModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.name.label')}
|
label={t<string>('builder.common.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -109,7 +109,7 @@ const SkillModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.level.label')}
|
label={t<string>('builder.common.form.level.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -122,7 +122,7 @@ const SkillModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<div className="col-span-2">
|
<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">
|
<div className="px-3">
|
||||||
<Slider
|
<Slider
|
||||||
@ -146,7 +146,7 @@ const SkillModal: React.FC = () => {
|
|||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
valueLabelDisplay="auto"
|
valueLabelDisplay="auto"
|
||||||
aria-label={t('builder.common.form.levelNum.label')}
|
aria-label={t<string>('builder.common.form.levelNum.label')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -158,7 +158,7 @@ const SkillModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<ArrayInput
|
<ArrayInput
|
||||||
label={t('builder.common.form.keywords.label')}
|
label={t<string>('builder.common.form.keywords.label')}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
errors={fieldState.error}
|
errors={fieldState.error}
|
||||||
|
|||||||
@ -56,8 +56,8 @@ const VolunteerModal: React.FC = () => {
|
|||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -107,7 +107,7 @@ const VolunteerModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.leftSidebar.sections.volunteer.form.organization.label')}
|
label={t<string>('builder.leftSidebar.sections.volunteer.form.organization.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -121,7 +121,7 @@ const VolunteerModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.common.form.position.label')}
|
label={t<string>('builder.common.form.position.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -136,7 +136,7 @@ const VolunteerModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.start-date.label')}
|
label={t<string>('builder.common.form.start-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
@ -160,7 +160,7 @@ const VolunteerModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.end-date.label')}
|
label={t<string>('builder.common.form.end-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
@ -182,7 +182,7 @@ const VolunteerModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t<string>('builder.common.form.url.label')}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
@ -200,7 +200,7 @@ const VolunteerModal: React.FC = () => {
|
|||||||
multiline
|
multiline
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||||
|
|||||||
@ -56,8 +56,8 @@ const WorkModal: React.FC = () => {
|
|||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -107,7 +107,7 @@ const WorkModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.name.label')}
|
label={t<string>('builder.common.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -121,7 +121,7 @@ const WorkModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.common.form.position.label')}
|
label={t<string>('builder.common.form.position.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -136,7 +136,7 @@ const WorkModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.start-date.label')}
|
label={t<string>('builder.common.form.start-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
@ -160,7 +160,7 @@ const WorkModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.end-date.label')}
|
label={t<string>('builder.common.form.end-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
@ -170,7 +170,7 @@ const WorkModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
error={!!fieldState.error}
|
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}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t<string>('builder.common.form.url.label')}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
@ -200,7 +200,7 @@ const WorkModal: React.FC = () => {
|
|||||||
multiline
|
multiline
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||||
|
|||||||
@ -58,7 +58,7 @@ const CreateResumeModal: React.FC = () => {
|
|||||||
const slug = name
|
const slug = name
|
||||||
? name
|
? name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[`~!@#$%^&*()_|+=?;:'",.<>{}[]\\\/]/gi, '')
|
.replace(/[^\w\s]/gi, '')
|
||||||
.replace(/[ ]/gi, '-')
|
.replace(/[ ]/gi, '-')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
@ -85,15 +85,15 @@ const CreateResumeModal: React.FC = () => {
|
|||||||
<BaseModal
|
<BaseModal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
icon={<Add />}
|
icon={<Add />}
|
||||||
heading={t('modals.dashboard.create-resume.heading')}
|
heading={t<string>('modals.dashboard.create-resume.heading')}
|
||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
footerChildren={
|
footerChildren={
|
||||||
<Button type="submit" disabled={isLoading} onClick={handleSubmit(onSubmit)}>
|
<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>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<p>{t('modals.dashboard.create-resume.body')}</p>
|
<p>{t<string>('modals.dashboard.create-resume.body')}</p>
|
||||||
|
|
||||||
<form className="grid gap-4">
|
<form className="grid gap-4">
|
||||||
<Controller
|
<Controller
|
||||||
@ -102,7 +102,7 @@ const CreateResumeModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('modals.dashboard.create-resume.form.name.label')}
|
label={t<string>('modals.dashboard.create-resume.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -115,7 +115,7 @@ const CreateResumeModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('modals.dashboard.create-resume.form.slug.label')}
|
label={t<string>('modals.dashboard.create-resume.form.slug.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -125,7 +125,7 @@ const CreateResumeModal: React.FC = () => {
|
|||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label={t('modals.dashboard.create-resume.form.public.label') as string}
|
label={t<string>('modals.dashboard.create-resume.form.public.label')}
|
||||||
control={
|
control={
|
||||||
<Controller
|
<Controller
|
||||||
name="isPublic"
|
name="isPublic"
|
||||||
|
|||||||
@ -63,7 +63,7 @@ const ImportExternalModal: React.FC = () => {
|
|||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
|
|
||||||
if (file.size > FILE_UPLOAD_MAX_SIZE) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,13 +78,13 @@ const ImportExternalModal: React.FC = () => {
|
|||||||
<BaseModal
|
<BaseModal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
icon={<ImportExport />}
|
icon={<ImportExport />}
|
||||||
heading={t('modals.dashboard.import-external.heading')}
|
heading={t<string>('modals.dashboard.import-external.heading')}
|
||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
>
|
>
|
||||||
<div className="grid gap-5">
|
<div className="grid gap-5">
|
||||||
<h2 className="inline-flex items-center gap-2 text-lg font-medium">
|
<h2 className="inline-flex items-center gap-2 text-lg font-medium">
|
||||||
<LinkedIn />
|
<LinkedIn />
|
||||||
{t('modals.dashboard.import-external.linkedin.heading')}
|
{t<string>('modals.dashboard.import-external.linkedin.heading')}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className="mb-2">
|
<p className="mb-2">
|
||||||
@ -110,7 +110,7 @@ const ImportExternalModal: React.FC = () => {
|
|||||||
startIcon={<UploadFile />}
|
startIcon={<UploadFile />}
|
||||||
onClick={() => handleClick('linkedin')}
|
onClick={() => handleClick('linkedin')}
|
||||||
>
|
>
|
||||||
{t('modals.dashboard.import-external.linkedin.actions.upload-archive')}
|
{t<string>('modals.dashboard.import-external.linkedin.actions.upload-archive')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@ -128,7 +128,7 @@ const ImportExternalModal: React.FC = () => {
|
|||||||
<div className="grid gap-5">
|
<div className="grid gap-5">
|
||||||
<h2 className="inline-flex items-center gap-2 text-lg font-medium">
|
<h2 className="inline-flex items-center gap-2 text-lg font-medium">
|
||||||
<Code />
|
<Code />
|
||||||
{t('modals.dashboard.import-external.json-resume.heading')}
|
{t<string>('modals.dashboard.import-external.json-resume.heading')}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className="mb-2">
|
<p className="mb-2">
|
||||||
@ -154,7 +154,7 @@ const ImportExternalModal: React.FC = () => {
|
|||||||
startIcon={<UploadFile />}
|
startIcon={<UploadFile />}
|
||||||
onClick={() => handleClick('json-resume')}
|
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>
|
</Button>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@ -172,10 +172,10 @@ const ImportExternalModal: React.FC = () => {
|
|||||||
<div className="grid gap-5">
|
<div className="grid gap-5">
|
||||||
<h2 className="inline-flex items-center gap-2 text-lg font-medium">
|
<h2 className="inline-flex items-center gap-2 text-lg font-medium">
|
||||||
<TrackChanges />
|
<TrackChanges />
|
||||||
{t('modals.dashboard.import-external.reactive-resume.heading')}
|
{t<string>('modals.dashboard.import-external.reactive-resume.heading')}
|
||||||
</h2>
|
</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">
|
<div className="flex gap-4">
|
||||||
<Button
|
<Button
|
||||||
@ -184,7 +184,7 @@ const ImportExternalModal: React.FC = () => {
|
|||||||
startIcon={<UploadFile />}
|
startIcon={<UploadFile />}
|
||||||
onClick={() => handleClick('reactive-resume')}
|
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>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -193,7 +193,7 @@ const ImportExternalModal: React.FC = () => {
|
|||||||
startIcon={<UploadFile />}
|
startIcon={<UploadFile />}
|
||||||
onClick={() => handleClick('reactive-resume-v2')}
|
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>
|
</Button>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
|||||||
@ -56,7 +56,7 @@ const RenameResumeModal: React.FC = () => {
|
|||||||
const slug = name
|
const slug = name
|
||||||
? name
|
? name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[`~!@#$%^&*()_|+=?;:'",.<>{}[]\\\/]/gi, '')
|
.replace(/[^\w\s]/gi, '')
|
||||||
.replace(/[ ]/gi, '-')
|
.replace(/[ ]/gi, '-')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
@ -92,11 +92,11 @@ const RenameResumeModal: React.FC = () => {
|
|||||||
<BaseModal
|
<BaseModal
|
||||||
icon={<DriveFileRenameOutline />}
|
icon={<DriveFileRenameOutline />}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
heading={t('modals.dashboard.rename-resume.heading')}
|
heading={t<string>('modals.dashboard.rename-resume.heading')}
|
||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
footerChildren={
|
footerChildren={
|
||||||
<Button type="submit" disabled={isLoading} onClick={handleSubmit(onSubmit)}>
|
<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>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -107,7 +107,7 @@ const RenameResumeModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('modals.dashboard.rename-resume.form.name.label')}
|
label={t<string>('modals.dashboard.rename-resume.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -120,7 +120,7 @@ const RenameResumeModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('modals.dashboard.rename-resume.form.slug.label')}
|
label={t<string>('modals.dashboard.rename-resume.form.slug.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
|
|||||||
@ -3,7 +3,35 @@ const path = require('path');
|
|||||||
const i18nConfig = {
|
const i18nConfig = {
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: 'en',
|
defaultLocale: 'en',
|
||||||
locales: ['ar', 'bn', 'da', 'de', 'en', 'es', 'fr', 'hi', 'it', 'kn', 'pl', 'ta', 'tr', 'zh'],
|
locales: [
|
||||||
|
'ar',
|
||||||
|
'bn',
|
||||||
|
'cs',
|
||||||
|
'da',
|
||||||
|
'de',
|
||||||
|
'el',
|
||||||
|
'en',
|
||||||
|
'es',
|
||||||
|
'fa',
|
||||||
|
'fr',
|
||||||
|
'he',
|
||||||
|
'hi',
|
||||||
|
'hu',
|
||||||
|
'id',
|
||||||
|
'it',
|
||||||
|
'kn',
|
||||||
|
'ml',
|
||||||
|
'nl',
|
||||||
|
'or',
|
||||||
|
'pl',
|
||||||
|
'pt',
|
||||||
|
'ru',
|
||||||
|
'sv',
|
||||||
|
'ta',
|
||||||
|
'tr',
|
||||||
|
'vi',
|
||||||
|
'zh',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
nsSeparator: '.',
|
nsSeparator: '.',
|
||||||
localePath: path.resolve('./public/locales'),
|
localePath: path.resolve('./public/locales'),
|
||||||
|
|||||||
@ -12,7 +12,7 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
images: {
|
images: {
|
||||||
domains: ['www.gravatar.com'],
|
domains: ['cdn.rxresu.me', 'www.gravatar.com'],
|
||||||
},
|
},
|
||||||
|
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
|
|||||||
@ -9,70 +9,74 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@beam-australia/react-env": "^3.1.1",
|
"@beam-australia/react-env": "^3.1.1",
|
||||||
"@emotion/css": "^11.7.1",
|
"@date-io/dayjs": "^2.14.0",
|
||||||
"@emotion/react": "^11.8.2",
|
"@emotion/css": "^11.9.0",
|
||||||
"@emotion/styled": "^11.8.1",
|
"@emotion/react": "^11.9.3",
|
||||||
"@hookform/resolvers": "2.8.8",
|
"@emotion/styled": "^11.9.3",
|
||||||
"@monaco-editor/react": "^4.3.1",
|
"@hookform/resolvers": "2.9.1",
|
||||||
"@mui/icons-material": "^5.5.1",
|
"@monaco-editor/react": "^4.4.5",
|
||||||
"@mui/lab": "^5.0.0-alpha.73",
|
"@mui/icons-material": "^5.8.4",
|
||||||
"@mui/material": "^5.5.1",
|
"@mui/lab": "^5.0.0-alpha.86",
|
||||||
"@reduxjs/toolkit": "^1.8.0",
|
"@mui/material": "^5.8.4",
|
||||||
"axios": "^0.26.1",
|
"@mui/system": "^5.8.4",
|
||||||
|
"@mui/x-date-pickers": "5.0.0-alpha.6",
|
||||||
|
"@next/env": "^12.1.6",
|
||||||
|
"@reduxjs/toolkit": "^1.8.2",
|
||||||
|
"axios": "^0.27.2",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"dayjs": "^1.11.0",
|
"dayjs": "^1.11.3",
|
||||||
"downloadjs": "^1.4.7",
|
"downloadjs": "^1.4.7",
|
||||||
"joi": "^17.6.0",
|
"joi": "^17.6.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"md5-hex": "^4.0.0",
|
"md5-hex": "^4.0.0",
|
||||||
"monaco-editor": "^0.33.0",
|
"monaco-editor": "^0.33.0",
|
||||||
"nanoid": "^3.3.1",
|
"nanoid": "^3.3.4",
|
||||||
"next": "12.1.0",
|
"next": "12.1.6",
|
||||||
"next-i18next": "^10.5.0",
|
"next-i18next": "^11.0.0",
|
||||||
"react": ">=17",
|
"react": "18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-colorful": "^5.5.1",
|
"react-colorful": "^5.5.1",
|
||||||
"react-dnd": "^15.1.1",
|
"react-dnd": "16.0.1",
|
||||||
"react-dnd-html5-backend": "^15.1.2",
|
"react-dnd-html5-backend": "16.0.1",
|
||||||
"react-dom": ">=17",
|
"react-dom": "18.2.0",
|
||||||
"react-google-login": "^5.2.2",
|
"react-hook-form": "^7.32.2",
|
||||||
"react-hook-form": "^7.28.0",
|
|
||||||
"react-hot-toast": "2.2.0",
|
"react-hot-toast": "2.2.0",
|
||||||
"react-hotkeys-hook": "^3.4.4",
|
"react-hotkeys-hook": "^3.4.6",
|
||||||
"react-icons": "^4.3.1",
|
"react-icons": "^4.4.0",
|
||||||
"react-markdown": "^8.0.1",
|
"react-markdown": "^8.0.3",
|
||||||
"react-query": "^3.34.16",
|
"react-query": "^3.39.1",
|
||||||
"react-redux": "^7.2.6",
|
"react-redux": "^8.0.2",
|
||||||
"react-zoom-pan-pinch": "^2.1.3",
|
"react-zoom-pan-pinch": "^2.1.3",
|
||||||
"redux": "^4.1.2",
|
"redux": "^4.2.0",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"redux-saga": "^1.1.3",
|
"redux-saga": "^1.1.3",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"sharp": "^0.30.3",
|
"sharp": "^0.30.6",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"webfontloader": "^1.6.28"
|
"webfontloader": "^1.6.28"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.17.7",
|
"@babel/core": "^7.18.5",
|
||||||
"@reactive-resume/schema": "workspace:*",
|
"@reactive-resume/schema": "workspace:*",
|
||||||
"@tailwindcss/typography": "^0.5.2",
|
"@tailwindcss/typography": "^0.5.2",
|
||||||
"@types/downloadjs": "^1.4.3",
|
"@types/downloadjs": "^1.4.3",
|
||||||
"@types/lodash": "^4.14.180",
|
"@types/lodash": "^4.14.182",
|
||||||
"@types/node": "17.0.21",
|
"@types/node": "18.0.0",
|
||||||
"@types/react": "17.0.40",
|
"@types/react": "18.0.14",
|
||||||
"@types/react-beautiful-dnd": "^13.1.2",
|
"@types/react-beautiful-dnd": "^13.1.2",
|
||||||
"@types/react-redux": "^7.1.23",
|
"@types/react-redux": "^7.1.24",
|
||||||
"@types/tailwindcss": "^3.0.9",
|
"@types/tailwindcss": "^3.0.10",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"@types/webfontloader": "^1.6.34",
|
"@types/webfontloader": "^1.6.34",
|
||||||
"autoprefixer": "^10.4.3",
|
"autoprefixer": "^10.4.7",
|
||||||
"eslint": "^8.11.0",
|
"csstype": "^3.1.0",
|
||||||
"eslint-config-next": "12.1.0",
|
"eslint": "^8.18.0",
|
||||||
"next-sitemap": "^2.5.10",
|
"eslint-config-next": "12.1.6",
|
||||||
"postcss": "^8.4.11",
|
"next-sitemap": "^3.1.1",
|
||||||
"prettier": "^2.6.0",
|
"postcss": "^8.4.14",
|
||||||
"sass": "^1.49.9",
|
"prettier": "^2.7.1",
|
||||||
"tailwindcss": "^3.0.23",
|
"sass": "^1.52.3",
|
||||||
"typescript": "<4.6.0"
|
"tailwindcss": "^3.1.3",
|
||||||
|
"typescript": "^4.7.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,7 +62,7 @@ const Build: NextPage<Props> = ({ username, slug }) => {
|
|||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
{resume.name} | {t('common.title')}
|
{resume.name} | {t<string>('common.title')}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
|
|||||||
@ -59,6 +59,14 @@ const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
|
|||||||
}
|
}
|
||||||
}, [dispatch, 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 }), {
|
useQuery<Resume>(`resume/${username}/${slug}`, () => fetchResumeByIdentifier({ username, slug }), {
|
||||||
initialData,
|
initialData,
|
||||||
retry: false,
|
retry: false,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import clsx from 'clsx';
|
|||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
import { GetServerSideProps, NextPage } from 'next';
|
import { GetServerSideProps, NextPage } from 'next';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ type QueryParams = {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
resume?: Resume;
|
resume?: Resume;
|
||||||
|
locale: string;
|
||||||
redirect?: any;
|
redirect?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -35,7 +37,13 @@ export const getServerSideProps: GetServerSideProps<Props | Promise<Props>, Quer
|
|||||||
const resume = await fetchResumeByIdentifier({ username, slug, options: { secretKey } });
|
const resume = await fetchResumeByIdentifier({ username, slug, options: { secretKey } });
|
||||||
const displayLocale = resume.metadata.locale || locale || 'en';
|
const displayLocale = resume.metadata.locale || locale || 'en';
|
||||||
|
|
||||||
return { props: { resume, ...(await serverSideTranslations(displayLocale, ['common'])) } };
|
return {
|
||||||
|
props: {
|
||||||
|
resume,
|
||||||
|
locale: displayLocale,
|
||||||
|
...(await serverSideTranslations(displayLocale, ['common'])),
|
||||||
|
},
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@ -46,7 +54,9 @@ export const getServerSideProps: GetServerSideProps<Props | Promise<Props>, Quer
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Printer: NextPage<Props> = ({ resume: initialData }) => {
|
const Printer: NextPage<Props> = ({ resume: initialData, locale }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const resume = useAppSelector((state) => state.resume);
|
const resume = useAppSelector((state) => state.resume);
|
||||||
@ -55,6 +65,12 @@ const Printer: NextPage<Props> = ({ resume: initialData }) => {
|
|||||||
if (initialData) dispatch(setResume(initialData));
|
if (initialData) dispatch(setResume(initialData));
|
||||||
}, [dispatch, initialData]);
|
}, [dispatch, initialData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { pathname, asPath, query } = router;
|
||||||
|
|
||||||
|
router.push({ pathname, query }, asPath, { locale });
|
||||||
|
}, [router, locale]);
|
||||||
|
|
||||||
if (!resume || isEmpty(resume)) return null;
|
if (!resume || isEmpty(resume)) return null;
|
||||||
|
|
||||||
const layout: string[][][] = get(resume, 'metadata.layout', []);
|
const layout: string[][][] = get(resume, 'metadata.layout', []);
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import '@/styles/globals.scss';
|
import '@/styles/globals.scss';
|
||||||
|
|
||||||
import DateAdapter from '@mui/lab/AdapterDayjs';
|
import DayjsAdapter from '@date-io/dayjs';
|
||||||
import LocalizationProvider from '@mui/lab/LocalizationProvider';
|
import { LocalizationProvider } from '@mui/lab';
|
||||||
import type { AppProps } from 'next/app';
|
import type { AppProps } from 'next/app';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
|
import Script from 'next/script';
|
||||||
import { appWithTranslation } from 'next-i18next';
|
import { appWithTranslation } from 'next-i18next';
|
||||||
import { Toaster } from 'react-hot-toast';
|
import { Toaster } from 'react-hot-toast';
|
||||||
import { QueryClientProvider } from 'react-query';
|
import { QueryClientProvider } from 'react-query';
|
||||||
@ -31,7 +32,7 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
|||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<ReduxProvider store={store}>
|
<ReduxProvider store={store}>
|
||||||
<LocalizationProvider dateAdapter={DateAdapter}>
|
<LocalizationProvider dateAdapter={DayjsAdapter}>
|
||||||
<PersistGate loading={null} persistor={persistor}>
|
<PersistGate loading={null} persistor={persistor}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<WrapperRegistry>
|
<WrapperRegistry>
|
||||||
@ -52,6 +53,8 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
|||||||
</PersistGate>
|
</PersistGate>
|
||||||
</LocalizationProvider>
|
</LocalizationProvider>
|
||||||
</ReduxProvider>
|
</ReduxProvider>
|
||||||
|
|
||||||
|
<Script src="https://accounts.google.com/gsi/client" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -33,7 +33,7 @@ const Dashboard: NextPage = () => {
|
|||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
{t('dashboard.title')} | {t('common.title')}
|
{t<string>('dashboard.title')} | {t<string>('common.title')}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
@ -51,15 +51,15 @@ const Dashboard: NextPage = () => {
|
|||||||
<ResumeCard
|
<ResumeCard
|
||||||
modal="dashboard.create-resume"
|
modal="dashboard.create-resume"
|
||||||
icon={Add}
|
icon={Add}
|
||||||
title={t('dashboard.create-resume.title')}
|
title={t<string>('dashboard.create-resume.title')}
|
||||||
subtitle={t('dashboard.create-resume.subtitle')}
|
subtitle={t<string>('dashboard.create-resume.subtitle')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ResumeCard
|
<ResumeCard
|
||||||
modal="dashboard.import-external"
|
modal="dashboard.import-external"
|
||||||
icon={ImportExport}
|
icon={ImportExport}
|
||||||
title={t('dashboard.import-external.title')}
|
title={t<string>('dashboard.import-external.title')}
|
||||||
subtitle={t('dashboard.import-external.subtitle')}
|
subtitle={t<string>('dashboard.import-external.subtitle')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{data.map((resume) => (
|
{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 { Masonry } from '@mui/lab';
|
||||||
import { Button } from '@mui/material';
|
import { Button, IconButton, NoSsr } from '@mui/material';
|
||||||
import type { GetStaticProps, NextPage } from 'next';
|
import type { GetStaticProps, NextPage } from 'next';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@ -11,15 +11,16 @@ import Testimony from '@/components/landing/Testimony';
|
|||||||
import Footer from '@/components/shared/Footer';
|
import Footer from '@/components/shared/Footer';
|
||||||
import LanguageSwitcher from '@/components/shared/LanguageSwitcher';
|
import LanguageSwitcher from '@/components/shared/LanguageSwitcher';
|
||||||
import Logo from '@/components/shared/Logo';
|
import Logo from '@/components/shared/Logo';
|
||||||
import NoSSR from '@/components/shared/NoSSR';
|
|
||||||
import { screenshots } from '@/config/screenshots';
|
import { screenshots } from '@/config/screenshots';
|
||||||
|
import { FLAG_DISABLE_SIGNUPS } from '@/constants/flags';
|
||||||
import testimonials from '@/data/testimonials';
|
import testimonials from '@/data/testimonials';
|
||||||
import { logout } from '@/store/auth/authSlice';
|
import { logout } from '@/store/auth/authSlice';
|
||||||
|
import { setTheme } from '@/store/build/buildSlice';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
import { setModalState } from '@/store/modal/modalSlice';
|
import { setModalState } from '@/store/modal/modalSlice';
|
||||||
import styles from '@/styles/pages/Home.module.scss';
|
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' }) => {
|
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => {
|
||||||
return {
|
return {
|
||||||
@ -34,12 +35,15 @@ const Home: NextPage = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const theme = useAppSelector((state) => state.build.theme);
|
||||||
const isLoggedIn = useAppSelector((state) => state.auth.isLoggedIn);
|
const isLoggedIn = useAppSelector((state) => state.auth.isLoggedIn);
|
||||||
|
|
||||||
const handleLogin = () => dispatch(setModalState({ modal: 'auth.login', state: { open: true } }));
|
const handleLogin = () => dispatch(setModalState({ modal: 'auth.login', state: { open: true } }));
|
||||||
|
|
||||||
const handleRegister = () => dispatch(setModalState({ modal: 'auth.register', 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());
|
const handleLogout = () => dispatch(logout());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -50,52 +54,52 @@ const Home: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.main}>
|
<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}>
|
<div className={styles.buttonWrapper}>
|
||||||
{isLoggedIn ? (
|
{isLoggedIn ? (
|
||||||
<>
|
<>
|
||||||
<Link href="/dashboard" passHref>
|
<Link href="/dashboard" passHref>
|
||||||
<Button>{t('landing.actions.app')}</Button>
|
<Button>{t<string>('landing.actions.app')}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Button variant="outlined" onClick={handleLogout}>
|
<Button variant="outlined" onClick={handleLogout}>
|
||||||
{t('landing.actions.logout')}
|
{t<string>('landing.actions.logout')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Button onClick={handleLogin}>{t('landing.actions.login')}</Button>
|
<Button onClick={handleLogin}>{t<string>('landing.actions.login')}</Button>
|
||||||
|
|
||||||
<Button variant="outlined" onClick={handleRegister}>
|
<Button variant="outlined" onClick={handleRegister} disabled={FLAG_DISABLE_SIGNUPS}>
|
||||||
{t('landing.actions.register')}
|
{t<string>('landing.actions.register')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</NoSSR>
|
</NoSsr>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className={styles.section}>
|
<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>
|
||||||
|
|
||||||
<section className={styles.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">
|
<ul className="list-inside list-disc leading-loose">
|
||||||
<li>{t('landing.features.list.free')}</li>
|
<li>{t<string>('landing.features.list.free')}</li>
|
||||||
<li>{t('landing.features.list.ads')}</li>
|
<li>{t<string>('landing.features.list.ads')}</li>
|
||||||
<li>{t('landing.features.list.tracking')}</li>
|
<li>{t<string>('landing.features.list.tracking')}</li>
|
||||||
<li>{t('landing.features.list.languages')}</li>
|
<li>{t<string>('landing.features.list.languages')}</li>
|
||||||
<li>{t('landing.features.list.import')}</li>
|
<li>{t<string>('landing.features.list.import')}</li>
|
||||||
<li>{t('landing.features.list.export')}</li>
|
<li>{t<string>('landing.features.list.export')}</li>
|
||||||
<li>
|
<li>
|
||||||
<Trans t={t} i18nKey="landing.features.list.more">
|
<Trans t={t} i18nKey="landing.features.list.more">
|
||||||
And a lot of exciting features,
|
And a lot of exciting features,
|
||||||
@ -108,7 +112,7 @@ const Home: NextPage = () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<h6>{t('landing.screenshots.heading')}</h6>
|
<h6>{t<string>('landing.screenshots.heading')}</h6>
|
||||||
|
|
||||||
<div className={styles.screenshots}>
|
<div className={styles.screenshots}>
|
||||||
{screenshots.map(({ src, alt }) => (
|
{screenshots.map(({ src, alt }) => (
|
||||||
@ -120,7 +124,7 @@ const Home: NextPage = () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<h6>{t('landing.testimonials.heading')}</h6>
|
<h6>{t<string>('landing.testimonials.heading')}</h6>
|
||||||
|
|
||||||
<p className="my-3">
|
<p className="my-3">
|
||||||
<Trans t={t} i18nKey="landing.testimonials.body">
|
<Trans t={t} i18nKey="landing.testimonials.body">
|
||||||
@ -145,37 +149,37 @@ const Home: NextPage = () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<h6>{t('landing.links.heading')}</h6>
|
<h6>{t<string>('landing.links.heading')}</h6>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Link href="/meta/privacy" passHref>
|
<Link href="/meta/privacy" passHref>
|
||||||
<Button variant="text" startIcon={<LinkIcon />}>
|
<Button variant="text" startIcon={<LinkIcon />}>
|
||||||
{t('landing.links.links.privacy')}
|
{t<string>('landing.links.links.privacy')}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link href="/meta/service" passHref>
|
<Link href="/meta/service" passHref>
|
||||||
<Button variant="text" startIcon={<LinkIcon />}>
|
<Button variant="text" startIcon={<LinkIcon />}>
|
||||||
{t('landing.links.links.service')}
|
{t<string>('landing.links.links.service')}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<a href={GITHUB_URL} target="_blank" rel="noreferrer">
|
<a href={GITHUB_URL} target="_blank" rel="noreferrer">
|
||||||
<Button variant="text" startIcon={<LinkIcon />}>
|
<Button variant="text" startIcon={<LinkIcon />}>
|
||||||
{t('landing.links.links.github')}
|
{t<string>('landing.links.links.github')}
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href={DONATION_URL} target="_blank" rel="noreferrer">
|
<a href={DONATION_URL} target="_blank" rel="noreferrer">
|
||||||
<Button variant="text" startIcon={<LinkIcon />}>
|
<Button variant="text" startIcon={<LinkIcon />}>
|
||||||
{t('landing.links.links.donate')}
|
{t<string>('landing.links.links.donate')}
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className={styles.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} />
|
<Image src="/images/sponsors/digitalocean.svg" alt="Powered By DigitalOcean" width={200} height={40} />
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
@ -187,7 +191,11 @@ const Home: NextPage = () => {
|
|||||||
<div>v{process.env.appVersion}</div>
|
<div>v{process.env.appVersion}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LanguageSwitcher />
|
<div className={styles.actions}>
|
||||||
|
<IconButton onClick={handleToggle}>{theme === 'dark' ? <DarkMode /> : <LightMode />}</IconButton>
|
||||||
|
|
||||||
|
<LanguageSwitcher />
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import get from 'lodash/get';
|
|||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
import { GetServerSideProps, NextPage } from 'next';
|
import { GetServerSideProps, NextPage } from 'next';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@ -35,6 +36,8 @@ export const getServerSideProps: GetServerSideProps<Props> = async ({ query, loc
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Preview: NextPage<Props> = ({ shortId }) => {
|
const Preview: NextPage<Props> = ({ shortId }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const { data: resume } = useQuery<Resume>(`resume/${shortId}`, () => fetchResumeByShortId({ shortId }), {
|
const { data: resume } = useQuery<Resume>(`resume/${shortId}`, () => fetchResumeByShortId({ shortId }), {
|
||||||
@ -52,6 +55,14 @@ const Preview: NextPage<Props> = ({ shortId }) => {
|
|||||||
if (resume) dispatch(setResume(resume));
|
if (resume) dispatch(setResume(resume));
|
||||||
}, [resume, dispatch]);
|
}, [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;
|
if (!resume || isEmpty(resume)) return null;
|
||||||
|
|
||||||
const layout: string[][][] = get(resume, 'metadata.layout', []);
|
const layout: string[][][] = get(resume, 'metadata.layout', []);
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 265 KiB After Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 314 KiB After Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 91 KiB |