mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-10 04:22:27 +10:00
Compare commits
467 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 30fd283898 | |||
| 726ea7312b | |||
| f3a7180d4b | |||
| 0173ce32c3 | |||
| d4b6c16bf9 | |||
| c571f201d3 | |||
| e4ecf50ed4 | |||
| 5ee99cfdab | |||
| 72e610b50d | |||
| ba34787333 | |||
| e11b0e6224 | |||
| c78ee18e05 | |||
| 5f5b484243 | |||
| bcc451a6a1 | |||
| 55a7f6a556 | |||
| e9b6265c60 | |||
| 2e2f3271c9 | |||
| fa3e92d643 | |||
| 1f9b52eda6 | |||
| 7074b6fc76 | |||
| b4c4fb94f7 | |||
| 22bdb64fa9 | |||
| af02158d05 | |||
| 6a8db92fc4 | |||
| 6f219ef17e | |||
| 667e51abdc | |||
| 7b98277c32 | |||
| 77ed7ed8be | |||
| ce584d9326 | |||
| 5685352375 | |||
| 036b2917a6 | |||
| e972320722 | |||
| 4ac1e9db35 | |||
| 9fe4403b40 | |||
| 4f4084ab45 | |||
| 72227dc9ab | |||
| d44795a421 | |||
| e9584144e4 | |||
| bbedfa3b75 | |||
| 03f7d74096 | |||
| a62693d611 | |||
| 421f195e1e | |||
| b22dff523f | |||
| 58d0c6e315 | |||
| 36178cac22 | |||
| 376786fa25 | |||
| efceda1c55 | |||
| 047e317c51 | |||
| 36ad63adb9 | |||
| 45c88caf58 | |||
| ca11a9217a | |||
| fd6fbbba77 | |||
| e2fb83bda9 | |||
| 40567e8f61 | |||
| 64c899b159 | |||
| b267cc4097 | |||
| f4657b6592 | |||
| 6a2f512638 | |||
| 499005c21f | |||
| 0e18d3fc48 | |||
| 3b831c4eb4 | |||
| 40564944ef | |||
| fdbb6d2e5b | |||
| 398cd63082 | |||
| efd4af14e5 | |||
| 889697fc31 | |||
| 3aedf6618d | |||
| abf42e13af | |||
| 40bcbebadd | |||
| 364f2e6d49 | |||
| 7e5dfd75f9 | |||
| b94d10c614 | |||
| 8c40b417ec | |||
| 1f17dfe6ea | |||
| be6ea1a224 | |||
| 583e9effae | |||
| 619b2757c8 | |||
| 9e27eee029 | |||
| c2d3c611e1 | |||
| 735f589e54 | |||
| 1e3d6fbb77 | |||
| 3995e7159a | |||
| 6662acf0b0 | |||
| feb8abca95 | |||
| 75c83bd91d | |||
| f6d5897ed3 | |||
| ed356763a1 | |||
| 4847246d84 | |||
| a0ae6cb77e | |||
| 2aa2550be0 | |||
| df39913d49 | |||
| 2225505d48 | |||
| afe20e61ee | |||
| 794e9c6511 | |||
| e7e423bf29 | |||
| 2173297207 | |||
| b091cfa474 | |||
| 057bb3a414 | |||
| c1442c9acc | |||
| 977f1beafd | |||
| 39ee710e97 | |||
| 1d1841c8db | |||
| 3e44774ed4 | |||
| 9e2fa01896 | |||
| 7811f9840c | |||
| 34425c6200 | |||
| 46f9fc549a | |||
| 237abf359b | |||
| c5e8739009 | |||
| 0ea8040977 | |||
| 1f10e8efe3 | |||
| 8c2688670e | |||
| bc5d49b568 | |||
| 27ea84e720 | |||
| 0becb66bfd | |||
| 11f88492e9 | |||
| ae3e01466f | |||
| 5d04dd8a83 | |||
| 52c15a8151 | |||
| f6104e7051 | |||
| ed710f6fe5 | |||
| 7e6e239d7f | |||
| b4381a22f3 | |||
| ba6ca4d220 | |||
| 5486906b05 | |||
| 7348b295cb | |||
| 025762fdf6 | |||
| 96411cdb90 | |||
| 835f453384 | |||
| cc475ae1e9 | |||
| a5249ec646 | |||
| d0e3090421 | |||
| 14f68c8937 | |||
| 9c0c6076b3 | |||
| 36bf729161 | |||
| 3a430ad98c | |||
| 90a8610dd7 | |||
| d62ddab140 | |||
| ca0186bb67 | |||
| 88ac8389ea | |||
| 2f3864fff2 | |||
| 7878e52cc4 | |||
| 08b1967a4e | |||
| b870ca8297 | |||
| 1507c54671 | |||
| 0984ca4daf | |||
| 438798f8de | |||
| 27aadb8948 | |||
| 244a4118cf | |||
| cde320ce46 | |||
| 4772df7618 | |||
| ecb95b35f3 | |||
| 24b09af563 | |||
| 9471fb4169 | |||
| 2f296d6f08 | |||
| cd3d3caa15 | |||
| 440eefe46e | |||
| 5787e2badb | |||
| c235e5ab16 | |||
| f584c70f27 | |||
| 3aa0279519 | |||
| 6830aec2f9 | |||
| 9a3d7af325 | |||
| 52f7e8557f | |||
| 12c17d1c7c | |||
| 67918187a1 | |||
| dcbc0c2b45 | |||
| 26b6a741c2 | |||
| d7064129e8 | |||
| 279dd36a13 | |||
| 49e47b28de | |||
| a782343e0a | |||
| 1e7821a46d | |||
| f7bea5a218 | |||
| 05fa4f3192 | |||
| ed357f0ebc | |||
| 1774832a58 | |||
| 2837befd52 | |||
| 38d866c0c2 | |||
| 87c7acf4f1 | |||
| 1bd68118ce | |||
| 5c34b28c80 | |||
| c550183720 | |||
| 3605579b1b | |||
| fa2e28688f | |||
| 20f1031e28 | |||
| 292cb6d0ed | |||
| 45f2dc1cfc | |||
| e319dd3e3d | |||
| 9678f7a6e5 | |||
| 0cca4e21fb | |||
| f6758f191d | |||
| 983662f877 | |||
| c7fc28a5c5 | |||
| 1f7c33e805 | |||
| 437cc331a8 | |||
| aef51375b8 | |||
| bdd65968e5 | |||
| 061a789c18 | |||
| 68507d0501 | |||
| 1e28c5adfa | |||
| 3b09550ebd | |||
| 16aef9cbec | |||
| b24da90ba7 | |||
| 2aa7dbd3ad | |||
| 9f8f2c4b8b | |||
| 5331ecccc1 | |||
| f2ec86940c | |||
| cd74e707ba | |||
| ff101dbfac | |||
| 5024c19f87 | |||
| c9850b5815 | |||
| 6fe4e7d7e1 | |||
| a5b8b91e82 | |||
| cc7095adc3 | |||
| e2703f55aa | |||
| 8c5849c988 | |||
| 5322ab2420 | |||
| b84e6bcfb1 | |||
| a4ab0174c7 | |||
| 7e93b5a757 | |||
| 044820fa71 | |||
| 322178e8a4 | |||
| 6358fbad30 | |||
| d342c0a9af | |||
| 63084eebb4 | |||
| 3b4ea00db8 | |||
| c8f7bffe7e | |||
| 3ff56f89d9 | |||
| 7fb9f27837 | |||
| c9685d4ce7 | |||
| 4dc987e27d | |||
| f7af06ae9a | |||
| a5c337faa3 | |||
| fc4704f0a6 | |||
| d968334ada | |||
| fea6d23178 | |||
| 3fefc95572 | |||
| b07e7d1213 | |||
| d47b8bfb03 | |||
| 5bf7fbdae1 | |||
| fca766b382 | |||
| feadfb1b67 | |||
| e69000f221 | |||
| 6b4a54465a | |||
| 878659999f | |||
| 1868c47e30 | |||
| 51442efc23 | |||
| 556e962ec5 | |||
| b5ce67f863 | |||
| c3ce89dc3a | |||
| e87930c758 | |||
| 815a693e58 | |||
| 8287fcae96 | |||
| cd7fe6c404 | |||
| d47d5dd819 | |||
| 1919d79e43 | |||
| ab08cd9e34 | |||
| 2522bdd0a2 | |||
| f9b6aefffe | |||
| 2ba6658a0b | |||
| dbc46f27a3 | |||
| f21e1caed1 | |||
| 4ffe2a6330 | |||
| 1bc0438872 | |||
| 57fb9fdaea | |||
| 58ce641f18 | |||
| 5f4e7802e4 | |||
| 42d3109ae1 | |||
| f7ca7b97fa | |||
| f5d8a54134 | |||
| eaec14dc62 | |||
| c93b3264cd | |||
| bf41aa9c6c | |||
| 8af6bfd5ae | |||
| ab08c10874 | |||
| 9af9a0284e | |||
| 716a05032d | |||
| 43e43e7d76 | |||
| c91af3668d | |||
| 52f41f0b3b | |||
| 3b709d606b | |||
| 2e5fafac62 | |||
| ea2aee2d25 | |||
| e36fbb5f64 | |||
| 5221ef707b | |||
| f0df806f01 | |||
| 9d01d6a833 | |||
| 1914ebb9ae | |||
| 686dba90c9 | |||
| 95dc3bf571 | |||
| 1c8fdbf848 | |||
| d8357c9959 | |||
| 90e994377b | |||
| 82c6ee6d5d | |||
| 7b615e73c3 | |||
| 268e4a87fe | |||
| 73f8eb84c9 | |||
| a31ef89996 | |||
| d6bca7ebab | |||
| e0a42fd928 | |||
| deb4e0a0de | |||
| a687062866 | |||
| 700439c8a8 | |||
| fb09283e53 | |||
| 88ac365e03 | |||
| aec78cf875 | |||
| 77c587681b | |||
| 7ac8b906d9 | |||
| e9a5f86a6a | |||
| 7238a3b50e | |||
| ebe13fa82e | |||
| 6ee290a625 | |||
| 69f2b7070f | |||
| 11bea1c7c4 | |||
| 68a1dc65c1 | |||
| 4b1ce539d5 | |||
| a6fbb8191d | |||
| 552ff281b8 | |||
| 54fad2f6d8 | |||
| 78edcd7d0e | |||
| a8034b21d5 | |||
| f0e95905d2 | |||
| 69a5276614 | |||
| 2e62eea351 | |||
| 13d972b8f3 | |||
| 03cb198e95 | |||
| 67ee55b502 | |||
| b5998d7f3a | |||
| f71cf99b77 | |||
| a2092a6a39 | |||
| 43c09666a0 | |||
| 0da23f95fd | |||
| e8f44e2142 | |||
| fbb237e982 | |||
| 7f7c1d7b87 | |||
| be0b7f20f9 | |||
| 0672988fff | |||
| 75dad60cb5 | |||
| 0140e3fce0 | |||
| 42d0e14b98 | |||
| 9a42d684fb | |||
| ab6ad65445 | |||
| b613764ccc | |||
| ac44d0489f | |||
| c57e6fbbb8 | |||
| 6c6da215c8 | |||
| be700c7629 | |||
| b697f73492 | |||
| 3106f94989 | |||
| 50f41f73d5 | |||
| 83e3f59e68 | |||
| 056c61e985 | |||
| d1a1b68302 | |||
| 6bd7b9a50f | |||
| e6967aab88 | |||
| 47e96803e3 | |||
| f9ef4d0a64 | |||
| c4b4e6013f | |||
| 24bbc46c32 | |||
| 85bc9ef124 | |||
| 33755a8573 | |||
| ab45321889 | |||
| 940b310f64 | |||
| 8026241b6c | |||
| 89b35392bd | |||
| 62eb239ec4 | |||
| 7fdf8c1f0c | |||
| 538697238a | |||
| 7bc4a998fe | |||
| e33df485ab | |||
| 36ae54fe17 | |||
| 50958fd6df | |||
| e9e595f0d0 | |||
| 43ddfba777 | |||
| 78a32961d7 | |||
| 9b1f3eda05 | |||
| 1154621e5c | |||
| e7aeee77a7 | |||
| fab3988a36 | |||
| 354cad88d3 | |||
| 876f930f30 | |||
| 5b3ea46f0f | |||
| 37a2563c11 | |||
| cb977a146b | |||
| 72b2551b6d | |||
| c94633e616 | |||
| 7fee2d670f | |||
| 837b06eb38 | |||
| 2b8860b21c | |||
| 3a7b98d30e | |||
| 284a39aa77 | |||
| c14c9955dd | |||
| 4de787157a | |||
| 6bc6425a01 | |||
| 6051305908 | |||
| 5e13253454 | |||
| c1fd2b40e3 | |||
| fccf7a7b56 | |||
| 5098b094db | |||
| 7c1eb74aca | |||
| 7f9ede8ff0 | |||
| 172b23e429 | |||
| f287ca6183 | |||
| 7548e36aaf | |||
| 2f754616b4 | |||
| dc51f6f9b2 | |||
| 0cdac1d657 | |||
| c3cfe8ae7b | |||
| 08435c173b | |||
| 62cc2d6eac | |||
| 5a6f6e2b6c | |||
| 2dfa8c04a1 | |||
| 63e3f94d2d | |||
| 7f45a8cb7f | |||
| 4377ebb811 | |||
| ed3af6975b | |||
| 7904905a8b | |||
| bd2e6d2bf2 | |||
| ae4e9e688e | |||
| 78c45b7019 | |||
| 9a2fbbec4e | |||
| 93d751d9be | |||
| 9926ed2262 | |||
| 62220d20e7 | |||
| 2f6108cd29 | |||
| 7aeed37869 | |||
| ed99659b7b | |||
| 5e33d00910 | |||
| c00d0341e6 | |||
| 511ae036c2 | |||
| 1642ec9ba2 | |||
| 1115bc2b69 | |||
| 27e5c7811c | |||
| 3b739f0bb7 | |||
| d937ba2056 | |||
| de110d7de1 | |||
| b03229b5e0 | |||
| 280fc73c7b | |||
| 677ad2a115 | |||
| f394b26d18 | |||
| aab4e2e941 | |||
| f0f552a635 | |||
| 136e143e12 | |||
| 857e4b8670 | |||
| ff03d41d97 | |||
| 2bad37aaf3 | |||
| 3a40fbf78b | |||
| 49c638fb18 | |||
| 50e8d60773 | |||
| bf157a8d1a | |||
| c4f5955fcd | |||
| 86d33b0f21 | |||
| 56bca30639 | |||
| eed3b76959 | |||
| 615eb3ad5d | |||
| b505199319 | |||
| 91e55e642c | |||
| f549d8749a | |||
| f31123659e | |||
| 93633c9415 | |||
| 19b9fa4857 | |||
| a5c84214f9 | |||
| 65bb8b5ceb | |||
| 06a11a1f2a | |||
| 53eedc8500 | |||
| fc0b69796f |
@ -2,9 +2,9 @@
|
||||
/app
|
||||
|
||||
# Build Artifacts
|
||||
dist
|
||||
.next
|
||||
.turbo
|
||||
/schema/dist
|
||||
/server/dist
|
||||
/client/.next
|
||||
|
||||
# IDEs
|
||||
.vscode
|
||||
@ -19,7 +19,7 @@ CHANGELOG.md
|
||||
CODE_OF_CONDUCT.md
|
||||
|
||||
# Project Dependencies
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
|
||||
11
.env.example
11
.env.example
@ -1,7 +1,3 @@
|
||||
# Turbo Cache (Optional)
|
||||
TURBO_TEAM=
|
||||
TURBO_TOKEN=
|
||||
|
||||
# Server + Client
|
||||
TZ=UTC
|
||||
PUBLIC_URL=http://localhost:3000
|
||||
@ -15,7 +11,7 @@ POSTGRES_PASSWORD=postgres
|
||||
|
||||
# Server
|
||||
SECRET_KEY=
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_HOST=localhost
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_SSL_CERT=
|
||||
JWT_SECRET=
|
||||
@ -34,6 +30,7 @@ STORAGE_ENDPOINT=
|
||||
STORAGE_URL_PREFIX=
|
||||
STORAGE_ACCESS_KEY=
|
||||
STORAGE_SECRET_KEY=
|
||||
PDF_DELETION_TIME=345600000
|
||||
|
||||
# Flags (Client)
|
||||
PUBLIC_FLAG_DISABLE_SIGNUPS=false
|
||||
# Client
|
||||
PUBLIC_FLAG_DISABLE_SIGNUPS=false
|
||||
|
||||
@ -1,12 +1,23 @@
|
||||
{
|
||||
"ignorePatterns": ["/app"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": ["plugin:@typescript-eslint/recommended"],
|
||||
"plugins": ["@typescript-eslint/eslint-plugin", "simple-import-sort"],
|
||||
"plugins": ["@typescript-eslint/eslint-plugin", "unused-imports", "simple-import-sort"],
|
||||
"rules": {
|
||||
// ESLint
|
||||
"no-unused-vars": "off",
|
||||
|
||||
// Unused Imports
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "none",
|
||||
"varsIgnorePattern": "^_",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
|
||||
// Simple Import Sort
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
@ -14,6 +25,7 @@
|
||||
// TypeScript ESLint
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off"
|
||||
|
||||
13
.github/workflows/digitalocean-deploy.yml
vendored
13
.github/workflows/digitalocean-deploy.yml
vendored
@ -8,14 +8,21 @@ on:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
on-success:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: Install DigitalOcean CLI
|
||||
uses: digitalocean/action-doctl@v2.1.1
|
||||
uses: digitalocean/action-doctl@v2.3.0
|
||||
with:
|
||||
token: ${{ secrets.DIGITALOCEAN_TOKEN }}
|
||||
|
||||
- name: Create Deployment with Latest Version
|
||||
run: doctl apps create-deployment ${{ secrets.DIGITALOCEAN_APP_ID }} --wait --force-rebuild
|
||||
|
||||
on-failure:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
|
||||
steps:
|
||||
- name: Abruptly end the worklfow
|
||||
run: exit 1
|
||||
|
||||
103
.github/workflows/docker-build-push.yml
vendored
103
.github/workflows/docker-build-push.yml
vendored
@ -1,112 +1,61 @@
|
||||
name: Build and Push Docker Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
client:
|
||||
name: Client
|
||||
build_matrix:
|
||||
name: Build and Push Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
strategy:
|
||||
matrix:
|
||||
image: [client, server]
|
||||
arch: [linux/amd64, linux/arm64]
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
uses: actions/checkout@v3.5.2
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
name: App Version
|
||||
uses: martinbeentjes/npm-get-version-action@v1.3.1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.0.0
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
with:
|
||||
platforms: ${{ matrix.arch }}
|
||||
|
||||
- id: buildx
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.0.0
|
||||
with:
|
||||
install: true
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.0.0
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.0.0
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: $GITHUB_REPOSITORY_OWNER
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Build and Push Client Image
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
- name: Build and Push Docker Image
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: client/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: ${{ matrix.arch }}
|
||||
file: ${{ matrix.image }}/Dockerfile
|
||||
build-args: |
|
||||
TURBO_TOKEN=${{ secrets.TURBO_TOKEN }}
|
||||
tags: |
|
||||
amruthpillai/reactive-resume:client-latest
|
||||
amruthpillai/reactive-resume:client-${{ steps.version.outputs.tag }}
|
||||
ghcr.io/amruthpillai/reactive-resume:client-latest
|
||||
ghcr.io/amruthpillai/reactive-resume:client-${{ steps.version.outputs.tag }}
|
||||
|
||||
server:
|
||||
name: Server
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.0.0
|
||||
|
||||
- id: buildx
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.0.0
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: $GITHUB_REPOSITORY_OWNER
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Build and Push Server Image
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: server/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
amruthpillai/reactive-resume:server-latest
|
||||
amruthpillai/reactive-resume:server-${{ steps.version.outputs.tag }}
|
||||
ghcr.io/amruthpillai/reactive-resume:server-latest
|
||||
ghcr.io/amruthpillai/reactive-resume:server-${{ steps.version.outputs.tag }}
|
||||
amruthpillai/reactive-resume:${{ matrix.image }}-latest
|
||||
amruthpillai/reactive-resume:${{ matrix.image }}-${{ steps.version.outputs.current-version }}
|
||||
ghcr.io/amruthpillai/reactive-resume:${{ matrix.image }}-latest
|
||||
ghcr.io/amruthpillai/reactive-resume:${{ matrix.image }}-${{ steps.version.outputs.current-version }}
|
||||
|
||||
84
.github/workflows/docker-build.yml
vendored
84
.github/workflows/docker-build.yml
vendored
@ -1,84 +0,0 @@
|
||||
name: Build Docker Image
|
||||
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
client:
|
||||
name: Client
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.0.0
|
||||
|
||||
- id: buildx
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.0.0
|
||||
with:
|
||||
install: true
|
||||
|
||||
- id: variables
|
||||
name: Get Short SHA
|
||||
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
||||
|
||||
- name: Build Client Image
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
file: client/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
amruthpillai/reactive-resume:client-latest
|
||||
amruthpillai/reactive-resume:client-${{ steps.variables.outputs.sha_short }}
|
||||
ghcr.io/amruthpillai/reactive-resume:client-latest
|
||||
ghcr.io/amruthpillai/reactive-resume:client-${{ steps.variables.outputs.sha_short }}
|
||||
|
||||
server:
|
||||
name: Server
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.0.0
|
||||
|
||||
- id: buildx
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.0.0
|
||||
with:
|
||||
install: true
|
||||
|
||||
- id: variables
|
||||
name: Get Short SHA
|
||||
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
||||
|
||||
- name: Build Server Image
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
file: server/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
amruthpillai/reactive-resume:server-latest
|
||||
amruthpillai/reactive-resume:server-${{ steps.variables.outputs.sha_short }}
|
||||
ghcr.io/amruthpillai/reactive-resume:server-latest
|
||||
ghcr.io/amruthpillai/reactive-resume:server-${{ steps.variables.outputs.sha_short }}
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,6 +1,8 @@
|
||||
# Environment Variables
|
||||
.env
|
||||
.env.*
|
||||
*.env
|
||||
!.env.gitpod
|
||||
!.env.example
|
||||
|
||||
# Project Dependencies
|
||||
@ -9,5 +11,8 @@ node_modules
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Turbo
|
||||
# Intellij
|
||||
.idea
|
||||
|
||||
# Turborepo
|
||||
.turbo
|
||||
41
.gitpod.yml
Normal file
41
.gitpod.yml
Normal file
@ -0,0 +1,41 @@
|
||||
tasks:
|
||||
- name: Run PostgreSQL Database
|
||||
command: docker run --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -d postgres
|
||||
|
||||
- name: Install Project Dependencies
|
||||
command: |
|
||||
pnpm install
|
||||
pnpm dlx playwright install --with-deps chromium
|
||||
gp sync-done deps
|
||||
|
||||
- name: Generate Environment Variables
|
||||
init: gp sync-await deps
|
||||
command: |
|
||||
if [ -f .env ]; then
|
||||
echo "Found .env in workspace, skipping generation"
|
||||
else
|
||||
pnpm generate-env
|
||||
fi
|
||||
gp sync-done env
|
||||
|
||||
- name: Build and Run Project
|
||||
init: gp sync-await env
|
||||
command: |
|
||||
pnpm build
|
||||
pnpm start
|
||||
|
||||
ports:
|
||||
# PostgreSQL
|
||||
- port: 5432
|
||||
onOpen: ignore
|
||||
visibility: private
|
||||
|
||||
# Client
|
||||
- port: 3100
|
||||
onOpen: ignore
|
||||
visibility: public
|
||||
|
||||
# Client
|
||||
- port: 3000
|
||||
onOpen: open-browser
|
||||
visibility: public
|
||||
19
README.md
19
README.md
@ -6,7 +6,8 @@
|
||||
[](https://github.com/AmruthPillai/Reactive-Resume/blob/main/LICENSE)
|
||||
[](https://translate.rxresu.me)
|
||||
[](https://hub.docker.com/r/amruthpillai/reactive-resume)
|
||||

|
||||

|
||||
[](https://gitpod.io/#https://github.com/AmruthPillai/Reactive-Resume)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2FAmruthPillai%2FReactive-Resume?ref=badge_shield)
|
||||
|
||||
## [Go to App](https://rxresu.me) | [Docs](https://docs.rxresu.me)
|
||||
@ -18,6 +19,7 @@ You have complete control over what goes into your resume, how it looks, what co
|
||||
## Table of Contents
|
||||
|
||||
- [Reactive Resume](#reactive-resume)
|
||||
- [Go to App | Docs](#go-to-app--docs)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Features](#features)
|
||||
- [Languages](#languages)
|
||||
@ -26,6 +28,8 @@ You have complete control over what goes into your resume, how it looks, what co
|
||||
- [Contributing](#contributing)
|
||||
- [Report Bugs and Feature Requests](#report-bugs-and-feature-requests)
|
||||
- [Donations](#donations)
|
||||
- [GitHub Sponsor](#github-sponsor)
|
||||
- [PayPal](#paypal)
|
||||
- [Infrastructure](#infrastructure)
|
||||
- [Contributors Wall](#contributors-wall)
|
||||
- [License](#license)
|
||||
@ -52,6 +56,7 @@ You have complete control over what goes into your resume, how it looks, what co
|
||||
|
||||
## Languages
|
||||
|
||||
- Amharic (አማርኛ)
|
||||
- Arabic (اَلْعَرَبِيَّةُ)
|
||||
- Bengali (বাংলা)
|
||||
- Bulgarian (български)
|
||||
@ -89,7 +94,7 @@ You have complete control over what goes into your resume, how it looks, what co
|
||||
- Swedish (Svenska)
|
||||
- Tamil (தமிழ்)
|
||||
- Turkish (Türkçe)
|
||||
- Ukranian (Українська мова)
|
||||
- Ukrainian (Українська мова)
|
||||
- Vietnamese (Tiếng Việt)
|
||||
|
||||
Help by [translating Reactive Resume](https://translate.rxresu.me) to your language!
|
||||
@ -100,7 +105,11 @@ The docs include an extensive [Tutorial](https://docs.rxresu.me/tutorial) sectio
|
||||
|
||||
## Build from Source
|
||||
|
||||
For extensive information on how to build the app on your local machine, head over to the docs's [Source Code](https://docs.rxresu.me/source-code) section.
|
||||
[](https://gitpod.io/#https://github.com/AmruthPillai/Reactive-Resume)
|
||||
|
||||
Initially building the image and project on Gitpod will take at least ~10 minutes, so please be patient on first launch.
|
||||
|
||||
For extensive information on how to build the app on your local machine, head over to the docs [Source Code](https://docs.rxresu.me/source-code) section.
|
||||
|
||||
## Contributing
|
||||
|
||||
@ -125,6 +134,10 @@ Reactive Resume would be nothing without the folks who supported me and kept the
|
||||
### [GitHub Sponsor](https://github.com/sponsors/AmruthPillai)
|
||||
### [PayPal](https://paypal.me/RajaRajanA)
|
||||
|
||||
## GitHub Star History
|
||||
|
||||
[](https://star-history.com/#AmruthPillai/Reactive-Resume&Date)
|
||||
|
||||
## Infrastructure
|
||||
|
||||
- [Next.js](https://nextjs.org/), frontend
|
||||
|
||||
@ -12,8 +12,8 @@ android {
|
||||
targetSdk 32
|
||||
versionCode 3
|
||||
versionName "1.0"
|
||||
resConfigs 'en'
|
||||
|
||||
resConfigs "en"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@ -38,6 +38,7 @@ android {
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
namespace 'me.rxresu.app'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="me.rxresu.app">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
|
||||
@ -6,7 +6,6 @@ import android.webkit.WebView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var webView: WebView
|
||||
|
||||
private var url = "https://rxresu.me"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
plugins {
|
||||
id 'com.android.application' version '7.1.2' apply false
|
||||
id 'com.android.library' version '7.1.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
|
||||
id 'com.android.application' version '7.4.2' apply false
|
||||
id 'com.android.library' version '7.4.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.8.21' apply false
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
|
||||
2
app/gradle/wrapper/gradle-wrapper.properties
vendored
2
app/gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
#Wed Mar 09 21:34:49 CET 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
2
client/.gitignore
vendored
2
client/.gitignore
vendored
@ -39,4 +39,4 @@ yarn-error.log*
|
||||
__ENV.js
|
||||
|
||||
# next-sitemap
|
||||
sitemap*.xml
|
||||
sitemap*.xml
|
||||
|
||||
@ -2,12 +2,12 @@ FROM node:lts-alpine AS base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache g++ git curl make python3 \
|
||||
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
RUN apk add --no-cache g++ git make curl python3 libc6-compat \
|
||||
&& corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
FROM base as dependencies
|
||||
|
||||
COPY package.json pnpm-*.yaml turbo.json ./
|
||||
COPY package.json pnpm-*.yaml ./
|
||||
COPY ./schema/package.json ./schema/package.json
|
||||
COPY ./client/package.json ./client/package.json
|
||||
|
||||
@ -21,17 +21,14 @@ COPY --from=dependencies /app/node_modules ./node_modules
|
||||
COPY --from=dependencies /app/schema/node_modules ./schema/node_modules
|
||||
COPY --from=dependencies /app/client/node_modules ./client/node_modules
|
||||
|
||||
ARG TURBO_TEAM
|
||||
ARG TURBO_TOKEN
|
||||
ENV TURBO_TOKEN=$TURBO_TOKEN
|
||||
|
||||
ENV TURBO_TEAM $TURBO_TEAM
|
||||
ENV TURBO_TOKEN $TURBO_TOKEN
|
||||
|
||||
RUN pnpm run build --filter client
|
||||
RUN pnpm exec turbo --filter client build
|
||||
|
||||
FROM base as production
|
||||
|
||||
COPY --from=builder /app/package.json /app/pnpm-*.yaml /app/turbo.json ./
|
||||
COPY --from=builder /app/package.json /app/pnpm-*.yaml ./
|
||||
COPY --from=builder /app/client/package.json ./client/package.json
|
||||
|
||||
RUN pnpm install --filter client --prod --frozen-lockfile --workspace-root
|
||||
@ -45,7 +42,4 @@ EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=20s --retries=3 --start-period=15s \
|
||||
CMD curl -fSs 127.0.0.1:3000 || exit 1
|
||||
|
||||
CMD [ "pnpm", "run", "start", "--filter", "client" ]
|
||||
CMD [ "pnpm", "run", "--filter", "client", "start" ]
|
||||
43
client/Dockerfile.standalone
Normal file
43
client/Dockerfile.standalone
Normal file
@ -0,0 +1,43 @@
|
||||
FROM node:18-alpine AS base
|
||||
|
||||
FROM base AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
||||
elif [ -f package-lock.json ]; then npm ci; \
|
||||
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN yarn global add pnpm && pnpm build
|
||||
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
|
||||
CMD ["node", "client/server.js"]
|
||||
@ -13,11 +13,12 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import { ButtonBase, Divider, Tooltip, useMediaQuery, useTheme } from '@mui/material';
|
||||
import clsx from 'clsx';
|
||||
import { get } from 'lodash';
|
||||
import dayjs from 'dayjs';
|
||||
import get from 'lodash/get';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation } from 'react-query';
|
||||
import { ReactZoomPanPinchRef } from 'react-zoom-pan-pinch';
|
||||
import { ReactZoomPanPinchHandlers } from 'react-zoom-pan-pinch';
|
||||
import { ActionCreators } from 'redux-undo';
|
||||
|
||||
import { ServerError } from '@/services/axios';
|
||||
@ -28,7 +29,7 @@ import getResumeUrl from '@/utils/getResumeUrl';
|
||||
|
||||
import styles from './ArtboardController.module.scss';
|
||||
|
||||
const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, centerView }) => {
|
||||
const ArtboardController: React.FC<ReactZoomPanPinchHandlers> = ({ zoomIn, zoomOut, centerView }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const theme = useTheme();
|
||||
@ -67,8 +68,9 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
||||
|
||||
const slug = get(resume, 'slug');
|
||||
const username = get(resume, 'user.username');
|
||||
const updatedAt = get(resume, 'updatedAt');
|
||||
|
||||
const url = await mutateAsync({ username, slug });
|
||||
const url = await mutateAsync({ username, slug, lastUpdated: dayjs(updatedAt).unix().toString() });
|
||||
|
||||
download(url);
|
||||
};
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
@apply h-full w-full #{!important};
|
||||
@apply h-full w-full overflow-visible #{!important};
|
||||
}
|
||||
|
||||
.artboard {
|
||||
|
||||
@ -20,7 +20,6 @@ import {
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { Resume } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import get from 'lodash/get';
|
||||
import { useRouter } from 'next/router';
|
||||
@ -28,6 +27,7 @@ import { useTranslation } from 'next-i18next';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation } from 'react-query';
|
||||
import { Resume } from 'schema';
|
||||
|
||||
import { RESUMES_QUERY } from '@/constants/index';
|
||||
import { ServerError } from '@/services/axios';
|
||||
@ -53,13 +53,12 @@ const Header = () => {
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const { mutateAsync: duplicateMutation } = useMutation<Resume, ServerError, DuplicateResumeParams>(duplicateResume);
|
||||
|
||||
const { mutateAsync: deleteMutation } = useMutation<void, ServerError, DeleteResumeParams>(deleteResume);
|
||||
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const { left, right } = useAppSelector((state) => state.build.sidebar);
|
||||
|
||||
const { mutateAsync: deleteMutation } = useMutation<void, ServerError, DeleteResumeParams>(deleteResume);
|
||||
const { mutateAsync: duplicateMutation } = useMutation<Resume, ServerError, DuplicateResumeParams>(duplicateResume);
|
||||
|
||||
const name = useMemo(() => get(resume, 'name'), [resume]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
}
|
||||
|
||||
&.break::after {
|
||||
content: 'A4 Page Break';
|
||||
content: 'Page Break';
|
||||
top: calc(297mm - 19px);
|
||||
|
||||
@apply absolute w-full border-b border-dashed border-neutral-800/75;
|
||||
@ -28,10 +28,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.markdown {
|
||||
ul {
|
||||
padding-left: 1.5em;
|
||||
text-indent: -1.5em;
|
||||
&.format-letter {
|
||||
width: 216mm;
|
||||
min-height: 279mm;
|
||||
|
||||
&.break::after {
|
||||
top: calc(279mm - 19px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { CustomCSS, Theme, Typography } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useMemo } from 'react';
|
||||
import { CustomCSS, PageConfig, ThemeConfig, Typography } from 'schema';
|
||||
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
import templateMap from '@/templates/templateMap';
|
||||
@ -23,22 +23,24 @@ const Page: React.FC<Props> = ({ page, showPageNumbers = false }) => {
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const breakLine: boolean = useAppSelector((state) => state.build.page.breakLine);
|
||||
|
||||
const theme: Theme = get(resume, 'metadata.theme');
|
||||
const theme: ThemeConfig = get(resume, 'metadata.theme');
|
||||
const customCSS: CustomCSS = get(resume, 'metadata.css');
|
||||
const template: string = get(resume, 'metadata.template');
|
||||
const typography: Typography = get(resume, 'metadata.typography');
|
||||
const pageConfig: PageConfig = get(resume, 'metadata.page', {} as PageConfig);
|
||||
|
||||
const themeCSS = useMemo(() => !isEmpty(theme) && generateThemeStyles(theme), [theme]);
|
||||
const typographyCSS = useMemo(() => !isEmpty(typography) && generateTypographyStyles(typography), [typography]);
|
||||
const TemplatePage: React.FC<PageProps> | null = useMemo(() => templateMap[template].component, [template]);
|
||||
|
||||
return (
|
||||
<div data-page={page + 1} className={styles.container}>
|
||||
<div className={styles.container} data-page={page + 1} data-format={pageConfig?.format || 'A4'}>
|
||||
<div
|
||||
className={clsx({
|
||||
reset: true,
|
||||
[styles.page]: true,
|
||||
[styles.break]: breakLine,
|
||||
[styles['format-letter']]: pageConfig?.format === 'Letter',
|
||||
[css(themeCSS)]: true,
|
||||
[css(typographyCSS)]: true,
|
||||
[css(customCSS.value)]: customCSS.visible,
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
import { Add, Star } from '@mui/icons-material';
|
||||
import { Button, Divider, IconButton, SwipeableDrawer, Tooltip, useMediaQuery, useTheme } from '@mui/material';
|
||||
import { Section as SectionRecord } from '@reactive-resume/schema';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import get from 'lodash/get';
|
||||
import Link from 'next/link';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useMemo } from 'react';
|
||||
import React, { ReactComponentElement, useMemo } from 'react';
|
||||
import { Section as SectionRecord } from 'schema';
|
||||
import { validate } from 'uuid';
|
||||
|
||||
import Logo from '@/components/shared/Logo';
|
||||
import { getCustomSections, left } from '@/config/sections';
|
||||
import { getCustomSections, getSectionsByType, left } from '@/config/sections';
|
||||
import { setSidebarState } from '@/store/build/buildSlice';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { addSection } from '@/store/resume/resumeSlice';
|
||||
@ -26,6 +27,7 @@ const LeftSidebar = () => {
|
||||
const isDesktop = useMediaQuery(theme.breakpoints.up('lg'));
|
||||
|
||||
const sections = useAppSelector((state) => state.resume.present.sections);
|
||||
|
||||
const { open } = useAppSelector((state) => state.build.sidebar.left);
|
||||
|
||||
const customSections = useMemo(() => getCustomSections(sections), [sections]);
|
||||
@ -52,7 +54,49 @@ const LeftSidebar = () => {
|
||||
items: [],
|
||||
};
|
||||
|
||||
dispatch(addSection({ value: newSection }));
|
||||
dispatch(addSection({ value: newSection, type: 'custom' }));
|
||||
};
|
||||
|
||||
const sectionsList = () => {
|
||||
const sectionsComponents: Array<ReactComponentElement<any>> = [];
|
||||
|
||||
for (const item of left) {
|
||||
const id = (item as any).id;
|
||||
const component = (item as any).component;
|
||||
const type = component.props.type;
|
||||
const addMore = !!component.props.addMore;
|
||||
|
||||
sectionsComponents.push(
|
||||
<section key={id} id={id}>
|
||||
{component}
|
||||
</section>
|
||||
);
|
||||
|
||||
if (addMore) {
|
||||
const additionalSections = getSectionsByType(sections, type);
|
||||
const elements = [];
|
||||
for (const element of additionalSections) {
|
||||
const newId = element.id;
|
||||
|
||||
const props = cloneDeep(component.props);
|
||||
props.path = 'sections.' + newId;
|
||||
props.name = element.name;
|
||||
props.isDeletable = true;
|
||||
props.addMore = false;
|
||||
props.isDuplicated = true;
|
||||
const newComponent = React.cloneElement(component, props);
|
||||
|
||||
elements.push(
|
||||
<section key={newId} id={`section-${newId}`}>
|
||||
{newComponent}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
sectionsComponents.push(...elements);
|
||||
}
|
||||
}
|
||||
|
||||
return sectionsComponents;
|
||||
};
|
||||
|
||||
return (
|
||||
@ -65,12 +109,10 @@ const LeftSidebar = () => {
|
||||
variant={isDesktop ? 'persistent' : 'temporary'}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<nav className="overflow-y-scroll">
|
||||
<nav className="overflow-y-auto">
|
||||
<div>
|
||||
<Link href="/dashboard">
|
||||
<a className="inline-flex">
|
||||
<Logo size={40} />
|
||||
</a>
|
||||
<Logo size={40} />
|
||||
</Link>
|
||||
<Divider />
|
||||
</div>
|
||||
@ -81,15 +123,20 @@ const LeftSidebar = () => {
|
||||
arrow
|
||||
key={id}
|
||||
placement="right"
|
||||
title={get(sections, `${id}.name`, t<string>(`builder.leftSidebar.sections.${id}.heading`)) as string}
|
||||
title={t<string>(`builder.leftSidebar.sections.${id}.heading`, get(sections, `${id}.name`))}
|
||||
>
|
||||
<IconButton onClick={() => handleClick(id)}>{icon}</IconButton>
|
||||
</Tooltip>
|
||||
))}
|
||||
|
||||
{customSections.map(({ id }) => (
|
||||
<Tooltip key={id} title={get(sections, `${id}.name`, '') as string} placement="right" arrow>
|
||||
<IconButton onClick={() => handleClick(id)}>
|
||||
<Tooltip
|
||||
key={id}
|
||||
title={t<string>(`builder.leftSidebar.sections.${id}.heading`, get(sections, `${id}.name`))}
|
||||
placement="right"
|
||||
arrow
|
||||
>
|
||||
<IconButton onClick={() => id && handleClick(id)}>
|
||||
<Star />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@ -100,15 +147,11 @@ const LeftSidebar = () => {
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
{left.map(({ id, component }) => (
|
||||
<section key={id} id={id}>
|
||||
{component}
|
||||
</section>
|
||||
))}
|
||||
{sectionsList()}
|
||||
|
||||
{customSections.map(({ id }) => (
|
||||
<section key={id} id={`section-${id}`}>
|
||||
<Section path={`sections.${id}`} isEditable isHideable isDeletable />
|
||||
<Section path={`sections.${id}`} type="custom" isEditable isHideable isDeletable />
|
||||
</section>
|
||||
))}
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { Circle, Square, SquareRounded } from '@mui/icons-material';
|
||||
import { Checkbox, Divider, FormControlLabel, Slider, ToggleButton, ToggleButtonGroup } from '@mui/material';
|
||||
import { Photo, PhotoShape } from '@reactive-resume/schema';
|
||||
import get from 'lodash/get';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Photo, PhotoShape } from 'schema';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setResumeState } from '@/store/resume/resumeSlice';
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Avatar, IconButton, Skeleton, Tooltip } from '@mui/material';
|
||||
import { Photo, Resume } from '@reactive-resume/schema';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useRef } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation } from 'react-query';
|
||||
import { Photo, Resume } from 'schema';
|
||||
|
||||
import { ServerError } from '@/services/axios';
|
||||
import { deletePhoto, DeletePhotoParams, uploadPhoto, UploadPhotoParams } from '@/services/resume';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Add } from '@mui/icons-material';
|
||||
import { Button } from '@mui/material';
|
||||
import { ListItem } from '@reactive-resume/schema';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ListItem } from 'schema';
|
||||
|
||||
import Heading from '@/components/shared/Heading';
|
||||
import List from '@/components/shared/List';
|
||||
|
||||
@ -1,37 +1,43 @@
|
||||
import { Add } from '@mui/icons-material';
|
||||
import { Button } from '@mui/material';
|
||||
import { ListItem } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import get from 'lodash/get';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ListItem, Section as SectionRecord, SectionType } from 'schema';
|
||||
import { validate } from 'uuid';
|
||||
|
||||
import Heading from '@/components/shared/Heading';
|
||||
import List from '@/components/shared/List';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { ModalName, setModalState } from '@/store/modal/modalSlice';
|
||||
import { duplicateItem } from '@/store/resume/resumeSlice';
|
||||
import { duplicateItem, duplicateSection } from '@/store/resume/resumeSlice';
|
||||
|
||||
import SectionSettings from './SectionSettings';
|
||||
|
||||
type Props = {
|
||||
path: `sections.${string}`;
|
||||
type?: SectionType;
|
||||
name?: string;
|
||||
titleKey?: string;
|
||||
subtitleKey?: string;
|
||||
isEditable?: boolean;
|
||||
isHideable?: boolean;
|
||||
isDeletable?: boolean;
|
||||
addMore?: boolean;
|
||||
isDuplicated?: boolean;
|
||||
};
|
||||
|
||||
const Section: React.FC<Props> = ({
|
||||
path,
|
||||
name = 'Section Name',
|
||||
type = 'basic',
|
||||
titleKey = 'title',
|
||||
subtitleKey = 'subtitle',
|
||||
isEditable = false,
|
||||
isHideable = false,
|
||||
isDeletable = false,
|
||||
addMore = false,
|
||||
isDuplicated = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -41,22 +47,40 @@ const Section: React.FC<Props> = ({
|
||||
const visibility = useAppSelector<boolean>((state) => get(state.resume.present, `${path}.visible`, true));
|
||||
|
||||
const handleAdd = () => {
|
||||
const id = path.split('.')[1];
|
||||
const modal: ModalName = validate(id) ? 'builder.sections.custom' : `builder.${path}`;
|
||||
const modal: ModalName = `builder.sections.${type}`;
|
||||
|
||||
dispatch(setModalState({ modal, state: { open: true, payload: { path } } }));
|
||||
};
|
||||
|
||||
const handleEdit = (item: ListItem) => {
|
||||
const id = path.split('.')[1];
|
||||
const modal: ModalName = validate(id) ? 'builder.sections.custom' : `builder.${path}`;
|
||||
let modal: ModalName = validate(id) ? 'builder.sections.custom' : `builder.${path}`;
|
||||
|
||||
const payload = validate(id) ? { path, item } : { item };
|
||||
|
||||
if (isDuplicated) {
|
||||
modal = `builder.sections.${type}`;
|
||||
payload.path = path;
|
||||
}
|
||||
|
||||
dispatch(setModalState({ modal, state: { open: true, payload } }));
|
||||
};
|
||||
|
||||
const handleDuplicate = (item: ListItem) => dispatch(duplicateItem({ path: `${path}.items`, value: item }));
|
||||
|
||||
const handleDuplicateSection = () => {
|
||||
const newSection: SectionRecord = {
|
||||
name: `${heading}`,
|
||||
type: type,
|
||||
visible: true,
|
||||
columns: 2,
|
||||
items: [],
|
||||
isDuplicated: true,
|
||||
};
|
||||
|
||||
dispatch(duplicateSection({ value: newSection, type }));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading path={path} name={name} isEditable={isEditable} isHideable={isHideable} isDeletable={isDeletable} />
|
||||
@ -74,9 +98,21 @@ const Section: React.FC<Props> = ({
|
||||
<SectionSettings path={path} />
|
||||
|
||||
<Button variant="outlined" startIcon={<Add />} onClick={handleAdd}>
|
||||
{t<string>('builder.common.actions.add', { token: heading })}
|
||||
{t<string>('builder.common.actions.add', {
|
||||
token: t<string>(`builder.leftSidebar.${path}.heading`, { defaultValue: heading }),
|
||||
})}
|
||||
</Button>
|
||||
</footer>
|
||||
|
||||
{addMore ? (
|
||||
<div className="py-6 text-right">
|
||||
<Button fullWidth variant="outlined" startIcon={<Add />} onClick={handleDuplicateSection}>
|
||||
{t<string>('builder.common.actions.duplicate')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Divider, IconButton, SwipeableDrawer, Tooltip, useMediaQuery, useTheme } from '@mui/material';
|
||||
import { capitalize } from 'lodash';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import Avatar from '@/components/shared/Avatar';
|
||||
@ -43,7 +43,7 @@ const RightSidebar = () => {
|
||||
variant={isDesktop ? 'persistent' : 'temporary'}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<nav className="overflow-y-scroll">
|
||||
<nav className="overflow-y-auto">
|
||||
<div>
|
||||
<Avatar size={40} />
|
||||
<Divider />
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { CustomCSS as CustomCSSType } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import get from 'lodash/get';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React from 'react';
|
||||
import { CustomCSS as CustomCSSType } from 'schema';
|
||||
|
||||
import Heading from '@/components/shared/Heading';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
@ -17,7 +17,9 @@ const CustomCSS = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const customCSS: CustomCSSType = useAppSelector((state) => get(state.resume.present, 'metadata.css', {}));
|
||||
const customCSS: CustomCSSType = useAppSelector((state) =>
|
||||
get(state.resume.present, 'metadata.css', {} as CustomCSSType)
|
||||
);
|
||||
|
||||
const handleChange = (value: string | undefined) => {
|
||||
dispatch(setResumeState({ path: 'metadata.css.value', value }));
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { PictureAsPdf, Schema } from '@mui/icons-material';
|
||||
import { List, ListItem, ListItemButton, ListItemText } from '@mui/material';
|
||||
import dayjs from 'dayjs';
|
||||
import get from 'lodash/get';
|
||||
import pick from 'lodash/pick';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
@ -45,8 +46,9 @@ const Export = () => {
|
||||
|
||||
const slug = get(resume, 'slug');
|
||||
const username = get(resume, 'user.username');
|
||||
const updatedAt = get(resume, 'updatedAt');
|
||||
|
||||
const url = await mutateAsync({ username, slug });
|
||||
const url = await mutateAsync({ username, slug, lastUpdated: dayjs(updatedAt).unix().toString() });
|
||||
|
||||
download(url);
|
||||
};
|
||||
|
||||
@ -3,7 +3,7 @@ import { Button } from '@mui/material';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import Heading from '@/components/shared/Heading';
|
||||
import { DOCS_URL, DONATION_URL, GITHUB_ISSUES_URL, GITHUB_URL } from '@/constants/index';
|
||||
import { DOCS_URL, DONATION_URL, GITHUB_ISSUES_URL, GITHUB_URL, REDDIT_URL } from '@/constants/index';
|
||||
|
||||
import styles from './Links.module.scss';
|
||||
|
||||
@ -50,6 +50,12 @@ const Links = () => {
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<a href={REDDIT_URL} target="_blank" rel="noreferrer">
|
||||
<Button variant="text" startIcon={<Link />}>
|
||||
{t<string>('builder.rightSidebar.sections.links.reddit')}
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<a href={DOCS_URL} target="_blank" rel="noreferrer">
|
||||
<Button variant="text" startIcon={<Link />}>
|
||||
{t<string>('builder.rightSidebar.sections.links.docs')}
|
||||
|
||||
@ -10,13 +10,13 @@ import {
|
||||
Switch,
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import { DateConfig, Resume } from '@reactive-resume/schema';
|
||||
import dayjs from 'dayjs';
|
||||
import get from 'lodash/get';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { DateConfig, PageConfig, Resume } from 'schema';
|
||||
|
||||
import Heading from '@/components/shared/Heading';
|
||||
import ThemeSwitch from '@/components/shared/ThemeSwitch';
|
||||
@ -48,9 +48,10 @@ const Settings = () => {
|
||||
const slug: string = useMemo(() => get(resume, 'slug'), [resume]);
|
||||
const username: string = useMemo(() => get(resume, 'user.username'), [resume]);
|
||||
const dateConfig: DateConfig = useMemo(() => get(resume, 'metadata.date'), [resume]);
|
||||
const pageConfig: PageConfig | undefined = useMemo(() => get(resume, 'metadata.page'), [resume]);
|
||||
|
||||
const isDarkMode = useMemo(() => theme === 'dark', [theme]);
|
||||
const exampleString = useMemo(() => `Eg. ${dayjs().utc().format(dateConfig.format)}`, [dateConfig.format]);
|
||||
const exampleDateString = useMemo(() => `Eg. ${dayjs().format(dateConfig.format)}`, [dateConfig.format]);
|
||||
const themeString = useMemo(() => (isDarkMode ? 'Matte Black Everything' : 'As bright as your future'), [isDarkMode]);
|
||||
|
||||
const { mutateAsync: loadSampleDataMutation } = useMutation<Resume, ServerError, LoadSampleDataParams>(
|
||||
@ -60,6 +61,9 @@ const Settings = () => {
|
||||
|
||||
const handleSetTheme = (value: boolean) => dispatch(setTheme({ theme: value ? 'dark' : 'light' }));
|
||||
|
||||
const handleChangePageFormat = (value: PageConfig['format'] | null) =>
|
||||
dispatch(setResumeState({ path: 'metadata.page.format', value }));
|
||||
|
||||
const handleChangeDateFormat = (value: string | null) =>
|
||||
dispatch(setResumeState({ path: 'metadata.date.format', value }));
|
||||
|
||||
@ -94,7 +98,7 @@ const Settings = () => {
|
||||
<>
|
||||
<Heading path="metadata.settings" name={t<string>('builder.rightSidebar.sections.settings.heading')} />
|
||||
|
||||
<List sx={{ padding: 0 }}>
|
||||
<List disablePadding>
|
||||
{/* Global Settings */}
|
||||
<>
|
||||
<ListSubheader disableSticky className="rounded">
|
||||
@ -118,13 +122,13 @@ const Settings = () => {
|
||||
primary={t<string>('builder.rightSidebar.sections.settings.global.date.primary')}
|
||||
secondary={t<string>('builder.rightSidebar.sections.settings.global.date.secondary')}
|
||||
/>
|
||||
<Autocomplete<string, false, boolean, false>
|
||||
<Autocomplete<string, false, true, false>
|
||||
disableClearable
|
||||
className="my-2 w-full"
|
||||
options={dateFormatOptions}
|
||||
value={dateConfig.format}
|
||||
onChange={(_, value) => handleChangeDateFormat(value)}
|
||||
renderInput={(params) => <TextField {...params} helperText={exampleString} />}
|
||||
renderInput={(params) => <TextField {...params} helperText={exampleDateString} />}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
@ -134,7 +138,7 @@ const Settings = () => {
|
||||
primary={t<string>('builder.rightSidebar.sections.settings.global.language.primary')}
|
||||
secondary={t<string>('builder.rightSidebar.sections.settings.global.language.secondary')}
|
||||
/>
|
||||
<Autocomplete<Language, false, boolean, false>
|
||||
<Autocomplete<Language, false, true, false>
|
||||
disableClearable
|
||||
className="my-2 w-full"
|
||||
options={languages}
|
||||
@ -159,6 +163,23 @@ const Settings = () => {
|
||||
{t<string>('builder.rightSidebar.sections.settings.page.heading')}
|
||||
</ListSubheader>
|
||||
|
||||
<ListItem className="flex-col">
|
||||
<ListItemText
|
||||
className="w-full"
|
||||
primary={t<string>('builder.rightSidebar.sections.settings.page.format.primary')}
|
||||
secondary={t<string>('builder.rightSidebar.sections.settings.page.format.secondary')}
|
||||
/>
|
||||
<Autocomplete<PageConfig['format'], false, true, false>
|
||||
disableClearable
|
||||
defaultValue="A4"
|
||||
className="my-2 w-full"
|
||||
options={['A4', 'Letter']}
|
||||
value={pageConfig?.format || 'A4'}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
onChange={(_, value) => handleChangePageFormat(value)}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={t<string>('builder.rightSidebar.sections.settings.page.orientation.primary')}
|
||||
@ -191,7 +212,7 @@ const Settings = () => {
|
||||
{t<string>('builder.rightSidebar.sections.settings.resume.heading')}
|
||||
</ListSubheader>
|
||||
|
||||
<ListItem>
|
||||
<ListItem disableGutters>
|
||||
<ListItemButton onClick={handleLoadSampleData}>
|
||||
<ListItemIcon>
|
||||
<Anchor />
|
||||
@ -203,7 +224,7 @@ const Settings = () => {
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItem disableGutters>
|
||||
<ListItemButton onClick={handleResetResume}>
|
||||
<ListItemIcon>
|
||||
<DeleteForever />
|
||||
|
||||
@ -31,7 +31,14 @@ const Templates = () => {
|
||||
<div key={template.id} className={styles.template}>
|
||||
<div className={clsx(styles.preview, { [styles.selected]: template.id === currentTemplate })}>
|
||||
<ButtonBase onClick={() => handleChange(template)}>
|
||||
<Image src={template.preview} alt={template.name} className="rounded-sm" layout="fill" priority />
|
||||
<Image
|
||||
fill
|
||||
priority
|
||||
alt={template.name}
|
||||
src={template.preview}
|
||||
className="rounded-sm"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
/>
|
||||
</ButtonBase>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Theme as ThemeType } from '@reactive-resume/schema';
|
||||
import get from 'lodash/get';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ThemeConfig } from 'schema';
|
||||
|
||||
import ColorAvatar from '@/components/shared/ColorAvatar';
|
||||
import ColorPicker from '@/components/shared/ColorPicker';
|
||||
@ -16,7 +16,7 @@ const Theme = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { background, text, primary } = useAppSelector<ThemeType>((state) =>
|
||||
const { background, text, primary } = useAppSelector<ThemeConfig>((state) =>
|
||||
get(state.resume.present, 'metadata.theme')
|
||||
);
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { Autocomplete, Skeleton, Slider, TextField } from '@mui/material';
|
||||
import { Font, TypeCategory, TypeProperty, Typography as TypographyType } from '@reactive-resume/schema';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Font, TypeCategory, TypeProperty, Typography as TypographyType } from 'schema';
|
||||
|
||||
import Heading from '@/components/shared/Heading';
|
||||
import { FONTS_QUERY } from '@/constants/index';
|
||||
|
||||
@ -16,9 +16,7 @@ type Props = {
|
||||
const ResumeCard: React.FC<Props> = ({ modal, icon: Icon, title, subtitle }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleClick = () => {
|
||||
dispatch(setModalState({ modal, state: { open: true } }));
|
||||
};
|
||||
const handleClick = () => dispatch(setModalState({ modal, state: { open: true } }));
|
||||
|
||||
return (
|
||||
<section className={styles.resume}>
|
||||
|
||||
@ -7,7 +7,6 @@ import {
|
||||
OpenInNew,
|
||||
} from '@mui/icons-material';
|
||||
import { ButtonBase, ListItemIcon, ListItemText, Menu, MenuItem, Tooltip } from '@mui/material';
|
||||
import { Resume } from '@reactive-resume/schema';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
@ -15,6 +14,7 @@ import { useTranslation } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation } from 'react-query';
|
||||
import { Resume } from 'schema';
|
||||
|
||||
import { RESUMES_QUERY } from '@/constants/index';
|
||||
import { ServerError } from '@/services/axios';
|
||||
@ -115,9 +115,7 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
||||
}}
|
||||
>
|
||||
<ButtonBase className={styles.preview}>
|
||||
{resume.image ? (
|
||||
<Image src={resume.image} alt={resume.name} objectFit="cover" layout="fill" priority />
|
||||
) : null}
|
||||
{resume.image ? <Image src={resume.image} alt={resume.name} priority width={400} height={0} /> : null}
|
||||
</ButtonBase>
|
||||
</Link>
|
||||
|
||||
|
||||
@ -6,15 +6,17 @@ import { useState } from 'react';
|
||||
|
||||
import { logout } from '@/store/auth/authSlice';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setModalState } from '@/store/modal/modalSlice';
|
||||
import getGravatarUrl from '@/utils/getGravatarUrl';
|
||||
|
||||
import styles from './Avatar.module.scss';
|
||||
|
||||
type Props = {
|
||||
size?: number;
|
||||
interactive?: boolean;
|
||||
};
|
||||
|
||||
const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
||||
const Avatar: React.FC<Props> = ({ size = 64, interactive = true }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useTranslation();
|
||||
@ -34,6 +36,11 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleOpenProfile = () => {
|
||||
dispatch(setModalState({ modal: 'auth.profile', state: { open: true } }));
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
dispatch(logout());
|
||||
handleClose();
|
||||
@ -43,20 +50,20 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton onClick={handleOpen}>
|
||||
<IconButton onClick={handleOpen} disabled={!interactive}>
|
||||
<Image
|
||||
width={size}
|
||||
height={size}
|
||||
alt={user?.name}
|
||||
className={styles.avatar}
|
||||
src={getGravatarUrl(email, size)}
|
||||
alt={user?.name ?? 'User Avatar'}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<Menu anchorEl={anchorEl} onClose={handleClose} open={Boolean(anchorEl)}>
|
||||
<MenuItem>
|
||||
<MenuItem onClick={handleOpenProfile}>
|
||||
<div>
|
||||
<span className="text-xs opacity-50">{t<string>('common.avatar.menu.greeting')}</span>
|
||||
<span className="text-xs opacity-50">{t<string>('common.avatar.menu.greeting')},</span>
|
||||
<p>{user?.name}</p>
|
||||
</div>
|
||||
</MenuItem>
|
||||
|
||||
@ -62,7 +62,7 @@ const Heading: React.FC<Props> = ({
|
||||
{editMode ? (
|
||||
<TextField size="small" value={heading} className="w-3/4" onChange={handleChange} />
|
||||
) : (
|
||||
<h1>{heading}</h1>
|
||||
<h1>{t<string>(`builder.leftSidebar.${path}.heading`, { defaultValue: heading })}</h1>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { ListItem as ListItemType } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import get from 'lodash/get';
|
||||
import isArray from 'lodash/isArray';
|
||||
@ -8,6 +7,7 @@ import { useTranslation } from 'next-i18next';
|
||||
import { useCallback } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { ListItem as ListItemType } from 'schema';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { deleteItem, setResumeState } from '@/store/resume/resumeSlice';
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { DeleteOutline, DriveFileRenameOutline, FileCopy, MoreVert } from '@mui/icons-material';
|
||||
import { Divider, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Tooltip } from '@mui/material';
|
||||
import { ListItem as ListItemType } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import isFunction from 'lodash/isFunction';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { DropTargetMonitor, useDrag, useDrop, XYCoord } from 'react-dnd';
|
||||
import { ListItem as ListItemType } from 'schema';
|
||||
|
||||
import styles from './ListItem.module.scss';
|
||||
|
||||
|
||||
@ -4,8 +4,8 @@ type Props = {
|
||||
size?: 256 | 64 | 48 | 40 | 32;
|
||||
};
|
||||
|
||||
const Logo: React.FC<Props> = ({ size = 64 }) => {
|
||||
return <Image alt="Reactive Resume" src="/images/logos/logo.svg" className="rounded" width={size} height={size} />;
|
||||
};
|
||||
const Logo: React.FC<Props> = ({ size = 64 }) => (
|
||||
<Image alt="Reactive Resume" src="/images/logos/logo.svg" className="rounded" width={size} height={size} priority />
|
||||
);
|
||||
|
||||
export default Logo;
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import clsx from 'clsx';
|
||||
import { isEmpty } from 'lodash';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
|
||||
type Props = {
|
||||
children?: string;
|
||||
@ -12,7 +14,11 @@ const Markdown: React.FC<Props> = ({ className, children }) => {
|
||||
if (!children || isEmpty(children)) return null;
|
||||
|
||||
return (
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} className={clsx('markdown', className)}>
|
||||
<ReactMarkdown
|
||||
className={clsx('markdown', className)}
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import dayjs from 'dayjs';
|
||||
import { isEmpty } from 'lodash';
|
||||
import get from 'lodash/get';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
@ -58,12 +57,11 @@ const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, m
|
||||
<DatePicker
|
||||
openTo="year"
|
||||
label={label}
|
||||
value={value}
|
||||
views={['year', 'month', 'day']}
|
||||
renderInput={(params) => <TextField {...params} error={false} className={className} />}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && onChangeValue('');
|
||||
date && dayjs(date).utc().isValid() && onChangeValue(dayjs(date).utc().toISOString());
|
||||
value={dayjs(value)}
|
||||
slots={{ textField: (params) => <TextField {...params} error={false} className={className} /> }}
|
||||
onChange={(date: dayjs.Dayjs | null) => {
|
||||
if (!date) return onChangeValue('');
|
||||
if (dayjs(date).isValid()) return onChangeValue(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -2,23 +2,25 @@ export type Language = {
|
||||
code: string;
|
||||
name: string;
|
||||
localName?: string;
|
||||
isRTL?: boolean;
|
||||
};
|
||||
|
||||
export const languages: Language[] = [
|
||||
{ code: 'ar', name: 'Arabic', localName: 'اَلْعَرَبِيَّةُ' },
|
||||
{ code: 'am', name: 'Amharic', localName: 'አማርኛ' },
|
||||
{ code: 'ar', name: 'Arabic', localName: 'اَلْعَرَبِيَّةُ', isRTL: true },
|
||||
{ code: 'bg', name: 'Bulgarian', localName: 'български' },
|
||||
{ code: 'bn', name: 'Bengali', localName: 'বাংলা' },
|
||||
{ code: 'ca', name: 'Catalan', localName: 'Valencian' },
|
||||
{ code: 'cs', name: 'Czech', localName: 'čeština' },
|
||||
{ code: 'da', name: 'Danish', localName: 'Dansk' },
|
||||
{ code: 'de', name: 'German', localName: 'Deutsch' },
|
||||
{ code: 'de', name: 'German', localName: 'Deutsch Formell / Sie' },
|
||||
{ code: 'el', name: 'Greek', localName: 'Ελληνικά' },
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'es', name: 'Spanish', localName: 'Español' },
|
||||
{ code: 'fa', name: 'Persian', localName: 'فارسی' },
|
||||
{ code: 'fa', name: 'Persian', localName: 'فارسی', isRTL: true },
|
||||
{ code: 'fi', name: 'Finnish', localName: 'Suomi' },
|
||||
{ code: 'fr', name: 'French', localName: 'Français' },
|
||||
{ code: 'he', name: 'Hebrew', localName: 'Ivrit' },
|
||||
{ code: 'he', name: 'Hebrew', localName: 'Ivrit', isRTL: true },
|
||||
{ code: 'hi', name: 'Hindi', localName: 'हिन्दी' },
|
||||
{ code: 'hu', name: 'Hungarian', localName: 'Magyar' },
|
||||
{ code: 'id', name: 'Indonesian', localName: 'Bahasa Indonesia' },
|
||||
@ -35,6 +37,7 @@ export const languages: Language[] = [
|
||||
{ code: 'or', name: 'Odia', localName: 'ଓଡ଼ିଆ' },
|
||||
{ code: 'pl', name: 'Polish', localName: 'Polski' },
|
||||
{ code: 'pt', name: 'Portuguese', localName: 'Português' },
|
||||
{ code: 'pt-BR', name: 'Brazilian Portuguese', localName: 'Brasil' },
|
||||
{ code: 'ro', name: 'Romanian', localName: 'limba română' },
|
||||
{ code: 'ru', name: 'Russian', localName: 'русский' },
|
||||
{ code: 'sr', name: 'Serbian', localName: 'српски језик' },
|
||||
|
||||
@ -23,8 +23,8 @@ import {
|
||||
VolunteerActivism,
|
||||
Work,
|
||||
} from '@mui/icons-material';
|
||||
import { Section as SectionRecord } from '@reactive-resume/schema';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { Section as SectionRecord, SectionType } from 'schema';
|
||||
|
||||
import Basics from '@/components/build/LeftSidebar/sections/Basics';
|
||||
import Location from '@/components/build/LeftSidebar/sections/Location';
|
||||
@ -60,59 +60,136 @@ export const left: SidebarSection[] = [
|
||||
{
|
||||
id: 'work',
|
||||
icon: <Work />,
|
||||
component: <Section path="sections.work" titleKey="name" subtitleKey="position" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'work'}
|
||||
addMore={true}
|
||||
path="sections.work"
|
||||
titleKey="name"
|
||||
subtitleKey="position"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'education',
|
||||
icon: <School />,
|
||||
component: <Section path="sections.education" titleKey="institution" subtitleKey="area" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'education'}
|
||||
path="sections.education"
|
||||
titleKey="institution"
|
||||
subtitleKey="area"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'awards',
|
||||
icon: <EmojiEvents />,
|
||||
component: <Section path="sections.awards" titleKey="title" subtitleKey="awarder" isEditable isHideable />,
|
||||
component: (
|
||||
<Section type={'awards'} path="sections.awards" titleKey="title" subtitleKey="awarder" isEditable isHideable />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'certifications',
|
||||
icon: <CardGiftcard />,
|
||||
component: <Section path="sections.certifications" titleKey="name" subtitleKey="issuer" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'certifications'}
|
||||
path="sections.certifications"
|
||||
titleKey="name"
|
||||
subtitleKey="issuer"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'publications',
|
||||
icon: <MenuBook />,
|
||||
component: <Section path="sections.publications" titleKey="name" subtitleKey="publisher" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'publications'}
|
||||
path="sections.publications"
|
||||
titleKey="name"
|
||||
subtitleKey="publisher"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'skills',
|
||||
icon: <Architecture />,
|
||||
component: <Section path="sections.skills" titleKey="name" subtitleKey="level" isEditable isHideable />,
|
||||
component: (
|
||||
<Section type={'skills'} path="sections.skills" titleKey="name" subtitleKey="level" isEditable isHideable />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'languages',
|
||||
icon: <Language />,
|
||||
component: <Section path="sections.languages" titleKey="name" subtitleKey="level" isEditable isHideable />,
|
||||
component: (
|
||||
<Section type={'languages'} path="sections.languages" titleKey="name" subtitleKey="level" isEditable isHideable />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'interests',
|
||||
icon: <Sailing />,
|
||||
component: <Section path="sections.interests" titleKey="name" subtitleKey="keywords" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'interests'}
|
||||
path="sections.interests"
|
||||
titleKey="name"
|
||||
subtitleKey="keywords"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'volunteer',
|
||||
icon: <VolunteerActivism />,
|
||||
component: (
|
||||
<Section path="sections.volunteer" titleKey="organization" subtitleKey="position" isEditable isHideable />
|
||||
<Section
|
||||
type={'volunteer'}
|
||||
path="sections.volunteer"
|
||||
titleKey="organization"
|
||||
subtitleKey="position"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'projects',
|
||||
icon: <Coffee />,
|
||||
component: <Section path="sections.projects" titleKey="name" subtitleKey="description" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'projects'}
|
||||
path="sections.projects"
|
||||
titleKey="name"
|
||||
subtitleKey="description"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'references',
|
||||
icon: <Groups />,
|
||||
component: <Section path="sections.references" titleKey="name" subtitleKey="relationship" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'references'}
|
||||
path="sections.references"
|
||||
titleKey="name"
|
||||
subtitleKey="relationship"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@ -164,7 +241,19 @@ export const right: SidebarSection[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const getCustomSections = (sections: Record<string, SectionRecord>): Array<Required<SectionRecord>> => {
|
||||
export const getSectionsByType = (sections: Record<string, SectionRecord>, type: SectionType): SectionRecord[] => {
|
||||
if (isEmpty(sections)) return [];
|
||||
|
||||
return Object.entries(sections).reduce((acc, [id, section]) => {
|
||||
if (section.type.startsWith(type) && section.isDuplicated) {
|
||||
return [...acc, { ...section, id }];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as SectionRecord[]);
|
||||
};
|
||||
|
||||
export const getCustomSections = (sections: Record<string, SectionRecord>): SectionRecord[] => {
|
||||
if (isEmpty(sections)) return [];
|
||||
|
||||
return Object.entries(sections).reduce((acc, [id, section]) => {
|
||||
@ -173,7 +262,7 @@ export const getCustomSections = (sections: Record<string, SectionRecord>): Arra
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as Array<Required<SectionRecord>>);
|
||||
}, [] as SectionRecord[]);
|
||||
};
|
||||
|
||||
const sections = [...left, ...right];
|
||||
|
||||
@ -13,6 +13,11 @@ export const DOCS_URL = 'https://docs.rxresu.me';
|
||||
export const DONATION_URL = 'https://paypal.me/RajaRajanA';
|
||||
export const TRANSLATE_URL = 'https://translate.rxresu.me/';
|
||||
export const DIGITALOCEAN_URL = 'https://pillai.xyz/digitalocean';
|
||||
export const REDDIT_URL = 'https://www.reddit.com/r/reactiveresume/';
|
||||
export const GITHUB_URL = 'https://github.com/AmruthPillai/Reactive-Resume';
|
||||
export const PRODUCT_HUNT_URL = 'https://www.producthunt.com/posts/reactive-resume-v3';
|
||||
export const GITHUB_ISSUES_URL = 'https://github.com/AmruthPillai/Reactive-Resume/issues/new/choose';
|
||||
|
||||
// Default Error Message
|
||||
export const DEFAULT_ERROR_MESSAGE =
|
||||
'Something went wrong while performing this action, please report this issue on GitHub.';
|
||||
|
||||
@ -4,7 +4,7 @@ import { Login, Visibility, VisibilityOff } from '@mui/icons-material';
|
||||
import { Button, IconButton, InputAdornment, TextField } from '@mui/material';
|
||||
import { CredentialResponse, GoogleLogin } from '@react-oauth/google';
|
||||
import Joi from 'joi';
|
||||
import { isEmpty } from 'lodash';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
@ -62,14 +62,7 @@ const LoginModal: React.FC = () => {
|
||||
};
|
||||
|
||||
const onSubmit = async ({ identifier, password }: FormData) => {
|
||||
await loginMutation(
|
||||
{ identifier, password },
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
}
|
||||
);
|
||||
await loginMutation({ identifier, password });
|
||||
|
||||
handleClose();
|
||||
};
|
||||
@ -86,14 +79,14 @@ const LoginModal: React.FC = () => {
|
||||
|
||||
const handleLoginWithGoogle = async (response: CredentialResponse) => {
|
||||
if (response.credential) {
|
||||
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleLoginWithGoogleError });
|
||||
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleGoogleLoginError });
|
||||
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoginWithGoogleError = () => {
|
||||
toast("Please try logging in using email/password, or use another browser that supports Google's One Tap API.");
|
||||
const handleGoogleLoginError = () => {
|
||||
toast.error("Google doesn't seem to be responding, please try logging in using email/password instead.");
|
||||
};
|
||||
|
||||
const PasswordVisibility = (): React.ReactElement => {
|
||||
@ -117,7 +110,7 @@ const LoginModal: React.FC = () => {
|
||||
footerChildren={
|
||||
<div className="flex gap-4">
|
||||
{!isEmpty(env('GOOGLE_CLIENT_ID')) && (
|
||||
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleLoginWithGoogleError} />
|
||||
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleGoogleLoginError} />
|
||||
)}
|
||||
|
||||
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
||||
@ -162,15 +155,15 @@ const LoginModal: React.FC = () => {
|
||||
{!FLAG_DISABLE_SIGNUPS && (
|
||||
<p className="text-xs">
|
||||
<Trans t={t} i18nKey="modals.auth.login.register-text">
|
||||
If you don't have one, you can <a onClick={handleCreateAccount}>create an account</a> here.
|
||||
If you don't have one, you can <a onClick={handleCreateAccount}>create an account here.</a>
|
||||
</Trans>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="text-xs">
|
||||
<Trans t={t} i18nKey="modals.auth.login.recover-text">
|
||||
In case you have forgotten your password, you can <a onClick={handleRecoverAccount}>recover your account</a>
|
||||
here.
|
||||
In case you have forgotten your password, you can
|
||||
<a onClick={handleRecoverAccount}>recover your account here.</a>
|
||||
</Trans>
|
||||
</p>
|
||||
</BaseModal>
|
||||
|
||||
@ -4,7 +4,7 @@ import { HowToReg } from '@mui/icons-material';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { CredentialResponse, GoogleLogin } from '@react-oauth/google';
|
||||
import Joi from 'joi';
|
||||
import { isEmpty } from 'lodash';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
@ -81,14 +81,14 @@ const RegisterModal: React.FC = () => {
|
||||
|
||||
const handleLoginWithGoogle = async (response: CredentialResponse) => {
|
||||
if (response.credential) {
|
||||
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleLoginWithGoogleError });
|
||||
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleGoogleLoginError });
|
||||
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoginWithGoogleError = () => {
|
||||
toast("Please try logging in using email/password, or use another browser that supports Google's One Tap API.");
|
||||
const handleGoogleLoginError = () => {
|
||||
toast("Google doesn't seem to be responding, please try logging in using email/password instead.");
|
||||
};
|
||||
|
||||
return (
|
||||
@ -100,7 +100,7 @@ const RegisterModal: React.FC = () => {
|
||||
footerChildren={
|
||||
<div className="flex gap-4">
|
||||
{!isEmpty(env('GOOGLE_CLIENT_ID')) && (
|
||||
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleLoginWithGoogleError} />
|
||||
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleGoogleLoginError} />
|
||||
)}
|
||||
|
||||
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
||||
|
||||
161
client/modals/auth/UserProfileModal.tsx
Normal file
161
client/modals/auth/UserProfileModal.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { CrisisAlert, ManageAccounts } from '@mui/icons-material';
|
||||
import { Button, Divider, TextField } from '@mui/material';
|
||||
import Joi from 'joi';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import Avatar from '@/components/shared/Avatar';
|
||||
import BaseModal from '@/components/shared/BaseModal';
|
||||
import { deleteAccount, updateProfile, UpdateProfileParams } from '@/services/auth';
|
||||
import { ServerError } from '@/services/axios';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setModalState } from '@/store/modal/modalSlice';
|
||||
|
||||
type FormData = {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
const defaultState: FormData = {
|
||||
name: '',
|
||||
email: '',
|
||||
};
|
||||
|
||||
const schema = Joi.object({
|
||||
name: Joi.string().required(),
|
||||
email: Joi.string()
|
||||
.email({ tlds: { allow: false } })
|
||||
.required(),
|
||||
});
|
||||
|
||||
const UserProfileModal = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [deleteText, setDeleteText] = useState<string>('');
|
||||
const isDeleteTextValid = useMemo(() => deleteText.toLowerCase() === 'delete', [deleteText]);
|
||||
|
||||
const user = useAppSelector((state) => state.auth.user);
|
||||
const { open: isOpen } = useAppSelector((state) => state.modal['auth.profile']);
|
||||
|
||||
const { mutateAsync: deleteAccountMutation } = useMutation<void, ServerError>(deleteAccount);
|
||||
const { mutateAsync: updateProfileMutation } = useMutation<void, ServerError, UpdateProfileParams>(updateProfile);
|
||||
|
||||
const { reset, getFieldState, control, handleSubmit } = useForm<FormData>({
|
||||
defaultValues: defaultState,
|
||||
resolver: joiResolver(schema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (user && !getFieldState('name').isTouched && !getFieldState('email').isTouched) {
|
||||
reset({ name: user.name, email: user.email });
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(setModalState({ modal: 'auth.profile', state: { open: false } }));
|
||||
};
|
||||
|
||||
const handleUpdate = handleSubmit(async (data) => {
|
||||
handleClose();
|
||||
await updateProfileMutation({ name: data.name });
|
||||
});
|
||||
|
||||
const handleDelete = async () => {
|
||||
await deleteAccountMutation();
|
||||
handleClose();
|
||||
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseModal isOpen={isOpen} handleClose={handleClose} heading="Your Account" icon={<ManageAccounts />}>
|
||||
<div className="grid gap-4">
|
||||
<form className="grid gap-4 xl:w-2/3">
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar interactive={false} />
|
||||
|
||||
<div className="grid flex-1 gap-1.5">
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
autoFocus
|
||||
label={t<string>('modals.auth.profile.form.name.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<p className="pl-4 text-[10.25px] opacity-50">
|
||||
<Trans t={t} i18nKey="modals.auth.profile.form.avatar.help-text">
|
||||
You can update your profile picture on{' '}
|
||||
<a href="https://gravatar.com/" target="_blank" rel="noreferrer">
|
||||
Gravatar
|
||||
</a>
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
disabled
|
||||
label={t<string>('modals.auth.profile.form.email.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={t<string>('modals.auth.profile.form.email.help-text')}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button onClick={handleUpdate}>{t<string>('modals.auth.profile.actions.save')}</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="my-2">
|
||||
<Divider />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<CrisisAlert />
|
||||
<h5 className="font-medium">{t<string>('modals.auth.profile.delete-account.heading')}</h5>
|
||||
</div>
|
||||
|
||||
<p className="text-xs opacity-75">
|
||||
{t<string>('modals.auth.profile.delete-account.body', { keyword: 'delete' })}
|
||||
</p>
|
||||
|
||||
<div className="flex max-w-xs flex-col gap-4">
|
||||
<TextField
|
||||
value={deleteText}
|
||||
placeholder="Type 'delete' to confirm"
|
||||
onChange={(e) => setDeleteText(e.target.value)}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button variant="contained" color="error" disabled={!isDeleteTextValid} onClick={handleDelete}>
|
||||
{t<string>('modals.auth.profile.delete-account.actions.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfileModal;
|
||||
@ -1,8 +1,8 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { Award, SectionPath } from '@reactive-resume/schema';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { Award, SectionPath } from 'schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
import get from 'lodash/get';
|
||||
@ -93,7 +93,7 @@ const AwardModal: React.FC = () => {
|
||||
heading={isEditMode ? editText : addText}
|
||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
||||
>
|
||||
<form className="my-2 grid grid-cols-2 gap-4">
|
||||
<form className="my-2 grid grid-cols-2 gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
name="title"
|
||||
control={control}
|
||||
@ -128,21 +128,23 @@ const AwardModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
inputRef={field.ref}
|
||||
label={t<string>('builder.common.form.date.label')}
|
||||
value={dayjs(field.value)}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
slots={{
|
||||
textField: (params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || params.inputProps?.placeholder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
onChange={(date: dayjs.Dayjs | null) => {
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || params.inputProps?.placeholder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -177,6 +179,7 @@ const AwardModal: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<input type="submit" style={{ display: 'none' }} />
|
||||
</form>
|
||||
</BaseModal>
|
||||
);
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { Certificate, SectionPath } from '@reactive-resume/schema';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { Certificate, SectionPath } from 'schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
import get from 'lodash/get';
|
||||
@ -93,7 +93,7 @@ const CertificateModal: React.FC = () => {
|
||||
heading={isEditMode ? editText : addText}
|
||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
||||
>
|
||||
<form className="my-2 grid grid-cols-2 gap-4">
|
||||
<form className="my-2 grid grid-cols-2 gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
@ -128,21 +128,23 @@ const CertificateModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
inputRef={field.ref}
|
||||
label={t<string>('builder.common.form.date.label')}
|
||||
value={dayjs(field.value)}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
slots={{
|
||||
textField: (params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || params.inputProps?.placeholder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
onChange={(date: dayjs.Dayjs | null) => {
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || params.inputProps?.placeholder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -177,6 +179,7 @@ const CertificateModal: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<input type="submit" style={{ display: 'none' }} />
|
||||
</form>
|
||||
</BaseModal>
|
||||
);
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import { Button, Slider, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { Custom } from '@reactive-resume/schema';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { Custom } from 'schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
import get from 'lodash/get';
|
||||
@ -60,13 +60,14 @@ const CustomModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal['builder.sections.custom']);
|
||||
|
||||
const path: string = get(payload, 'path', '');
|
||||
const path: string = get(payload, 'path', 'sections.custom');
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
const isEditMode = useMemo(() => !!item, [item]);
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
|
||||
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||
|
||||
@ -110,7 +111,7 @@ const CustomModal: React.FC = () => {
|
||||
heading={isEditMode ? editText : addText}
|
||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
||||
>
|
||||
<form className="my-2 grid grid-cols-2 gap-4">
|
||||
<form className="my-2 grid grid-cols-2 gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
name="title"
|
||||
control={control}
|
||||
@ -144,21 +145,23 @@ const CustomModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
inputRef={field.ref}
|
||||
label={t<string>('builder.common.form.start-date.label')}
|
||||
value={dayjs(field.value)}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
slots={{
|
||||
textField: (params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || params.inputProps?.placeholder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
onChange={(date: dayjs.Dayjs | null) => {
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || params.inputProps?.placeholder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -168,21 +171,23 @@ const CustomModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
inputRef={field.ref}
|
||||
label={t<string>('builder.common.form.end-date.label')}
|
||||
value={dayjs(field.value)}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
slots={{
|
||||
textField: (params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || t<string>('builder.common.form.end-date.help-text')}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
onChange={(date: dayjs.Dayjs | null) => {
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || 'Leave this field blank, if still present'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -260,9 +265,9 @@ const CustomModal: React.FC = () => {
|
||||
multiline
|
||||
minRows={3}
|
||||
maxRows={6}
|
||||
label={t<string>('builder.common.form.summary.label')}
|
||||
className="col-span-2"
|
||||
error={!!fieldState.error}
|
||||
label={t<string>('builder.common.form.summary.label')}
|
||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||
{...field}
|
||||
/>
|
||||
@ -282,6 +287,7 @@ const CustomModal: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<input type="submit" style={{ display: 'none' }} />
|
||||
</form>
|
||||
</BaseModal>
|
||||
);
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { Education, SectionPath } from '@reactive-resume/schema';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { Education, SectionPath } from 'schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
import get from 'lodash/get';
|
||||
@ -106,7 +106,7 @@ const EducationModal: React.FC = () => {
|
||||
heading={isEditMode ? editText : addText}
|
||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
||||
>
|
||||
<form className="my-2 grid grid-cols-2 gap-4">
|
||||
<form className="my-2 grid grid-cols-2 gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
name="institution"
|
||||
control={control}
|
||||
@ -167,21 +167,23 @@ const EducationModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
inputRef={field.ref}
|
||||
label={t<string>('builder.common.form.start-date.label')}
|
||||
value={dayjs(field.value)}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
slots={{
|
||||
textField: (params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || params.inputProps?.placeholder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
onChange={(date: dayjs.Dayjs | null) => {
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || params.inputProps?.placeholder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -191,21 +193,23 @@ const EducationModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
inputRef={field.ref}
|
||||
label={t<string>('builder.common.form.end-date.label')}
|
||||
value={dayjs(field.value)}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
slots={{
|
||||
textField: (params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || t<string>('builder.common.form.end-date.help-text')}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
onChange={(date: dayjs.Dayjs | null) => {
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || t<string>('builder.common.form.end-date.help-text')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -255,6 +259,7 @@ const EducationModal: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<input type="submit" style={{ display: 'none' }} />
|
||||
</form>
|
||||
</BaseModal>
|
||||
);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { Interest, SectionPath } from '@reactive-resume/schema';
|
||||
import { Interest, SectionPath } from 'schema';
|
||||
import Joi from 'joi';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
@ -84,7 +84,7 @@ const InterestModal: React.FC = () => {
|
||||
heading={isEditMode ? editText : addText}
|
||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
||||
>
|
||||
<form className="my-2 grid grid-cols-2 gap-4">
|
||||
<form className="my-2 grid grid-cols-2 gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
@ -114,6 +114,7 @@ const InterestModal: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<input type="submit" style={{ display: 'none' }} />
|
||||
</form>
|
||||
</BaseModal>
|
||||
);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import { Button, Slider, TextField } from '@mui/material';
|
||||
import { Language, SectionPath } from '@reactive-resume/schema';
|
||||
import { Language, SectionPath } from 'schema';
|
||||
import Joi from 'joi';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
@ -85,7 +85,7 @@ const LanguageModal: React.FC = () => {
|
||||
heading={isEditMode ? editText : addText}
|
||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
||||
>
|
||||
<form className="my-2 grid grid-cols-2 gap-4">
|
||||
<form className="my-2 grid grid-cols-2 gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
@ -150,6 +150,7 @@ const LanguageModal: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<input type="submit" style={{ display: 'none' }} />
|
||||
</form>
|
||||
</BaseModal>
|
||||
);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, AlternateEmail, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { Profile } from '@reactive-resume/schema';
|
||||
import { Profile } from 'schema';
|
||||
import Joi from 'joi';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
@ -89,7 +89,7 @@ const ProfileModal: React.FC = () => {
|
||||
handleClose={handleClose}
|
||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
||||
>
|
||||
<form className="my-2 grid grid-cols-2 gap-4">
|
||||
<form className="my-2 grid grid-cols-2 gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
name="network"
|
||||
control={control}
|
||||
@ -136,6 +136,7 @@ const ProfileModal: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<input type="submit" style={{ display: 'none' }} />
|
||||
</form>
|
||||
</BaseModal>
|
||||
);
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { Project, SectionPath } from '@reactive-resume/schema';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { Project, SectionPath } from 'schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
import get from 'lodash/get';
|
||||
@ -102,7 +102,7 @@ const ProjectModal: React.FC = () => {
|
||||
heading={isEditMode ? editText : addText}
|
||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
||||
>
|
||||
<form className="my-2 grid grid-cols-2 gap-4">
|
||||
<form className="my-2 grid grid-cols-2 gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
@ -137,21 +137,23 @@ const ProjectModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
inputRef={field.ref}
|
||||
label={t<string>('builder.common.form.start-date.label')}
|
||||
value={dayjs(field.value)}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
slots={{
|
||||
textField: (params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || params.inputProps?.placeholder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
onChange={(date: dayjs.Dayjs | null) => {
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || params.inputProps?.placeholder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -161,21 +163,23 @@ const ProjectModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
inputRef={field.ref}
|
||||
label={t<string>('builder.common.form.end-date.label')}
|
||||
value={dayjs(field.value)}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
slots={{
|
||||
textField: (params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || t<string>('builder.common.form.end-date.help-text')}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
onChange={(date: dayjs.Dayjs | null) => {
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || 'Leave this field blank, if still present'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -225,6 +229,7 @@ const ProjectModal: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<input type="submit" style={{ display: 'none' }} />
|
||||
</form>
|
||||
</BaseModal>
|
||||
);
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { Publication, SectionPath } from '@reactive-resume/schema';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { Publication, SectionPath } from 'schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
import get from 'lodash/get';
|
||||
@ -93,7 +93,7 @@ const PublicationModal: React.FC = () => {
|
||||
heading={isEditMode ? editText : addText}
|
||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
||||
>
|
||||
<form className="my-2 grid grid-cols-2 gap-4">
|
||||
<form className="my-2 grid grid-cols-2 gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
@ -128,21 +128,23 @@ const PublicationModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
inputRef={field.ref}
|
||||
label={t<string>('builder.common.form.date.label')}
|
||||
value={dayjs(field.value)}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
slots={{
|
||||
textField: (params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || params.inputProps?.placeholder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
onChange={(date: dayjs.Dayjs | null) => {
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || params.inputProps?.placeholder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -177,6 +179,7 @@ const PublicationModal: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<input type="submit" style={{ display: 'none' }} />
|
||||
</form>
|
||||
</BaseModal>
|
||||
);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { Reference, SectionPath } from '@reactive-resume/schema';
|
||||
import { Reference, SectionPath } from 'schema';
|
||||
import Joi from 'joi';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
@ -90,7 +90,7 @@ const ReferenceModal: React.FC = () => {
|
||||
heading={isEditMode ? editText : addText}
|
||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
||||
>
|
||||
<form className="my-2 grid grid-cols-2 gap-4">
|
||||
<form className="my-2 grid grid-cols-2 gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
@ -162,6 +162,7 @@ const ReferenceModal: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<input type="submit" style={{ display: 'none' }} />
|
||||
</form>
|
||||
</BaseModal>
|
||||
);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import { Button, Slider, TextField } from '@mui/material';
|
||||
import { SectionPath, Skill } from '@reactive-resume/schema';
|
||||
import { SectionPath, Skill } from 'schema';
|
||||
import Joi from 'joi';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
@ -88,7 +88,7 @@ const SkillModal: React.FC = () => {
|
||||
heading={isEditMode ? editText : addText}
|
||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
||||
>
|
||||
<form className="my-2 grid grid-cols-2 gap-4">
|
||||
<form className="my-2 grid grid-cols-2 gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
@ -166,6 +166,8 @@ const SkillModal: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<input type="submit" style={{ display: 'none' }} />
|
||||
</form>
|
||||
</BaseModal>
|
||||
);
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { SectionPath, Volunteer } from '@reactive-resume/schema';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { SectionPath, Volunteer } from 'schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
import get from 'lodash/get';
|
||||
@ -99,7 +99,7 @@ const VolunteerModal: React.FC = () => {
|
||||
heading={isEditMode ? editText : addText}
|
||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
||||
>
|
||||
<form className="my-2 grid grid-cols-2 gap-4">
|
||||
<form className="my-2 grid grid-cols-2 gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
name="organization"
|
||||
control={control}
|
||||
@ -134,21 +134,23 @@ const VolunteerModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
inputRef={field.ref}
|
||||
label={t<string>('builder.common.form.start-date.label')}
|
||||
value={dayjs(field.value)}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
slots={{
|
||||
textField: (params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || params.inputProps?.placeholder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
onChange={(date: dayjs.Dayjs | null) => {
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || params.inputProps?.placeholder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -158,21 +160,23 @@ const VolunteerModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
inputRef={field.ref}
|
||||
label={t<string>('builder.common.form.end-date.label')}
|
||||
value={dayjs(field.value)}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
slots={{
|
||||
textField: (params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || t<string>('builder.common.form.end-date.help-text')}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
onChange={(date: dayjs.Dayjs | null) => {
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || 'Leave this field blank, if still present'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -208,6 +212,7 @@ const VolunteerModal: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<input type="submit" style={{ display: 'none' }} />
|
||||
</form>
|
||||
</BaseModal>
|
||||
);
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { SectionPath, WorkExperience } from '@reactive-resume/schema';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { WorkExperience } from 'schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
import get from 'lodash/get';
|
||||
@ -20,8 +20,6 @@ import { addItem, editItem } from '@/store/resume/resumeSlice';
|
||||
|
||||
type FormData = WorkExperience;
|
||||
|
||||
const path: SectionPath = 'sections.work';
|
||||
|
||||
const defaultState: FormData = {
|
||||
name: '',
|
||||
position: '',
|
||||
@ -50,14 +48,28 @@ const WorkModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal['builder.sections.work']);
|
||||
const path: string = get(payload, 'path', 'sections.work');
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
|
||||
const isEditMode = useMemo(() => !!item, [item]);
|
||||
|
||||
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||
const addText = useMemo(
|
||||
() =>
|
||||
t<string>('builder.common.actions.add', {
|
||||
token: t<string>(`builder.leftSidebar.${path}.heading`, { defaultValue: heading }),
|
||||
}),
|
||||
[t, heading]
|
||||
);
|
||||
const editText = useMemo(
|
||||
() =>
|
||||
t<string>('builder.common.actions.edit', {
|
||||
token: t<string>(`builder.leftSidebar.${path}.heading`, { defaultValue: heading }),
|
||||
}),
|
||||
[t, heading]
|
||||
);
|
||||
|
||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||
defaultValues: defaultState,
|
||||
@ -77,7 +89,7 @@ const WorkModal: React.FC = () => {
|
||||
const handleClose = () => {
|
||||
dispatch(
|
||||
setModalState({
|
||||
modal: `builder.${path}`,
|
||||
modal: 'builder.sections.work',
|
||||
state: { open: false },
|
||||
})
|
||||
);
|
||||
@ -99,7 +111,7 @@ const WorkModal: React.FC = () => {
|
||||
heading={isEditMode ? editText : addText}
|
||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
||||
>
|
||||
<form className="my-2 grid grid-cols-2 gap-4">
|
||||
<form className="my-2 grid grid-cols-2 gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
@ -107,7 +119,7 @@ const WorkModal: React.FC = () => {
|
||||
<TextField
|
||||
required
|
||||
autoFocus
|
||||
label={t<string>('builder.common.form.name.label')}
|
||||
label={t<string>('builder.leftSidebar.sections.experience.form.name.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
{...field}
|
||||
@ -134,21 +146,23 @@ const WorkModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
inputRef={field.ref}
|
||||
label={t<string>('builder.common.form.start-date.label')}
|
||||
value={dayjs(field.value)}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
slots={{
|
||||
textField: (params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || params.inputProps?.placeholder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
onChange={(date: dayjs.Dayjs | null) => {
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || params.inputProps?.placeholder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -158,21 +172,23 @@ const WorkModal: React.FC = () => {
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
openTo="year"
|
||||
inputRef={field.ref}
|
||||
label={t<string>('builder.common.form.end-date.label')}
|
||||
value={dayjs(field.value)}
|
||||
views={['year', 'month', 'day']}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && field.onChange('');
|
||||
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||
slots={{
|
||||
textField: (params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || t<string>('builder.common.form.end-date.help-text')}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
onChange={(date: dayjs.Dayjs | null) => {
|
||||
date && dayjs(date).isValid() && field.onChange(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message || t<string>('builder.common.form.end-date.help-text')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -208,6 +224,7 @@ const WorkModal: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<input type="submit" style={{ display: 'none' }} />
|
||||
</form>
|
||||
</BaseModal>
|
||||
);
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add } from '@mui/icons-material';
|
||||
import { Button, FormControlLabel, FormGroup, Switch, TextField } from '@mui/material';
|
||||
import { Resume } from '@reactive-resume/schema';
|
||||
import { Resume } from 'schema';
|
||||
import Joi from 'joi';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import BaseModal from '@/components/shared/BaseModal';
|
||||
@ -66,15 +65,10 @@ const CreateResumeModal: React.FC = () => {
|
||||
}, [name, setValue]);
|
||||
|
||||
const onSubmit = async ({ name, slug, isPublic }: FormData) => {
|
||||
try {
|
||||
await mutateAsync({ name, slug, public: isPublic });
|
||||
await mutateAsync({ name, slug, public: isPublic });
|
||||
await queryClient.invalidateQueries(RESUMES_QUERY);
|
||||
|
||||
await queryClient.invalidateQueries(RESUMES_QUERY);
|
||||
|
||||
handleClose();
|
||||
} catch (error: any) {
|
||||
toast.error(error.message);
|
||||
}
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Code, ImportExport, LinkedIn, TrackChanges, UploadFile } from '@mui/icons-material';
|
||||
import { Button, Divider } from '@mui/material';
|
||||
import { Integration, Resume } from '@reactive-resume/schema';
|
||||
import { Integration, Resume } from 'schema';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { useRef } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
@ -68,8 +68,8 @@ const ImportExternalModal: React.FC = () => {
|
||||
}
|
||||
|
||||
await mutateAsync({ integration, file });
|
||||
|
||||
queryClient.invalidateQueries(RESUMES_QUERY);
|
||||
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { Resume } from '@reactive-resume/schema';
|
||||
import { Resume } from 'schema';
|
||||
import Joi from 'joi';
|
||||
import get from 'lodash/get';
|
||||
import noop from 'lodash/noop';
|
||||
|
||||
@ -8,6 +8,7 @@ import ForgotPasswordModal from './auth/ForgotPasswordModal';
|
||||
import LoginModal from './auth/LoginModal';
|
||||
import RegisterModal from './auth/RegisterModal';
|
||||
import ResetPasswordModal from './auth/ResetPasswordModal';
|
||||
import UserProfileModal from './auth/UserProfileModal';
|
||||
import AwardModal from './builder/sections/AwardModal';
|
||||
import CertificateModal from './builder/sections/CertificateModal';
|
||||
import CustomModal from './builder/sections/CustomModal';
|
||||
@ -49,6 +50,7 @@ const ModalWrapper: React.FC = () => {
|
||||
<RegisterModal />
|
||||
<ForgotPasswordModal />
|
||||
<ResetPasswordModal />
|
||||
<UserProfileModal />
|
||||
|
||||
{/* Dashboard */}
|
||||
<CreateResumeModal />
|
||||
|
||||
@ -4,6 +4,7 @@ const i18nConfig = {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: [
|
||||
'am',
|
||||
'ar',
|
||||
'bg',
|
||||
'bn',
|
||||
@ -34,6 +35,7 @@ const i18nConfig = {
|
||||
'or',
|
||||
'pl',
|
||||
'pt',
|
||||
'pt-BR',
|
||||
'ro',
|
||||
'ru',
|
||||
'sr',
|
||||
@ -46,6 +48,7 @@ const i18nConfig = {
|
||||
],
|
||||
},
|
||||
nsSeparator: '.',
|
||||
returnNull: false,
|
||||
localePath: path.resolve('./public/locales'),
|
||||
ns: ['common', 'modals', 'landing', 'dashboard', 'builder'],
|
||||
};
|
||||
|
||||
@ -2,5 +2,5 @@
|
||||
module.exports = {
|
||||
siteUrl: 'https://rxresu.me',
|
||||
changefreq: 'monthly',
|
||||
generateRobotsTxt: true,
|
||||
generateIndexSitemap: false,
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@reactive-resume/client",
|
||||
"name": "client",
|
||||
"scripts": {
|
||||
"dev": "react-env --prefix PUBLIC -- next dev",
|
||||
"lint": "next lint --fix",
|
||||
@ -9,76 +9,74 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@beam-australia/react-env": "^3.1.1",
|
||||
"@date-io/dayjs": "^2.15.0",
|
||||
"@emotion/css": "^11.10.0",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/styled": "^11.10.4",
|
||||
"@hello-pangea/dnd": "^16.0.0",
|
||||
"@hookform/resolvers": "2.9.8",
|
||||
"@monaco-editor/react": "^4.4.5",
|
||||
"@mui/icons-material": "^5.10.3",
|
||||
"@mui/lab": "^5.0.0-alpha.99",
|
||||
"@mui/material": "^5.10.5",
|
||||
"@mui/system": "^5.10.5",
|
||||
"@mui/x-date-pickers": "5.0.1",
|
||||
"@next/env": "^12.3.0",
|
||||
"@react-oauth/google": "^0.2.6",
|
||||
"@reduxjs/toolkit": "^1.8.5",
|
||||
"axios": "^0.27.2",
|
||||
"@emotion/css": "^11.11.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@hello-pangea/dnd": "^16.2.0",
|
||||
"@hookform/resolvers": "3.1.0",
|
||||
"@monaco-editor/react": "^4.5.1",
|
||||
"@mui/icons-material": "^5.11.16",
|
||||
"@mui/lab": "^5.0.0-alpha.133",
|
||||
"@mui/material": "^5.13.4",
|
||||
"@mui/system": "^5.13.2",
|
||||
"@mui/x-date-pickers": "6.6.0",
|
||||
"@react-oauth/google": "^0.11.0",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"axios": "^1.4.0",
|
||||
"clsx": "^1.2.1",
|
||||
"dayjs": "^1.11.5",
|
||||
"dayjs": "^1.11.8",
|
||||
"downloadjs": "^1.4.7",
|
||||
"joi": "^17.6.0",
|
||||
"joi": "^17.9.2",
|
||||
"lodash": "^4.17.21",
|
||||
"md5-hex": "^4.0.0",
|
||||
"monaco-editor": "^0.34.0",
|
||||
"nanoid": "^3.3.4",
|
||||
"next": "12.3.0",
|
||||
"next-i18next": "^12.0.1",
|
||||
"monaco-editor": "^0.39.0",
|
||||
"nanoid": "3.3.4",
|
||||
"next": "13.4.4",
|
||||
"next-i18next": "^13.3.0",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dnd": "16.0.1",
|
||||
"react-dnd-html5-backend": "16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.35.0",
|
||||
"react-hot-toast": "2.4.0",
|
||||
"react-hotkeys-hook": "^3.4.7",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-query": "^3.39.2",
|
||||
"react-redux": "^8.0.2",
|
||||
"react-zoom-pan-pinch": "^2.1.3",
|
||||
"redux": "^4.2.0",
|
||||
"react-hook-form": "^7.44.3",
|
||||
"react-hot-toast": "2.4.1",
|
||||
"react-icons": "^4.9.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-query": "^3.39.3",
|
||||
"react-redux": "^8.0.7",
|
||||
"react-zoom-pan-pinch": "^3.0.8",
|
||||
"redux": "^4.2.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.2.1",
|
||||
"redux-saga": "^1.2.3",
|
||||
"redux-undo": "^1.0.1",
|
||||
"rehype-katex": "^6.0.3",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sharp": "^0.31.0",
|
||||
"remark-math": "^5.1.1",
|
||||
"sharp": "^0.32.1",
|
||||
"uuid": "^9.0.0",
|
||||
"webfontloader": "^1.6.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.1",
|
||||
"@reactive-resume/schema": "workspace:*",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"@tailwindcss/typography": "^0.5.7",
|
||||
"@babel/core": "^7.22.1",
|
||||
"schema": "workspace:*",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/downloadjs": "^1.4.3",
|
||||
"@types/lodash": "^4.14.185",
|
||||
"@types/node": "^18.7.18",
|
||||
"@types/react": "^18.0.20",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-redux": "^7.1.24",
|
||||
"@types/tailwindcss": "^3.0.11",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/webfontloader": "^1.6.34",
|
||||
"autoprefixer": "^10.4.11",
|
||||
"csstype": "^3.1.1",
|
||||
"eslint-config-next": "^12.3.0",
|
||||
"eslint-plugin-tailwindcss": "^3.6.1",
|
||||
"next-sitemap": "^3.1.22",
|
||||
"postcss": "^8.4.16",
|
||||
"sass": "^1.54.9",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"typescript": "^4.8.3"
|
||||
"@types/lodash": "^4.14.195",
|
||||
"@types/node": "^20.2.5",
|
||||
"@types/react": "^18.2.8",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@types/react-redux": "^7.1.25",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@types/webfontloader": "^1.6.35",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"csstype": "^3.1.2",
|
||||
"eslint-config-next": "^13.4.4",
|
||||
"eslint-plugin-tailwindcss": "^3.12.1",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"next-sitemap": "^4.1.3",
|
||||
"postcss": "^8.4.24",
|
||||
"sass": "^1.62.1",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { Resume } from '@reactive-resume/schema';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { GetServerSideProps, NextPage } from 'next';
|
||||
import Head from 'next/head';
|
||||
@ -6,6 +5,7 @@ import { useTranslation } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useEffect } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Resume } from 'schema';
|
||||
|
||||
import Center from '@/components/build/Center/Center';
|
||||
import LeftSidebar from '@/components/build/LeftSidebar/LeftSidebar';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Download, Downloading } from '@mui/icons-material';
|
||||
import { ButtonBase } from '@mui/material';
|
||||
import { Resume } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import download from 'downloadjs';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
@ -12,8 +12,10 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { Resume } from 'schema';
|
||||
|
||||
import Page from '@/components/build/Center/Page';
|
||||
import { DEFAULT_ERROR_MESSAGE } from '@/constants/index';
|
||||
import { ServerError } from '@/services/axios';
|
||||
import { printResumeAsPdf, PrintResumeAsPdfParams } from '@/services/printer';
|
||||
import { fetchResumeByIdentifier } from '@/services/resume';
|
||||
@ -60,10 +62,12 @@ const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
|
||||
}, [dispatch, initialData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(resume) && router.locale !== resume.metadata.locale) {
|
||||
const locale = get(resume, 'metadata.locale', 'en');
|
||||
|
||||
if (!isEmpty(resume) && router.locale !== locale) {
|
||||
const { pathname, asPath, query } = router;
|
||||
|
||||
router.push({ pathname, query }, asPath, { locale: resume.metadata.locale });
|
||||
router.push({ pathname, query }, asPath, { locale });
|
||||
}
|
||||
}, [resume, router]);
|
||||
|
||||
@ -96,11 +100,13 @@ const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const url = await mutateAsync({ username, slug });
|
||||
const updatedAt = get(resume, 'updatedAt');
|
||||
|
||||
const url = await mutateAsync({ username, slug, lastUpdated: dayjs(updatedAt).unix().toString() });
|
||||
|
||||
download(url);
|
||||
} catch {
|
||||
toast.error('Something went wrong, please try again later.');
|
||||
toast.error(DEFAULT_ERROR_MESSAGE);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { Resume } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
@ -6,6 +5,7 @@ import { GetServerSideProps, NextPage } from 'next';
|
||||
import { useRouter } from 'next/router';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useEffect } from 'react';
|
||||
import { Resume } from 'schema';
|
||||
|
||||
import Page from '@/components/build/Center/Page';
|
||||
import { fetchResumeByIdentifier } from '@/services/resume';
|
||||
@ -27,7 +27,7 @@ type Props = {
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Props | Promise<Props>, QueryParams> = async ({
|
||||
query,
|
||||
locale,
|
||||
locale = 'en',
|
||||
}) => {
|
||||
const { username, slug, secretKey } = query as QueryParams;
|
||||
|
||||
@ -35,7 +35,7 @@ export const getServerSideProps: GetServerSideProps<Props | Promise<Props>, Quer
|
||||
if (isEmpty(secretKey)) throw new Error('There is no secret key!');
|
||||
|
||||
const resume = await fetchResumeByIdentifier({ username, slug, options: { secretKey } });
|
||||
const displayLocale = resume.metadata.locale || locale || 'en';
|
||||
const displayLocale = get(resume, 'metadata.locale') ?? locale;
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import '@/styles/globals.scss';
|
||||
|
||||
import env from '@beam-australia/react-env';
|
||||
import DayjsAdapter from '@date-io/dayjs';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers';
|
||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||
import { GoogleOAuthProvider } from '@react-oauth/google';
|
||||
import type { AppProps } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
@ -21,7 +21,7 @@ import WrapperRegistry from '@/wrappers/index';
|
||||
const App = ({ Component, pageProps }: AppProps): JSX.Element => (
|
||||
<>
|
||||
<Head>
|
||||
<title>Reactive Resume</title>
|
||||
<title>Reactive Resume | A free and open source resume builder</title>
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
@ -32,7 +32,7 @@ const App = ({ Component, pageProps }: AppProps): JSX.Element => (
|
||||
</Head>
|
||||
|
||||
<ReduxProvider store={store}>
|
||||
<LocalizationProvider dateAdapter={DayjsAdapter}>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<GoogleOAuthProvider clientId={env('GOOGLE_CLIENT_ID')}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
@ -1,15 +1,69 @@
|
||||
import { NextPage } from 'next';
|
||||
import NextDocument, { DocumentContext, Head, Html, Main, NextScript } from 'next/document';
|
||||
import Script from 'next/script';
|
||||
|
||||
const Document: NextPage = () => (
|
||||
<Html>
|
||||
<Head />
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<Script
|
||||
id="google-tag-manager"
|
||||
type="text/javascript"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-M9DK4S4');`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Script
|
||||
id="logo-schema"
|
||||
type="application/ld+json"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"url": "https://rxresu.me",
|
||||
"logo": "https://rxresu.me/images/logos/logo.svg"
|
||||
}`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Script
|
||||
id="software-application-schema"
|
||||
type="application/ld+json"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "Reactive Resume",
|
||||
"operatingSystem": "Windows, macOS, Linux, Android, iOS",
|
||||
"applicationCategory": "BrowserApplication",
|
||||
"aggregateRating": {
|
||||
"@type": "AggregateRating",
|
||||
"ratingValue": "5",
|
||||
"ratingCount": "11394"
|
||||
},
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0"
|
||||
}
|
||||
}`,
|
||||
}}
|
||||
/>
|
||||
</Head>
|
||||
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
|
||||
<script src="/__ENV.js" />
|
||||
|
||||
<noscript
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-M9DK4S4" height="0" width="0" style="display:none;visibility:hidden"></iframe>`,
|
||||
}}
|
||||
/>
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
|
||||
@ -17,13 +17,11 @@ import { fetchResumes } from '@/services/resume';
|
||||
import { useAppDispatch } from '@/store/hooks';
|
||||
import styles from '@/styles/pages/Dashboard.module.scss';
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common', 'modals', 'dashboard'])),
|
||||
},
|
||||
};
|
||||
};
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => ({
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common', 'modals', 'dashboard'])),
|
||||
},
|
||||
});
|
||||
|
||||
const Dashboard: NextPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -48,9 +46,7 @@ const Dashboard: NextPage = () => {
|
||||
|
||||
<header>
|
||||
<Link href="/">
|
||||
<a>
|
||||
<Logo size={40} />
|
||||
</a>
|
||||
<Logo size={40} />
|
||||
</Link>
|
||||
|
||||
<Avatar size={40} />
|
||||
@ -58,15 +54,15 @@ const Dashboard: NextPage = () => {
|
||||
|
||||
<main className={styles.resumes}>
|
||||
<ResumeCard
|
||||
modal="dashboard.create-resume"
|
||||
icon={Add}
|
||||
modal="dashboard.create-resume"
|
||||
title={t<string>('dashboard.create-resume.title')}
|
||||
subtitle={t<string>('dashboard.create-resume.subtitle')}
|
||||
/>
|
||||
|
||||
<ResumeCard
|
||||
modal="dashboard.import-external"
|
||||
icon={ImportExport}
|
||||
modal="dashboard.import-external"
|
||||
title={t<string>('dashboard.import-external.title')}
|
||||
subtitle={t<string>('dashboard.import-external.subtitle')}
|
||||
/>
|
||||
|
||||
@ -20,15 +20,13 @@ import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setModalState } from '@/store/modal/modalSlice';
|
||||
import styles from '@/styles/pages/Home.module.scss';
|
||||
|
||||
import { DIGITALOCEAN_URL, DOCS_URL, DONATION_URL, GITHUB_URL } from '../constants';
|
||||
import { DIGITALOCEAN_URL, DOCS_URL, DONATION_URL, GITHUB_URL, REDDIT_URL } from '../constants';
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common', 'modals', 'landing'])),
|
||||
},
|
||||
};
|
||||
};
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => ({
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common', 'modals', 'landing'])),
|
||||
},
|
||||
});
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -39,11 +37,8 @@ const Home: NextPage = () => {
|
||||
const isLoggedIn = useAppSelector((state) => state.auth.isLoggedIn);
|
||||
|
||||
const handleLogin = () => dispatch(setModalState({ modal: 'auth.login', state: { open: true } }));
|
||||
|
||||
const handleRegister = () => dispatch(setModalState({ modal: 'auth.register', state: { open: true } }));
|
||||
|
||||
const handleToggle = () => dispatch(setTheme({ theme: theme === 'light' ? 'dark' : 'light' }));
|
||||
|
||||
const handleLogout = () => dispatch(logout());
|
||||
|
||||
return (
|
||||
@ -117,7 +112,13 @@ const Home: NextPage = () => {
|
||||
<div className={styles.screenshots}>
|
||||
{screenshots.map(({ src, alt }) => (
|
||||
<a key={src} href={src} className={styles.image} target="_blank" rel="noreferrer">
|
||||
<Image src={src} alt={alt} layout="fill" objectFit="cover" />
|
||||
<Image
|
||||
fill
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="object-cover"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
@ -176,6 +177,12 @@ const Home: NextPage = () => {
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<a href={REDDIT_URL} target="_blank" rel="noreferrer">
|
||||
<Button variant="text" startIcon={<LinkIcon />}>
|
||||
{t<string>('landing.links.links.reddit')}
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<a href={DONATION_URL} target="_blank" rel="noreferrer">
|
||||
<Button variant="text" startIcon={<LinkIcon />}>
|
||||
{t<string>('landing.links.links.donate')}
|
||||
@ -186,7 +193,13 @@ const Home: NextPage = () => {
|
||||
|
||||
<section className={styles.section}>
|
||||
<a href={DIGITALOCEAN_URL} target="_blank" rel="noreferrer">
|
||||
<Image src="/images/sponsors/digitalocean.svg" alt="Powered By DigitalOcean" width={200} height={40} />
|
||||
<Image
|
||||
src={`/images/sponsors/${theme == 'dark' ? 'digitalocean' : 'digitaloceanLight'}.svg`}
|
||||
style={{ width: 200, height: 40, objectFit: 'contain' }}
|
||||
alt="Powered By DigitalOcean"
|
||||
width={200}
|
||||
height={40}
|
||||
/>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Download, Downloading } from '@mui/icons-material';
|
||||
import { ButtonBase } from '@mui/material';
|
||||
import { Resume } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import download from 'downloadjs';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
@ -10,8 +10,8 @@ import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { Resume } from 'schema';
|
||||
|
||||
import Page from '@/components/build/Center/Page';
|
||||
import { ServerError } from '@/services/axios';
|
||||
@ -68,13 +68,13 @@ const Preview: NextPage<Props> = ({ shortId }) => {
|
||||
const layout: string[][][] = get(resume, 'metadata.layout', []);
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const url = await mutateAsync({ username: resume.user.username, slug: resume.slug });
|
||||
const url = await mutateAsync({
|
||||
username: resume.user.username,
|
||||
slug: resume.slug,
|
||||
lastUpdated: dayjs(resume.updatedAt).unix().toString(),
|
||||
});
|
||||
|
||||
download(url);
|
||||
} catch {
|
||||
toast.error('Something went wrong, please try again later.');
|
||||
}
|
||||
download(url);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
101
client/public/images/sponsors/digitaloceanLight.svg
Normal file
101
client/public/images/sponsors/digitaloceanLight.svg
Normal file
@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 604 129" style="enable-background:new 0 0 604 129;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#0069ff;}
|
||||
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#0069ff;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M174.3,3c4.9,0,8.7,2.9,8.7,8.6c0,5.6-3.8,8.5-8.7,8.5h-7.6v11.1h-3.5V3H174.3z M166.7,17.1h7.2
|
||||
c3,0,5.6-1.8,5.6-5.5c0-3.8-2.5-5.5-5.6-5.5h-7.2V17.1z"/>
|
||||
<path class="st0" d="M208.8,21.7c0,6.1-4.3,10-9.9,10c-5.6,0-9.9-3.9-9.9-10c0-6.1,4.3-10,9.9-10
|
||||
C204.5,11.7,208.8,15.6,208.8,21.7z M192.3,21.7c0,4.5,2.9,7.2,6.6,7.2c3.7,0,6.6-2.7,6.6-7.2c0-4.5-2.9-7.1-6.6-7.1
|
||||
C195.2,14.5,192.3,17.2,192.3,21.7z"/>
|
||||
<path class="st0" d="M234.4,31.3l-5.2-13.8L224,31.3h-2.6L214.1,12h3.6l5.2,14l5.2-14h2.3l5.3,14l5.2-14h3.5L237,31.3H234.4z"/>
|
||||
<path class="st0" d="M253,22.9c0.2,3.7,2.6,5.9,6,5.9c2.8,0,4.8-1.3,5.4-3.4l3.2,0.2c-0.8,3.5-4.1,6.1-8.6,6.1
|
||||
c-5.5,0-9.6-3.7-9.6-10c0-6.3,4-10,9.5-10c5.5,0,8.8,3.7,8.8,9.4v1.8H253z M253,20.3h11.6c-0.1-3.4-2-5.7-5.6-5.7
|
||||
C255.6,14.5,253.2,16.5,253,20.3z"/>
|
||||
<path class="st0" d="M285.4,14.9c-3.4,0-5.6,2.3-5.6,5.3v11.1h-3.2V12h3.2v2.9c0.7-1.6,2.5-3.1,5.7-3.1V14.9z"/>
|
||||
<path class="st0" d="M294.7,22.9c0.2,3.7,2.6,5.9,6,5.9c2.8,0,4.8-1.3,5.4-3.4l3.2,0.2c-0.8,3.5-4.1,6.1-8.6,6.1
|
||||
c-5.5,0-9.6-3.7-9.6-10c0-6.3,4-10,9.5-10c5.5,0,8.8,3.7,8.8,9.4v1.8H294.7z M294.7,20.3h11.6c-0.1-3.4-2-5.7-5.6-5.7
|
||||
C297.4,14.5,294.9,16.5,294.7,20.3z"/>
|
||||
<path class="st0" d="M333.1,31.3v-3.1c-1.1,2-3.6,3.5-6.8,3.5c-5.3,0-9.3-3.8-9.3-10c0-6.2,4-10,9.3-10c3.2,0,5.6,1.4,6.6,3.2V2
|
||||
h3.2v29.4H333.1z M320.3,21.7c0,4.6,2.8,7.2,6.5,7.2c3.6,0,6.2-2.2,6.2-6.6v-1.1c0-4.3-2.6-6.6-6.2-6.6
|
||||
C323.1,14.5,320.3,17.1,320.3,21.7z"/>
|
||||
<path class="st0" d="M361.8,14.9c1.1-1.9,3.4-3.2,6.7-3.2c5.3,0,9.3,3.8,9.3,10c0,6.2-4,10-9.3,10c-3.3,0-5.7-1.5-6.8-3.5v3.1
|
||||
h-3.1V2h3.2V14.9z M361.9,21.1v1.1c0,4.4,2.6,6.6,6.2,6.6c3.7,0,6.5-2.5,6.5-7.2c0-4.6-2.8-7.1-6.5-7.1
|
||||
C364.5,14.5,361.9,16.8,361.9,21.1z"/>
|
||||
<path class="st0" d="M386.3,40.9l4.6-10.7L383.2,12h3.6l5.8,14.5l5.8-14.5h3.6l-12.2,28.9H386.3z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="XMLID_2369_">
|
||||
<g>
|
||||
<g id="XMLID_281_">
|
||||
<g id="XMLID_282_">
|
||||
<g>
|
||||
<g id="XMLID_283_">
|
||||
<g id="XMLID_287_">
|
||||
<path id="XMLID_288_" class="st0" d="M64.4,127l0-24.2c25.6,0,45.5-25.4,35.7-52.3c-3.6-10-11.6-17.9-21.6-21.6
|
||||
c-27-9.8-52.3,10-52.3,35.7c0,0,0,0,0,0L2,64.7C2,23.8,41.5-8,84.3,5.4c18.7,5.8,33.6,20.7,39.4,39.4
|
||||
C137,87.6,105.2,127,64.4,127z"/>
|
||||
</g>
|
||||
<polygon id="XMLID_286_" class="st1" points="64.4,102.9 40.4,102.9 40.4,78.9 40.4,78.9 64.4,78.9 64.4,78.9 "/>
|
||||
<polygon id="XMLID_285_" class="st1" points="40.3,121.5 21.8,121.5 21.8,121.5 21.8,102.9 40.4,102.9 40.4,121.5 "/>
|
||||
<path id="XMLID_284_" class="st1" d="M21.9,102.9H6.3c0,0,0,0,0,0V87.4c0,0,0,0,0,0h15.5c0,0,0,0,0,0V102.9z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="XMLID_254_">
|
||||
<path id="XMLID_278_" class="st0" d="M200.9,52.4c-5.5-3.8-12.4-5.8-20.5-5.8h-17.5v55.5h17.5c8,0,14.9-2.1,20.5-6.1
|
||||
c3-2.1,5.4-5.1,7.1-8.9c1.7-3.7,2.5-8.2,2.5-13.1c0-4.9-0.8-9.3-2.5-13C206.3,57.4,203.9,54.4,200.9,52.4z M173.1,56h5.5
|
||||
c6.1,0,11.1,1.2,15,3.6c4.2,2.6,6.4,7.4,6.4,14.4c0,7.2-2.2,12.3-6.4,15.1h0c-3.7,2.4-8.7,3.6-14.9,3.6h-5.6V56z"/>
|
||||
<path id="XMLID_277_" class="st0" d="M222.6,45.9c-1.7,0-3.1,0.6-4.3,1.8c-1.2,1.1-1.8,2.6-1.8,4.2c0,1.7,0.6,3.1,1.8,4.3
|
||||
c1.2,1.2,2.6,1.8,4.3,1.8c1.7,0,3.1-0.6,4.3-1.8c1.2-1.2,1.8-2.6,1.8-4.3c0-1.7-0.6-3.1-1.8-4.2
|
||||
C225.7,46.5,224.3,45.9,222.6,45.9z"/>
|
||||
<rect id="XMLID_276_" x="217.6" y="63" class="st0" width="9.8" height="39.1"/>
|
||||
<path id="XMLID_273_" class="st0" d="M263.2,66.3c-3-2.6-6.3-4.2-9.9-4.2c-5.4,0-9.9,1.9-13.4,5.6c-3.5,3.7-5.3,8.4-5.3,14.1
|
||||
c0,5.5,1.8,10.2,5.2,14c3.5,3.7,8,5.5,13.5,5.5c3.8,0,7.1-1.1,9.7-3.1V99c0,3.2-0.9,5.8-2.6,7.5c-1.7,1.7-4.1,2.6-7.1,2.6
|
||||
c-4.5,0-7.4-1.8-10.9-6.5l-6.7,6.4l0.2,0.3c1.4,2,3.7,4,6.6,5.9c2.9,1.9,6.6,2.8,10.9,2.8c5.8,0,10.6-1.8,14.1-5.4
|
||||
c3.5-3.6,5.3-8.4,5.3-14.2V63h-9.7V66.3z M260.6,89.4c-1.7,2-3.9,2.9-6.8,2.9c-2.8,0-5-0.9-6.7-2.9c-1.7-1.9-2.5-4.5-2.5-7.7
|
||||
c0-3.2,0.9-5.8,2.5-7.7c1.7-1.9,3.9-2.9,6.7-2.9c2.8,0,5,1,6.8,2.9c1.7,2,2.6,4.6,2.6,7.7C263.2,84.9,262.3,87.5,260.6,89.4z"/>
|
||||
<rect id="XMLID_272_" x="281.3" y="63" class="st0" width="9.8" height="39.1"/>
|
||||
<path id="XMLID_271_" class="st0" d="M286.3,45.9c-1.7,0-3.1,0.6-4.3,1.8c-1.2,1.1-1.8,2.6-1.8,4.2c0,1.7,0.6,3.1,1.8,4.3
|
||||
c1.2,1.2,2.6,1.8,4.3,1.8c1.7,0,3.1-0.6,4.3-1.8c1.2-1.2,1.8-2.6,1.8-4.3c0-1.7-0.6-3.1-1.8-4.2C289.4,46.5,288,45.9,286.3,45.9
|
||||
z"/>
|
||||
<path id="XMLID_270_" class="st0" d="M312.7,52.5H303V63h-5.6v9h5.6v16.2c0,5.1,1,8.7,3,10.8c2,2.1,5.6,3.2,10.6,3.2
|
||||
c1.6,0,3.2-0.1,4.8-0.2l0.4,0v-9l-3.4,0.2c-2.3,0-3.9-0.4-4.7-1.2c-0.8-0.8-1.1-2.6-1.1-5.2V72h9.2v-9h-9.2V52.5z"/>
|
||||
<rect id="XMLID_269_" x="368" y="46.6" class="st0" width="9.8" height="55.5"/>
|
||||
<path id="XMLID_268_" class="st0" d="M477.3,88.2c-1.8,2-3.6,3.7-4.9,4.6v0c-1.4,0.9-3.1,1.3-5.1,1.3c-2.9,0-5.2-1.1-7.1-3.2
|
||||
c-1.9-2.2-2.8-4.9-2.8-8.3s0.9-6.1,2.8-8.2c1.9-2.2,4.2-3.2,7.1-3.2c3.2,0,6.5,2,9.4,5.4l6.5-6.2l0,0c-4.2-5.5-9.7-8.1-16.1-8.1
|
||||
c-5.4,0-10.1,2-13.9,5.8c-3.8,3.9-5.7,8.8-5.7,14.6s1.9,10.7,5.7,14.6c3.8,3.9,8.5,5.9,13.9,5.9c7.1,0,12.9-3.1,16.8-8.7
|
||||
L477.3,88.2z"/>
|
||||
<path id="XMLID_265_" class="st0" d="M517.7,68.5c-1.4-1.9-3.3-3.5-5.7-4.7c-2.3-1.1-5.1-1.7-8.1-1.7c-5.5,0-10,2-13.4,6
|
||||
c-3.3,4-4.9,8.9-4.9,14.7c0,5.9,1.8,10.8,5.4,14.6c3.6,3.7,8.4,5.6,14.2,5.6c6.6,0,12.1-2.7,16.2-8l0.2-0.3l-6.4-6.2l0,0
|
||||
c-0.6,0.7-1.4,1.5-2.2,2.3c-1,0.9-1.9,1.6-2.9,2.1c-1.5,0.7-3.1,1.1-5,1.1c-2.7,0-5-0.8-6.7-2.4c-1.6-1.5-2.6-3.5-2.8-5.9h26.1
|
||||
l0.1-3.6c0-2.5-0.3-5-1-7.3C520.1,72.6,519.1,70.4,517.7,68.5z M496.2,77.7c0.5-1.9,1.3-3.4,2.6-4.6c1.3-1.3,3.1-2,5.2-2
|
||||
c2.4,0,4.2,0.7,5.5,2c1.2,1.2,1.8,2.8,2,4.6H496.2z"/>
|
||||
<path id="XMLID_262_" class="st0" d="M555.5,66L555.5,66c-3-2.5-7.1-3.8-12.3-3.8c-3.3,0-6.3,0.7-9.1,2.1
|
||||
c-2.6,1.3-5.1,3.5-6.7,6.3l0.1,0.1l6.3,6c2.6-4.1,5.5-5.6,9.3-5.6c2.1,0,3.8,0.6,5.1,1.6c1.3,1.1,1.9,2.5,1.9,4.2v1.9
|
||||
c-2.4-0.7-4.9-1.1-7.2-1.1c-4.9,0-8.9,1.2-11.8,3.4c-3,2.3-4.5,5.6-4.5,9.8c0,3.7,1.3,6.7,3.8,8.9c2.6,2.1,5.8,3.2,9.5,3.2
|
||||
c3.7,0,7.3-1.5,10.4-4.1v3.2h9.7V77C560,72.2,558.5,68.5,555.5,66z M538,87.2c1.1-0.8,2.7-1.2,4.7-1.2c2.4,0,4.9,0.5,7.5,1.4
|
||||
v3.8c-2.1,2-5,3-8.5,3c-1.7,0-3-0.4-3.9-1.1c-0.9-0.7-1.3-1.7-1.3-2.8C536.4,89,536.9,88,538,87.2z"/>
|
||||
<path id="XMLID_261_" class="st0" d="M597.9,66.7c-2.7-3.1-6.6-4.6-11.5-4.6c-3.9,0-7.1,1.1-9.4,3.3V63h-9.7v39.1h9.8V80.6
|
||||
c0-3,0.7-5.3,2.1-7c1.4-1.7,3.3-2.5,5.8-2.5c2.2,0,3.9,0.7,5.2,2.2c1.3,1.5,1.9,3.6,1.9,6.2v22.7h9.8V79.5
|
||||
C602,74.1,600.6,69.8,597.9,66.7z"/>
|
||||
<path id="XMLID_258_" class="st0" d="M355.6,66L355.6,66c-3-2.5-7.1-3.8-12.3-3.8c-3.3,0-6.3,0.7-9.1,2.1
|
||||
c-2.6,1.3-5.1,3.5-6.7,6.3l0.1,0.1l6.3,6c2.6-4.1,5.5-5.6,9.3-5.6c2.1,0,3.8,0.6,5.1,1.6c1.3,1.1,1.9,2.5,1.9,4.2v1.9
|
||||
c-2.4-0.7-4.9-1.1-7.2-1.1c-4.9,0-8.9,1.2-11.8,3.4c-3,2.3-4.5,5.6-4.5,9.8c0,3.7,1.3,6.7,3.8,8.9c2.6,2.1,5.8,3.2,9.5,3.2
|
||||
c3.7,0,7.3-1.5,10.4-4.1v3.2h9.7V77C360.2,72.2,358.7,68.5,355.6,66z M338.2,87.2c1.1-0.8,2.7-1.2,4.7-1.2
|
||||
c2.4,0,4.9,0.5,7.5,1.4v3.8c-2.1,2-5,3-8.5,3c-1.7,0-3-0.4-3.9-1.1c-0.9-0.7-1.3-1.7-1.3-2.8C336.6,89,337.1,88,338.2,87.2z"/>
|
||||
<path id="XMLID_255_" class="st0" d="M413.6,103c-15.8,0-28.6-12.8-28.6-28.6s12.8-28.6,28.6-28.6s28.6,12.8,28.6,28.6
|
||||
S429.4,103,413.6,103z M413.6,55.8c-10.2,0-18.5,8.3-18.5,18.5s8.3,18.5,18.5,18.5s18.5-8.3,18.5-18.5S423.8,55.8,413.6,55.8z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.7 KiB |
377
client/public/locales/am/builder.json
Normal file
377
client/public/locales/am/builder.json
Normal file
@ -0,0 +1,377 @@
|
||||
{
|
||||
"common": {
|
||||
"actions": {
|
||||
"add": "አዲስ {{token}} ጨምር",
|
||||
"delete": "{{token}} አጥፋ",
|
||||
"duplicate": "የተባዛ ክፍል",
|
||||
"edit": "{{token}} አዘምን"
|
||||
},
|
||||
"columns": {
|
||||
"heading": "አምዶች",
|
||||
"tooltip": "የአምዶች ብዛት ይቀይሩ"
|
||||
},
|
||||
"form": {
|
||||
"date": {
|
||||
"label": "ቀን"
|
||||
},
|
||||
"description": {
|
||||
"label": "መግለጫ"
|
||||
},
|
||||
"email": {
|
||||
"label": "የኢሜል አድራሻ"
|
||||
},
|
||||
"end-date": {
|
||||
"help-text": "አሁንም ካለ፤ ይህንን ቦታ ክፍት ይተውት።",
|
||||
"label": "የመጨረሻ ቀን"
|
||||
},
|
||||
"keywords": {
|
||||
"label": "ቁልፍ ቃላት"
|
||||
},
|
||||
"level": {
|
||||
"label": "ደረጃ"
|
||||
},
|
||||
"levelNum": {
|
||||
"label": "ደረጃ (ቁጥር)"
|
||||
},
|
||||
"name": {
|
||||
"label": "ስም"
|
||||
},
|
||||
"phone": {
|
||||
"label": "የስልክ ቁጥር"
|
||||
},
|
||||
"position": {
|
||||
"label": "የስራ ሚና"
|
||||
},
|
||||
"start-date": {
|
||||
"label": "የመጀመሪያ ቀን"
|
||||
},
|
||||
"subtitle": {
|
||||
"label": "ንዑስ ርዕስ"
|
||||
},
|
||||
"summary": {
|
||||
"label": "ማጠቃለያ"
|
||||
},
|
||||
"title": {
|
||||
"label": "ርዕስ"
|
||||
},
|
||||
"url": {
|
||||
"label": "ድህረገፅ"
|
||||
}
|
||||
},
|
||||
"glossary": {
|
||||
"page": "ገጽ"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"delete": "አጥፋ",
|
||||
"duplicate": "አባዛ",
|
||||
"edit": "አዘምን"
|
||||
},
|
||||
"empty-text": "ይህ ዝርዝር ባዶ ነው።"
|
||||
},
|
||||
"tooltip": {
|
||||
"delete-item": "እርግጠኛ ነዎት ይህንn ማጥፋት ይፈልጋሉ? ይህ የማይመለስ ተግባር ነው።",
|
||||
"delete-section": "ክፍሉን አጥፋ",
|
||||
"rename-section": "ክፍሉን እንደገና ይሰይሙ",
|
||||
"toggle-visibility": "ዕይታውን ቀያይር"
|
||||
}
|
||||
},
|
||||
"controller": {
|
||||
"tooltip": {
|
||||
"center-artboard": "መሃል የሰዕል ማሳያ",
|
||||
"copy-link": "የስራ ልምድ ሰነዱን ሊንክ ቅዳ",
|
||||
"export-pdf": "PDF አውጣ",
|
||||
"redo": "ድገም",
|
||||
"toggle-orientation": "የገጽ አቀማመጥን ቀያይር",
|
||||
"toggle-page-break-line": "የገጽ መግቻ መስመርን ቀያይር",
|
||||
"toggle-sidebars": "የጎን ክፍሎችን ቀይር",
|
||||
"undo": "ቀልብስ",
|
||||
"zoom-in": "አቅርብ",
|
||||
"zoom-out": "አሳንስ"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
"menu": {
|
||||
"delete": "አጥፋ",
|
||||
"duplicate": "አብዛ",
|
||||
"rename": "ዳግም ሰይም",
|
||||
"share-link": "ሊንክ አጋራ",
|
||||
"tooltips": {
|
||||
"delete": "እርግጠኛ ነዎት ይህን የስራ ልምድ ሰነድ ማጥፋት ይፈልጋሉ? ይህ የማይመለስ ተግባር ነው።",
|
||||
"share-link": "የሥራ ልምድዎን ለሌሎች እንዲታይ ለማድረግ ዕይታውን ወደ ይፋዊ መለወጥ ያስፈልግዎታል።"
|
||||
}
|
||||
}
|
||||
},
|
||||
"leftSidebar": {
|
||||
"sections": {
|
||||
"awards": {
|
||||
"form": {
|
||||
"awarder": {
|
||||
"label": "ሸላሚ"
|
||||
}
|
||||
}
|
||||
},
|
||||
"basics": {
|
||||
"actions": {
|
||||
"photo-filters": "የፎቶ ማጣሪያዎች"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "የትውልድ ቀን"
|
||||
},
|
||||
"heading": "መሰረታዊ ነገሮች",
|
||||
"headline": {
|
||||
"label": "ርዕስ"
|
||||
},
|
||||
"name": {
|
||||
"label": "ሙሉ ስም"
|
||||
},
|
||||
"photo-filters": {
|
||||
"effects": {
|
||||
"border": {
|
||||
"label": "ድንበር"
|
||||
},
|
||||
"grayscale": {
|
||||
"label": "ግራጫ ልኬት"
|
||||
},
|
||||
"heading": "ተፅዕኖዎች"
|
||||
},
|
||||
"shape": {
|
||||
"heading": "ቅርጽ"
|
||||
},
|
||||
"size": {
|
||||
"heading": "መጠን (በፒክስል)"
|
||||
}
|
||||
},
|
||||
"photo-upload": {
|
||||
"tooltip": {
|
||||
"remove": "ፎቶ አስወግድ",
|
||||
"upload": "ፎቶ ስቀል"
|
||||
}
|
||||
}
|
||||
},
|
||||
"certifications": {
|
||||
"form": {
|
||||
"issuer": {
|
||||
"label": "ሰጪ አካል"
|
||||
}
|
||||
}
|
||||
},
|
||||
"education": {
|
||||
"form": {
|
||||
"area-study": {
|
||||
"label": "የጥናት ዙሪያ"
|
||||
},
|
||||
"courses": {
|
||||
"label": "ትምህርቶች"
|
||||
},
|
||||
"degree": {
|
||||
"label": "ዲግሪ"
|
||||
},
|
||||
"grade": {
|
||||
"label": "ውጤት"
|
||||
},
|
||||
"institution": {
|
||||
"label": "ተቋም"
|
||||
}
|
||||
}
|
||||
},
|
||||
"experience": {
|
||||
"form": {
|
||||
"name": {
|
||||
"label": "Company Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"address": {
|
||||
"label": "አድራሻ"
|
||||
},
|
||||
"city": {
|
||||
"label": "ከተማ"
|
||||
},
|
||||
"country": {
|
||||
"label": "ሀገር"
|
||||
},
|
||||
"heading": "አካባቢ",
|
||||
"postal-code": {
|
||||
"label": "የፖስታ ኮድ"
|
||||
},
|
||||
"region": {
|
||||
"label": "ክልል"
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"form": {
|
||||
"network": {
|
||||
"label": "አውታረ መረብ"
|
||||
},
|
||||
"username": {
|
||||
"label": "የተጠቃሚ ስም"
|
||||
}
|
||||
},
|
||||
"heading": "መለያዎች",
|
||||
"heading_one": "መለያ"
|
||||
},
|
||||
"publications": {
|
||||
"form": {
|
||||
"publisher": {
|
||||
"label": "አታሚ"
|
||||
}
|
||||
}
|
||||
},
|
||||
"references": {
|
||||
"form": {
|
||||
"relationship": {
|
||||
"label": "ዝምድና"
|
||||
}
|
||||
}
|
||||
},
|
||||
"section": {
|
||||
"heading": "ክፍል"
|
||||
},
|
||||
"volunteer": {
|
||||
"form": {
|
||||
"organization": {
|
||||
"label": "ድርጅት"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rightSidebar": {
|
||||
"sections": {
|
||||
"css": {
|
||||
"heading": "የራስ CSS"
|
||||
},
|
||||
"export": {
|
||||
"heading": "አውጣ",
|
||||
"json": {
|
||||
"primary": "JSON",
|
||||
"secondary": "ወደ Reactive Resume ተመልሶ ሊመጣ የሚችል የስራ ልምድ ሰነድ የJSON ቅጂዎትን ያውርዱ።"
|
||||
},
|
||||
"pdf": {
|
||||
"loading": {
|
||||
"primary": "PDF በመስራት ላይ",
|
||||
"secondary": "እባክዎ PDF እስኪሰራ ይጠብቁ፣ ይህ እስከ 15 ሰከንድ ሊወስድ ይችላል።"
|
||||
},
|
||||
"normal": {
|
||||
"primary": "PDF",
|
||||
"secondary": "ማተም እና መላክ የሚችሉትን የስራ ልምድ ሰነድዎን በPDF ያውርዱ። ይህ ፋይል ለተጨማሪ አርትዖት ተመልሶ ሊመጣ አይችልም።"
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"heading": "አቀማመጥ",
|
||||
"tooltip": {
|
||||
"reset-layout": "አቀማመጡን መልስ"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"bugs-features": {
|
||||
"body": "የስራ ልምድ ሰነድዎን ለመስራት የሚከለክሎት ነገር አለ? ወይም የሚጨምሩት አስደናቂ ሀሳብ አለዎት? በ GitHub ላይ ጉዳይዎን ያቅርቡ።",
|
||||
"button": "GitHub ጉዳዮች",
|
||||
"heading": "ችግሮች? የባህሪ ጥያቄዎች?"
|
||||
},
|
||||
"docs": "ሰነዶች",
|
||||
"donate": {
|
||||
"body": "Reactive Resumeን መጠቀም ከወደዱ እባክዎን መተግበሪያው እንዲሰራ እና ያለ ማስታወቂያ ሁሌም በነፃ እንዲቀጥል በተቻለዎት መጠን ለመለገስ ያስቡበት።",
|
||||
"button": "ቡና ይጋብዙኝ",
|
||||
"heading": "ለ Reactive Resume ይለግሱ"
|
||||
},
|
||||
"github": "የምንጭ ኮድ",
|
||||
"heading": "አገናኞች",
|
||||
"reddit": "Reddit"
|
||||
},
|
||||
"settings": {
|
||||
"global": {
|
||||
"date": {
|
||||
"primary": "ቀን",
|
||||
"secondary": "በመላው መተግበሪያ ላይ የሚጠቀሙበት የቀን አይነት"
|
||||
},
|
||||
"heading": "ዓለም አቀፍ",
|
||||
"language": {
|
||||
"primary": "ቋንቋ",
|
||||
"secondary": "በመላው መተግበሪያ ላይ የሚጠቀሙበት ቋንቋ"
|
||||
},
|
||||
"theme": {
|
||||
"primary": "ገጽታ"
|
||||
}
|
||||
},
|
||||
"heading": "ቅንብሮች",
|
||||
"page": {
|
||||
"break-line": {
|
||||
"primary": "መስመር መቁረጫ",
|
||||
"secondary": "የA4 ገጽ ቁመትን ለመለየት በሁሉም ገጾች ላይ መስመር አሳይ"
|
||||
},
|
||||
"format": {
|
||||
"primary": "የወረቀት መጠን",
|
||||
"secondary": "ከቆመበት ቀጥል ገጾችዎ ልኬቶችን ይወስናል"
|
||||
},
|
||||
"heading": "ገጽ",
|
||||
"orientation": {
|
||||
"disabled": "አንድ ገጽ ብቻ ሲኖር ምንም ተጽእኖ የለውም",
|
||||
"primary": "አቅጣጫ",
|
||||
"secondary": "ገጾችን በአግድም ሆነ በቋሚ ለማሳየት"
|
||||
}
|
||||
},
|
||||
"resume": {
|
||||
"heading": "የስራልምድ ሰነድ",
|
||||
"reset": {
|
||||
"primary": "ሁሉንም ነገር ዳግም አስጀምር",
|
||||
"secondary": "በጣም ብዙ ስህተቶችን ሰርተዋል? ሁሉንም ለውጦች ዳግም ለማስጀመር እና ከባዶ ለመጀመር እዚህ ይንኩ። ይጠንቀቁ፤ ይህ እርምጃ ወደ ኋላ መመለስ አይቻልም።"
|
||||
},
|
||||
"sample": {
|
||||
"primary": "የናሙና መረጃን ጫን",
|
||||
"secondary": "የት መጀመር እንዳለብዎት እርግጠኛ አይደሉም? የተሟላ የስራ ልምድ ሰነድ እንዴት እንደሚመስል ለማየት አንዳንድ ናሙና መረጃ ለመጫን እዚህ ይንኩ።"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sharing": {
|
||||
"heading": "ማጋራት",
|
||||
"short-url": {
|
||||
"label": "አጭር ማስፈንጠሪያ ይመርጣሉ።"
|
||||
},
|
||||
"visibility": {
|
||||
"subtitle": "ማስፈንጥሪያ ያለው ማንኛውም ሰው የእርስዎን የስራ ልምድ እንዲመለከት ይፍቀዱለት",
|
||||
"title": "ይፋዊ"
|
||||
}
|
||||
},
|
||||
"templates": {
|
||||
"heading": "ምሳሌዎች"
|
||||
},
|
||||
"theme": {
|
||||
"form": {
|
||||
"background": {
|
||||
"label": "ዳራ"
|
||||
},
|
||||
"primary": {
|
||||
"label": "ዋና"
|
||||
},
|
||||
"text": {
|
||||
"label": "ጽሑፍ"
|
||||
}
|
||||
},
|
||||
"heading": "ገጽታ"
|
||||
},
|
||||
"typography": {
|
||||
"form": {
|
||||
"font-family": {
|
||||
"label": "የጽሁፍ ቅርጽ ቤተሰብ"
|
||||
},
|
||||
"font-size": {
|
||||
"label": "የጽሁፍ ቅርጽ መጠን"
|
||||
}
|
||||
},
|
||||
"heading": "የፊደል አጻጻፍ",
|
||||
"widgets": {
|
||||
"body": {
|
||||
"label": "ሐተታ"
|
||||
},
|
||||
"headings": {
|
||||
"label": "ርዕሶች"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
client/public/locales/am/common.json
Normal file
29
client/public/locales/am/common.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"avatar": {
|
||||
"menu": {
|
||||
"greeting": "ሰላም",
|
||||
"logout": "ውጣ"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"credit": "ተወዳጅ ፕሮጀክት በ<1>Amruth Pillai</1>",
|
||||
"license": "በማህበረሰቡ ፣ ለማህበረሰቡ።"
|
||||
},
|
||||
"markdown": {
|
||||
"help-text": "ይህ ክፍል ይደግፋል <1>markdown</1> አፃፃፍ ይደግፋል።"
|
||||
},
|
||||
"date": {
|
||||
"present": "አሁን"
|
||||
},
|
||||
"subtitle": "ነፃ እና ክፍት የስራ ልምድ ሰነድ መገንቢያ",
|
||||
"title": "Reactive Resume",
|
||||
"toast": {
|
||||
"error": {
|
||||
"upload-file-size": "እባክዎ ከ2 ሜጋባይት በታች የሆኑ ፋይሎችን ብቻ ይስቀሉ።",
|
||||
"upload-photo-size": "እባክዎትን ከ2 ሜጋባይት በታች የሆኑ ፎቶዎችን ብቻ ይስቀሉ፣ ቢቻል ካሬ።"
|
||||
},
|
||||
"success": {
|
||||
"resume-link-copied": "ወደ የስራ ታሪክዎ የሚወስድ አገናኝ በሰሌዳዎ ተይዟል።"
|
||||
}
|
||||
}
|
||||
}
|
||||
25
client/public/locales/am/dashboard.json
Normal file
25
client/public/locales/am/dashboard.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"create-resume": {
|
||||
"subtitle": "ከባዶ ጀምር",
|
||||
"title": "አዲስ የሥራ ታሪክ ፍጠር"
|
||||
},
|
||||
"import-external": {
|
||||
"subtitle": "LinkedIn, JSON Resume, Reactive Resume",
|
||||
"title": "ከውጭ ምንጮች አስገባ"
|
||||
},
|
||||
"resume": {
|
||||
"menu": {
|
||||
"delete": "አጥፋ",
|
||||
"duplicate": "አብዛ",
|
||||
"open": "ክፈት",
|
||||
"rename": "እንደገና ይሰይሙ",
|
||||
"share-link": "ሊንክ አጋራ",
|
||||
"tooltips": {
|
||||
"delete": "እርግጠኛ ነዎት ይህን የሥራ ታሪክ ማጥፋት ይፈልጋሉ? ይህ የማይመለስ ተግባር ነው።",
|
||||
"share-link": "የስራ ልምድዎን ለሌሎች እንዲታይ ለማድረግ ዕይታውን ወደ ይፋዊ መቀየር አለብዎት።"
|
||||
}
|
||||
},
|
||||
"timestamp": "መጨረሻ የተሻሻለው {{timestamp}} በፊት"
|
||||
},
|
||||
"title": "ዳሽቦርድ"
|
||||
}
|
||||
43
client/public/locales/am/landing.json
Normal file
43
client/public/locales/am/landing.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"actions": {
|
||||
"app": "ወደ መተግበሪያ ይሂዱ",
|
||||
"login": "ግባ",
|
||||
"logout": "ውጣ",
|
||||
"register": "ይመዝገቡ"
|
||||
},
|
||||
"features": {
|
||||
"heading": "መገለጫዎች",
|
||||
"list": {
|
||||
"ads": "ምንም ማስታወቂያ የለም",
|
||||
"export": "የስራ ልምድዎን ወደ JSON ወይም PDF ቅርጸት ይላኩ።",
|
||||
"free": "ሁሌም ነጻ",
|
||||
"import": "መረጃ ከ LinkedIn, JSON Resume ማምጣት",
|
||||
"languages": "በተለያዩ ቋንቋዎች ተደራሽ",
|
||||
"more": "እና ብዙ ተጨማሪ አስደሳች መገለጫዎች፤ <1>ሁሉንም እዚህ ያንብቡ</1>",
|
||||
"tracking": "ምንም የተጠቃሚ መከታተያ የለም።"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"heading": "አገናኞች",
|
||||
"links": {
|
||||
"docs": "ሰነዶች",
|
||||
"donate": "ይለግሱ",
|
||||
"github": "የምንጭ ኮድ",
|
||||
"privacy": "የግላዊነት መመሪያ",
|
||||
"reddit": "Reddit",
|
||||
"service": "የአገልግሎት ውሎች"
|
||||
}
|
||||
},
|
||||
"screenshots": {
|
||||
"heading": "የገጽ እይታዎች"
|
||||
},
|
||||
"summary": {
|
||||
"body": "Reactive Resume የእርስዎን የስራ ታሪክ የመፍጠር፣ የማዘመን እና የማጋራት መደበኛ ተግባራትን እንደ 1 2 3 ቀላል ለማድረግ የተሰራ ነፃ እና በነጻ የሚገኝ የስራ ልምድ ሰነድ መገንቢያ ነው። በዚህ መገልገያ የተለያዩ የስራ ልምድ ሰነዶችን በመስራት፣ ከቀጣሪዎች ወይም ከጓደኞች ጋር በማስፈንጠሪያ ማጋራት እና እንደ PDF ማተም ፣ ሁሉንም በነጻ ፣ ምንም ማስታወቂያ ሳይኖር ፣ ምንም ክትትል ሳይደረግ ፣ የመረጃዎን ትክክለኛነት እና ግላዊነት ተጠብቆ ማከናወን ይችላሉ።",
|
||||
"heading": "ማጠቃለያ"
|
||||
},
|
||||
"testimonials": {
|
||||
"body": "ጥሩም ይሁን መጥፎ፣ ስለ Reactive Resume እና ለእርስዎ እንዴት እንደነበረ አስተያየትዎን መስማት እፈልጋለሁ።<br/>በአለም ዙሪያ በተጠቃሚዎች የተላኩ አንዳንድ መልዕክቶች እነዚሁና",
|
||||
"contact": "በዚህ በኩል ልታገኙኝ <1>ኢሜል</1> ትችላላችሁ ወይም <3>በድረ-ገጽ</3> ላይ ባለው የእውቂያ ቅጽ ማግኘት ይችላሉ።",
|
||||
"heading": "ምስክሮች"
|
||||
}
|
||||
}
|
||||
160
client/public/locales/am/modals.json
Normal file
160
client/public/locales/am/modals.json
Normal file
@ -0,0 +1,160 @@
|
||||
{
|
||||
"auth": {
|
||||
"forgot-password": {
|
||||
"actions": {
|
||||
"send-email": "የይለፍ ቃል ዳግም ማስጀመሪያ ኢሜይል ላክ"
|
||||
},
|
||||
"body": "መልሰው ማግኘት ከሚፈልጉት መለያ ጋር የተያያዘውን የኢሜይል አድራሻ ያስገቡ።",
|
||||
"form": {
|
||||
"email": {
|
||||
"label": "የኢሜል አድራሻ"
|
||||
}
|
||||
},
|
||||
"heading": "የይለፍ ቃልዎን ረሱ?",
|
||||
"help-text": "መለያው ካለ የይለፍ ቃልዎን እንደገና ለማስጀመር የሚያስችል ኢሜል ይደርስዎታል።"
|
||||
},
|
||||
"login": {
|
||||
"actions": {
|
||||
"login": "ግባ"
|
||||
},
|
||||
"body": "እባክዎ የስራ ልምድ ሰነድዎ ወዳለበት ለመግባት፣ ለማግኘት፣ ለማስተዳደር እና ለማጋራት ከመለያዎ ጋር የተያያዘውን የተጠቃሚ ስምዎን እና የይለፍ ቃልዎን ያስገቡ።",
|
||||
"form": {
|
||||
"password": {
|
||||
"label": "የይለፍ ቃል"
|
||||
},
|
||||
"username": {
|
||||
"help-text": "እንዲሁም የኢሜል አድራሻዎን ማስገባት ይችላሉ",
|
||||
"label": "የተጠቃሚ ስም"
|
||||
}
|
||||
},
|
||||
"heading": "ወደ መለያዎ ይግቡ",
|
||||
"recover-text": "የይለፍ ቃልዎን ከረሱት፣ እዚህ <1>መለያዎን መልሰው ማግኘት</1> ይችላሉ።",
|
||||
"register-text": "ከሌለዎት እዚህ <1>መለያ መፍጠር</1> ይችላሉ።"
|
||||
},
|
||||
"register": {
|
||||
"actions": {
|
||||
"register": "ይመዝገቡ",
|
||||
"google": "በጉግል ይመዝገቡ"
|
||||
},
|
||||
"body": "እባክዎ መለያ ለመፍጠር የእርስዎን የግል መረጃ ያስገቡ።",
|
||||
"form": {
|
||||
"confirm-password": {
|
||||
"label": "የይለፍ ቃልዎን አረጋግጥ"
|
||||
},
|
||||
"email": {
|
||||
"label": "የኢሜል አድራሻ"
|
||||
},
|
||||
"name": {
|
||||
"label": "ሙሉ ስም"
|
||||
},
|
||||
"password": {
|
||||
"label": "የይለፍ ቃል"
|
||||
},
|
||||
"username": {
|
||||
"label": "የተጠቃሚ ስም"
|
||||
}
|
||||
},
|
||||
"heading": "መለያ ፍጠር",
|
||||
"loginText": "መለያ ካለዎት <1>እዚህ መግባት</1> ይችላሉ።"
|
||||
},
|
||||
"reset-password": {
|
||||
"actions": {
|
||||
"set-password": "አዲስ የይለፍ ቃል ያዘጋጁ"
|
||||
},
|
||||
"body": "ለመለያዎ አዲስ የይለፍ ቃል ያስገቡ።",
|
||||
"form": {
|
||||
"confirm-password": {
|
||||
"label": "የይለፍ ቃልዎን አረጋግጡ"
|
||||
},
|
||||
"password": {
|
||||
"label": "የይለፍ ቃል"
|
||||
}
|
||||
},
|
||||
"heading": "የይለፍ ቃልዎን ዳግም ያስጀምሩ"
|
||||
},
|
||||
"profile": {
|
||||
"heading": "መለያህ",
|
||||
"form": {
|
||||
"avatar": {
|
||||
"help-text": "የመገለጫ ሥዕልህን በ<1>ግራቫታር ማዘመን ትችላለህ</1>"
|
||||
},
|
||||
"name": {
|
||||
"label": "ሙሉ ስም"
|
||||
},
|
||||
"email": {
|
||||
"label": "የ ኢሜል አድራሻ",
|
||||
"help-text": "በአሁኑ ጊዜ የኢሜል አድራሻዎን ማዘመን አይቻልም፣ እባክዎ ይልቁንስ አዲስ መለያ ይፍጠሩ።"
|
||||
}
|
||||
},
|
||||
"delete-account": {
|
||||
"heading": "መለያ እና ውሂብ ሰርዝ",
|
||||
"body": "መለያህን፣ ዳታህን እና ሁሉንም ከቆመበት ቀጥል ለመሰረዝ በጽሑፍ ሳጥኑ ውስጥ \"{{keyword}}\" ተይብ እና ቁልፉን ተጫን። ይህ የማይቀለበስ እርምጃ መሆኑን እና ውሂብዎን እንደገና ማምጣት እንደማይቻል እባክዎ ልብ ይበሉ።",
|
||||
"actions": {
|
||||
"delete": "መለያ ሰርዝ"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"save": "ለውጦችን አስቀምጥ"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"create-resume": {
|
||||
"actions": {
|
||||
"create-resume": "የስራ ታሪክ ሰነድ ይፍጠሩ"
|
||||
},
|
||||
"body": "ስም በመስጠት የስራ ልምድዎ ሰነድዎን መገንባት ይጀምሩ። መጠሪያው ለሚያመለክቱበት የስራ ሚና ወይም የሚወዱት ምግብ ሊሆን ይችላል።",
|
||||
"form": {
|
||||
"name": {
|
||||
"label": "ስም"
|
||||
},
|
||||
"public": {
|
||||
"label": "በይፋ ተደራሽ ነው?"
|
||||
},
|
||||
"slug": {
|
||||
"label": "ማስፈንጠሪያ"
|
||||
}
|
||||
},
|
||||
"heading": "አዲስ የስራ ታሪክ ሰነድ ይፍጠሩ"
|
||||
},
|
||||
"import-external": {
|
||||
"heading": "ከውጭ ምንጮች አስገባ",
|
||||
"json-resume": {
|
||||
"actions": {
|
||||
"upload-json": "JSON ስቀል"
|
||||
},
|
||||
"body": "ለመቀጠል ዝግጁ የሆነ <1>የተረጋገጠ JSON ሰነድ</1> ካለዎት በReactive Resume ላይ ስራዎን ለማፋጠን ሊጠቀሙበት ይችላሉ። ከታች ያለውን አዝራር በመጫን የሚሰራ JSON ፋይል ይስቀሉ።",
|
||||
"heading": "ከ JSON ሰነድ"
|
||||
},
|
||||
"linkedin": {
|
||||
"actions": {
|
||||
"upload-archive": "የ ZIP ማህደር ስቀል"
|
||||
},
|
||||
"body": "መረጃዎን ከ LinkedIn ወደ በመላክ እና Reactive Resume ላይ በራስ-ሙላ መስኮችን በመጠቀም ጊዜ መቆጠብ ይችላሉ። በ LinkedIn ላይ ወደ <1>የመረጃ ግላዊነት</1> ክፍል ይሂዱ እና የመረጃ ማህደርዎን ይጠይቁ። ከተገኘ በኋላ፣ ከታች በሚገኘው የ ZIP ፋይሉን ይስቀሉ።",
|
||||
"heading": "ከ LinkedIn ስቀል"
|
||||
},
|
||||
"reactive-resume": {
|
||||
"actions": {
|
||||
"upload-json": "JSON ስቀል",
|
||||
"upload-json-v2": "JSON v2 ይስቀሉ።"
|
||||
},
|
||||
"body": "አሁን ካለው Reactive Resume ስሪት ጋር ወደ ውጭ የተላከ JSON ካለዎት፣ እንደገና ሊስተካከል የሚችል ስሪት ለማግኘት ወደዚህ መልሰው ማስገባት ይችላሉ።",
|
||||
"heading": "ከ Reactive Resume ስቀል"
|
||||
}
|
||||
},
|
||||
"rename-resume": {
|
||||
"actions": {
|
||||
"rename-resume": "የስራ ታሪክ ሰነዱን ደግመው ይሰይሙ"
|
||||
},
|
||||
"form": {
|
||||
"name": {
|
||||
"label": "ስም"
|
||||
},
|
||||
"slug": {
|
||||
"label": "ማስፈንጥሪያ"
|
||||
}
|
||||
},
|
||||
"heading": "የስራ ታሪክ ሰነዱን ደግመው ይሰይሙ"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
"actions": {
|
||||
"add": "إضافة {{token}} جديدة",
|
||||
"delete": "حذف {{token}}",
|
||||
"duplicate": "قسم مكرر",
|
||||
"edit": "تحرير {{token}}"
|
||||
},
|
||||
"columns": {
|
||||
@ -80,13 +81,13 @@
|
||||
"center-artboard": "لوحة الوسط",
|
||||
"copy-link": "انسخ الرابط للسيرة الذاتية",
|
||||
"export-pdf": "تصدير PDF",
|
||||
"redo": "إعادة",
|
||||
"toggle-orientation": "تبديل اتجاه الصفحة",
|
||||
"toggle-page-break-line": "تبديل سطر الصفحة",
|
||||
"toggle-sidebars": "تبديل الشريط الجانبي",
|
||||
"zoom-in": "تكبير",
|
||||
"zoom-out": "تصغير",
|
||||
"undo": "الغاء التحميل",
|
||||
"redo": "إعادة"
|
||||
"zoom-in": "تكبير",
|
||||
"zoom-out": "تصغير"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
@ -114,6 +115,9 @@
|
||||
"actions": {
|
||||
"photo-filters": "فلاتر الصور"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "تاريخ الميلاد"
|
||||
},
|
||||
"heading": "الأساسيات",
|
||||
"headline": {
|
||||
"label": "العنوان الرئيسي"
|
||||
@ -121,9 +125,6 @@
|
||||
"name": {
|
||||
"label": "الاسم الكامل"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "تاريخ الميلاد"
|
||||
},
|
||||
"photo-filters": {
|
||||
"effects": {
|
||||
"border": {
|
||||
@ -174,6 +175,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"experience": {
|
||||
"form": {
|
||||
"name": {
|
||||
"label": "Company Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"address": {
|
||||
"label": "العنوان"
|
||||
@ -264,14 +272,15 @@
|
||||
"button": "صفحة المشاكل على \"GitHub\"",
|
||||
"heading": "خلل برمجي؟ طلب وظائف ومميزات مخصصة؟"
|
||||
},
|
||||
"docs": "توثيق",
|
||||
"donate": {
|
||||
"body": "إذا أعجبك استخدام Resctive Resume ، فالرجاء التفكير في التبرع بأكبر قدر ممكن من أجل الحفاظ على استمرار التطبيق وتشغيله ، بدون إعلانات وبشكل مجاني إلى الأبد.",
|
||||
"button": "إشتر لي قهوة",
|
||||
"heading": "تبرع الى Reactive Resume"
|
||||
},
|
||||
"github": "الشفرة المصدرية",
|
||||
"docs": "توثيق",
|
||||
"heading": "الروابط"
|
||||
"heading": "الروابط",
|
||||
"reddit": "رديت"
|
||||
},
|
||||
"settings": {
|
||||
"global": {
|
||||
@ -294,6 +303,10 @@
|
||||
"primary": "خط فاصل",
|
||||
"secondary": "اعرض خط في كل الصفحات لتحديد ارتفاع صفحة A4"
|
||||
},
|
||||
"format": {
|
||||
"primary": "حجم الورق",
|
||||
"secondary": "تحدد أبعاد صفحات سيرتك الذاتية"
|
||||
},
|
||||
"heading": "صفحة",
|
||||
"orientation": {
|
||||
"disabled": "ليس له تأثير عندما تكون هناك صفحة واحدة فقط",
|
||||
|
||||
@ -20,23 +20,24 @@
|
||||
"links": {
|
||||
"heading": "الروابط",
|
||||
"links": {
|
||||
"docs": "توثيق",
|
||||
"donate": "تبرّع",
|
||||
"github": "الشفرة المصدرية",
|
||||
"docs": "توثيق",
|
||||
"privacy": "سياسة الخصوصية",
|
||||
"reddit": "رديت",
|
||||
"service": "شروط الإستخدام"
|
||||
}
|
||||
},
|
||||
"screenshots": {
|
||||
"heading": "لقطات الشاشة"
|
||||
},
|
||||
"testimonials": {
|
||||
"heading": "الآراء والتقييمات",
|
||||
"body": "سواء أكان ذلك جيدًا أم سيئًا ، أود أن أسمع رأيك في \"السيرة الذاتية التفاعلية\" وكيف كانت التجربة بالنسبة لك.<br/>فيما يلي بعض الرسائل المرسلة بواسطة مستخدمين من جميع أنحاء العالم.",
|
||||
"contact": "يمكنك التواصل معي من خلال <1> بريدي الإلكتروني</1> أو من خلال نموذج الاتصال الموجود على <3> موقع الويب الخاص بي</3>."
|
||||
},
|
||||
"summary": {
|
||||
"body": "Reactive Resume هو منشئ سيرة ذاتية مجاني ومفتوح المصدر تم إنشاؤه لجعل المهام الروتينية لإنشاء وتحديث ومشاركة سيرتك الذاتية سهلة مثل 1 و 2 و 3. باستخدام هذا التطبيق ، يمكنك إنشاء سيرة ذاتية متعددة ومشاركتها مع جهات التوظيف أو الأصدقاء من خلال رابط فريد وطباعته كملف PDF ، كل ذلك مجانًا ، بدون إعلانات ، بدون تتبع ، دون فقدان سلامة وخصوصية بياناتك.",
|
||||
"heading": "الملخص"
|
||||
},
|
||||
"testimonials": {
|
||||
"body": "سواء أكان ذلك جيدًا أم سيئًا ، أود أن أسمع رأيك في \"السيرة الذاتية التفاعلية\" وكيف كانت التجربة بالنسبة لك.<br/>فيما يلي بعض الرسائل المرسلة بواسطة مستخدمين من جميع أنحاء العالم.",
|
||||
"contact": "يمكنك التواصل معي من خلال <1> بريدي الإلكتروني</1> أو من خلال نموذج الاتصال الموجود على <3> موقع الويب الخاص بي</3>.",
|
||||
"heading": "الآراء والتقييمات"
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,6 +71,31 @@
|
||||
}
|
||||
},
|
||||
"heading": "إعادة تعيين كلمة المرور الخاصة بك"
|
||||
},
|
||||
"profile": {
|
||||
"heading": "الحساب الخاص بك",
|
||||
"form": {
|
||||
"avatar": {
|
||||
"help-text": "يمكنك تحديث صورة ملفك الشخصي على <1> Gravatar</1>"
|
||||
},
|
||||
"name": {
|
||||
"label": "الاسم الكامل"
|
||||
},
|
||||
"email": {
|
||||
"label": "عنوان البريد الإلكتروني",
|
||||
"help-text": "لا يمكن تحديث عنوان بريدك الإلكتروني في الوقت الحالي ، يرجى إنشاء حساب جديد بدلاً من ذلك."
|
||||
}
|
||||
},
|
||||
"delete-account": {
|
||||
"heading": "حذف الحساب والبيانات",
|
||||
"body": "لحذف حسابك وبياناتك وجميع سيرتك الذاتية ، اكتب \"{{keyword}}\" في مربع النص وانقر على الزر. يرجى ملاحظة أن هذا إجراء لا رجوع فيه ولا يمكن استرداد بياناتك مرة أخرى.",
|
||||
"actions": {
|
||||
"delete": "حذف الحساب"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"save": "حفظ التغييرات"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"actions": {
|
||||
"add": "Добави нов {{token}}",
|
||||
"delete": "Изтрий {{token}}",
|
||||
"duplicate": "Дублиран раздел",
|
||||
"edit": "Редакция на {{token}}"
|
||||
},
|
||||
"columns": {
|
||||
@ -80,13 +81,13 @@
|
||||
"center-artboard": "Централна табла",
|
||||
"copy-link": "Копирай линка в резюмето",
|
||||
"export-pdf": "Експорт в PDF",
|
||||
"redo": "Redo",
|
||||
"toggle-orientation": "Превключване на ориентацията на страницата",
|
||||
"toggle-page-break-line": "Линия за прекъсване на страницата",
|
||||
"toggle-sidebars": "Включване на страничната лента",
|
||||
"zoom-in": "Увеличи",
|
||||
"zoom-out": "Намали",
|
||||
"undo": "Отмяна на",
|
||||
"redo": "Redo"
|
||||
"zoom-in": "Увеличи",
|
||||
"zoom-out": "Намали"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
@ -114,6 +115,9 @@
|
||||
"actions": {
|
||||
"photo-filters": "Филтри за снимата"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "Дата на раждане"
|
||||
},
|
||||
"heading": "Основни",
|
||||
"headline": {
|
||||
"label": "Заглавие"
|
||||
@ -121,9 +125,6 @@
|
||||
"name": {
|
||||
"label": "Пълно име"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "Дата на раждане"
|
||||
},
|
||||
"photo-filters": {
|
||||
"effects": {
|
||||
"border": {
|
||||
@ -174,6 +175,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"experience": {
|
||||
"form": {
|
||||
"name": {
|
||||
"label": "Company Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"address": {
|
||||
"label": "Адрес"
|
||||
@ -264,14 +272,15 @@
|
||||
"button": "GitHub общност",
|
||||
"heading": "Бъгове? Искания за функции?"
|
||||
},
|
||||
"docs": "Документация",
|
||||
"donate": {
|
||||
"body": "Ако ви е харесало да използвате Reactive Resume, моля, помислете дали да не дарите колкото можете повече за поддръжка на приложението, без реклами и безплатно завинаги.",
|
||||
"button": "Почерпете ме с кафе",
|
||||
"heading": "Направи дарение и подкрепи Reactive Resume"
|
||||
},
|
||||
"github": "Програмен код",
|
||||
"docs": "Документация",
|
||||
"heading": "Връзки"
|
||||
"heading": "Връзки",
|
||||
"reddit": "Reddit"
|
||||
},
|
||||
"settings": {
|
||||
"global": {
|
||||
@ -294,6 +303,10 @@
|
||||
"primary": "Линия на прекъсване",
|
||||
"secondary": "Показване на линия на всички страници за обозначаване на височината на страница A4"
|
||||
},
|
||||
"format": {
|
||||
"primary": "Размер на хартията",
|
||||
"secondary": "Определя размерите на вашите страници с автобиография"
|
||||
},
|
||||
"heading": "Страница",
|
||||
"orientation": {
|
||||
"disabled": "Няма ефект, когато има само една страница",
|
||||
|
||||
@ -20,23 +20,24 @@
|
||||
"links": {
|
||||
"heading": "Връзки",
|
||||
"links": {
|
||||
"docs": "Документация",
|
||||
"donate": "Дарение",
|
||||
"github": "Програмен код",
|
||||
"docs": "Документация",
|
||||
"privacy": "Политика за поверителност",
|
||||
"reddit": "Reddit",
|
||||
"service": "Условия на ползване"
|
||||
}
|
||||
},
|
||||
"screenshots": {
|
||||
"heading": "Екранни снимки"
|
||||
},
|
||||
"testimonials": {
|
||||
"heading": "Препоръки",
|
||||
"body": "Позитивно или негативно, ще се радвам да чуя мнението ви за Reactive Resume и какъв е вашия опитът.<br/>Ето някои от съобщенията, изпратени от потребители по целия свят.",
|
||||
"contact": "Можете да се свържете с мен чрез <1>моя имейл</1> или чрез формата за контакт на <3>моя уебсайт</3> ."
|
||||
},
|
||||
"summary": {
|
||||
"body": "Reactive Resume е безплатен инструмент за създаване на автобиография/CV с отворен код, който е създаден, за да улесни обикновените задачи за създаване, актуализиране и споделяне на вашата автобиография като 1, 2, 3. С това приложение можете да създавате множество автобиографии, да ги споделяте директно със специалистите по подбор на персонал или приятели чрез уникална връзка, както и ги отпечатате като PDF. Всичко е безплатно, без реклами, без проследяване, без да губите целостта и поверителността на вашите данни.",
|
||||
"heading": "Обобщение"
|
||||
},
|
||||
"testimonials": {
|
||||
"body": "Позитивно или негативно, ще се радвам да чуя мнението ви за Reactive Resume и какъв е вашия опитът.<br/>Ето някои от съобщенията, изпратени от потребители по целия свят.",
|
||||
"contact": "Можете да се свържете с мен чрез <1>моя имейл</1> или чрез формата за контакт на <3>моя уебсайт</3> .",
|
||||
"heading": "Препоръки"
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,6 +71,31 @@
|
||||
}
|
||||
},
|
||||
"heading": "Нулиране на паролата"
|
||||
},
|
||||
"profile": {
|
||||
"heading": "Вашата сметка",
|
||||
"form": {
|
||||
"avatar": {
|
||||
"help-text": "Можете да актуализирате профилната си снимка в <1>Gravatar</1>"
|
||||
},
|
||||
"name": {
|
||||
"label": "Пълно име"
|
||||
},
|
||||
"email": {
|
||||
"label": "Имейл адрес",
|
||||
"help-text": "В момента не е възможно да актуализирате своя имейл адрес, моля, създайте нов акаунт вместо това."
|
||||
}
|
||||
},
|
||||
"delete-account": {
|
||||
"heading": "Изтриване на акаунт и данни",
|
||||
"body": "За да изтриете своя акаунт, вашите данни и всички ваши автобиографии, въведете \"{{keyword}}\" в текстовото поле и щракнете върху бутона. Моля, имайте предвид, че това е необратимо действие и вашите данни не могат да бъдат извлечени отново.",
|
||||
"actions": {
|
||||
"delete": "Изтриване на акаунт"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"save": "Запазите промените"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"actions": {
|
||||
"add": "নতুন {{token}} যোগ করুন",
|
||||
"delete": "{{token}} মুছুন৷",
|
||||
"duplicate": "সদৃশ বিভাগ",
|
||||
"edit": "{{token}} সম্পাদনা করুন"
|
||||
},
|
||||
"columns": {
|
||||
@ -80,13 +81,13 @@
|
||||
"center-artboard": "কেন্দ্র আর্টবোর্ড",
|
||||
"copy-link": "রিজিউমে লিঙ্ক কপি করুন",
|
||||
"export-pdf": "পিডিএফ আকারে পাঠান",
|
||||
"redo": "আবার করুন",
|
||||
"toggle-orientation": "পৃষ্ঠা ওরিয়েন্টেশন টগল করুন",
|
||||
"toggle-page-break-line": "পৃষ্ঠা বিরতি লাইন টগল করুন",
|
||||
"toggle-sidebars": "সাইডবার টগল করুন",
|
||||
"zoom-in": "বড় কর",
|
||||
"zoom-out": "ছোট করা",
|
||||
"undo": "পূর্বাবস্থায় ফেরান",
|
||||
"redo": "আবার করুন"
|
||||
"zoom-in": "বড় কর",
|
||||
"zoom-out": "ছোট করা"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
@ -114,6 +115,9 @@
|
||||
"actions": {
|
||||
"photo-filters": "ফটো ফিল্টার"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "জন্ম তারিখ"
|
||||
},
|
||||
"heading": "মৌলিক",
|
||||
"headline": {
|
||||
"label": "শিরোনাম"
|
||||
@ -121,9 +125,6 @@
|
||||
"name": {
|
||||
"label": "পূর্ণ নাম"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "জন্ম তারিখ"
|
||||
},
|
||||
"photo-filters": {
|
||||
"effects": {
|
||||
"border": {
|
||||
@ -174,6 +175,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"experience": {
|
||||
"form": {
|
||||
"name": {
|
||||
"label": "কোম্পানির নাম"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"address": {
|
||||
"label": "ঠিকানা"
|
||||
@ -264,14 +272,15 @@
|
||||
"button": "Github ইস্যু তালিকা",
|
||||
"heading": "বাগ? বৈশিষ্ট্য অনুরোধ?"
|
||||
},
|
||||
"docs": "ডকুমেন্টেশন",
|
||||
"donate": {
|
||||
"body": "আপনি যদি প্রতিক্রিয়াশীল জীবনবৃত্তান্ত ব্যবহার করতে পছন্দ করেন, অনুগ্রহ করে বিজ্ঞাপন ছাড়া এবং চিরতরে বিনামূল্যে অ্যাপটিকে চালু ও চালু রাখার জন্য যতটা সম্ভব দান করার কথা বিবেচনা করুন।",
|
||||
"button": "আমাকে একটা কফি কিনে দাও",
|
||||
"heading": "Reactive Resume -তে দান করুন"
|
||||
},
|
||||
"github": "সোর্স কোড",
|
||||
"docs": "ডকুমেন্টেশন",
|
||||
"heading": "লিঙ্ক"
|
||||
"heading": "লিঙ্ক",
|
||||
"reddit": "রেডডিট"
|
||||
},
|
||||
"settings": {
|
||||
"global": {
|
||||
@ -294,6 +303,10 @@
|
||||
"primary": "লাইন ভেঙ্গে ফেলুন",
|
||||
"secondary": "একটি A4 পৃষ্ঠার উচ্চতা চিহ্নিত করতে সমস্ত পৃষ্ঠায় একটি লাইন দেখান৷"
|
||||
},
|
||||
"format": {
|
||||
"primary": "কাগজের আকার",
|
||||
"secondary": "আপনার জীবনবৃত্তান্ত পৃষ্ঠাগুলির মাত্রা নির্ধারণ করে"
|
||||
},
|
||||
"heading": "পাতা",
|
||||
"orientation": {
|
||||
"disabled": "শুধুমাত্র একটি পৃষ্ঠা থাকলে কোন প্রভাব নেই",
|
||||
|
||||
@ -20,23 +20,24 @@
|
||||
"links": {
|
||||
"heading": "লিঙ্ক",
|
||||
"links": {
|
||||
"docs": "ডকুমেন্টেশন",
|
||||
"donate": "দান করুন",
|
||||
"github": "সোর্স কোড",
|
||||
"docs": "ডকুমেন্টেশন",
|
||||
"privacy": "গোপনীয়তা নীতি",
|
||||
"reddit": "রেডডিট",
|
||||
"service": "সেবা পাবার শর্ত"
|
||||
}
|
||||
},
|
||||
"screenshots": {
|
||||
"heading": "স্ক্রিনশট"
|
||||
},
|
||||
"testimonials": {
|
||||
"heading": "প্রশংসাপত্র",
|
||||
"body": "ভাল বা খারাপ, আমি প্রতিক্রিয়াশীল জীবনবৃত্তান্ত সম্পর্কে আপনার মতামত এবং আপনার জন্য অভিজ্ঞতা কেমন হয়েছে তা জানতে চাই।<br/>এখানে বিশ্বজুড়ে ব্যবহারকারীদের পাঠানো কিছু বার্তা রয়েছে৷",
|
||||
"contact": "আপনি <1>আমার ইমেল</1> বা <3>আমার ওয়েবসাইট</3>-এ যোগাযোগ ফর্মের মাধ্যমে আমার সাথে যোগাযোগ করতে পারেন।"
|
||||
},
|
||||
"summary": {
|
||||
"body": "Reactive Resume হল একটি বিনামূল্যের এবং ওপেন সোর্স জীবনবৃত্তান্ত নির্মাতা যা আপনার জীবনবৃত্তান্ত তৈরি, আপডেট এবং শেয়ার করার জাগতিক কাজগুলিকে 1, 2, 3 এর মতো সহজ করে তুলতে তৈরি করা হয়েছে৷ এই অ্যাপটির মাধ্যমে, আপনি একাধিক জীবনবৃত্তান্ত তৈরি করতে পারেন, নিয়োগকারীদের বা বন্ধুদের সাথে শেয়ার করতে পারেন৷ একটি অনন্য লিঙ্কের মাধ্যমে এবং আপনার ডেটার অখণ্ডতা এবং গোপনীয়তা হারানো ছাড়াই বিনামূল্যে, কোনও বিজ্ঞাপন, কোনও ট্র্যাকিং ছাড়াই একটি পিডিএফ হিসাবে মুদ্রণ করুন।",
|
||||
"heading": "সারসংক্ষেপ"
|
||||
},
|
||||
"testimonials": {
|
||||
"body": "ভাল বা খারাপ, আমি প্রতিক্রিয়াশীল জীবনবৃত্তান্ত সম্পর্কে আপনার মতামত এবং আপনার জন্য অভিজ্ঞতা কেমন হয়েছে তা জানতে চাই।<br/>এখানে বিশ্বজুড়ে ব্যবহারকারীদের পাঠানো কিছু বার্তা রয়েছে৷",
|
||||
"contact": "আপনি <1>আমার ইমেল</1> বা <3>আমার ওয়েবসাইট</3>-এ যোগাযোগ ফর্মের মাধ্যমে আমার সাথে যোগাযোগ করতে পারেন।",
|
||||
"heading": "প্রশংসাপত্র"
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,6 +71,31 @@
|
||||
}
|
||||
},
|
||||
"heading": "আপনার পাসওয়ার্ডটি রিসেট করুন"
|
||||
},
|
||||
"profile": {
|
||||
"heading": "আপনার অ্যাকাউন্ট",
|
||||
"form": {
|
||||
"avatar": {
|
||||
"help-text": "আপনি <1>Gravatar-এ আপনার প্রোফাইল ছবি আপডেট করতে পারেন</1>"
|
||||
},
|
||||
"name": {
|
||||
"label": "পুরো নাম"
|
||||
},
|
||||
"email": {
|
||||
"label": "ইমেইল ঠিকানা",
|
||||
"help-text": "এই মুহূর্তে আপনার ইমেল ঠিকানা আপডেট করা সম্ভব নয়, এর পরিবর্তে একটি নতুন অ্যাকাউন্ট তৈরি করুন।"
|
||||
}
|
||||
},
|
||||
"delete-account": {
|
||||
"heading": "অ্যাকাউন্ট এবং ডেটা মুছুন",
|
||||
"body": "আপনার অ্যাকাউন্ট, আপনার ডেটা এবং আপনার সমস্ত জীবনবৃত্তান্ত মুছে ফেলতে, পাঠ্যবক্সে \"{{keyword}}\" টাইপ করুন এবং বোতামে ক্লিক করুন৷ দয়া করে মনে রাখবেন এটি একটি অপরিবর্তনীয় ক্রিয়া এবং আপনার ডেটা আবার পুনরুদ্ধার করা যাবে না।",
|
||||
"actions": {
|
||||
"delete": "হিসাব মুছে ফেলা"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"save": "পরিবর্তনগুলোর সংরক্ষন"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"actions": {
|
||||
"add": "Afegeix nou {{token}}",
|
||||
"delete": "Suprimeix {{token}}",
|
||||
"duplicate": "Secció duplicada",
|
||||
"edit": "Edita {{token}}"
|
||||
},
|
||||
"columns": {
|
||||
@ -80,13 +81,13 @@
|
||||
"center-artboard": "Centrar tauler de dibuix",
|
||||
"copy-link": "Copia l'enllaç al currículum",
|
||||
"export-pdf": "Exporta com a PDF",
|
||||
"redo": "Refer",
|
||||
"toggle-orientation": "Commuta l'orientació de la pàgina",
|
||||
"toggle-page-break-line": "Commuta la línia de salt de pàgina",
|
||||
"toggle-sidebars": "Mostra o oculta la barra lateral",
|
||||
"zoom-in": "Amplia",
|
||||
"zoom-out": "Allunya",
|
||||
"undo": "Desfer",
|
||||
"redo": "Refer"
|
||||
"zoom-in": "Amplia",
|
||||
"zoom-out": "Allunya"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
@ -114,6 +115,9 @@
|
||||
"actions": {
|
||||
"photo-filters": "Filtres fotogràfics"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "Data de naixement"
|
||||
},
|
||||
"heading": "Bàsics",
|
||||
"headline": {
|
||||
"label": "Títol"
|
||||
@ -121,9 +125,6 @@
|
||||
"name": {
|
||||
"label": "Nom complet"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "Data de naixement"
|
||||
},
|
||||
"photo-filters": {
|
||||
"effects": {
|
||||
"border": {
|
||||
@ -174,6 +175,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"experience": {
|
||||
"form": {
|
||||
"name": {
|
||||
"label": "Company Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"address": {
|
||||
"label": "Adreça"
|
||||
@ -264,14 +272,15 @@
|
||||
"button": "Problemes de GitHub",
|
||||
"heading": "Errors? Sol·licituds de funcions?"
|
||||
},
|
||||
"docs": "Documentació",
|
||||
"donate": {
|
||||
"body": "Si us ha agradat utilitzar Reactive Resume, considereu donar tant com pugueu per mantenir l'aplicació en funcionament, sense anuncis i gratuïta per sempre.",
|
||||
"button": "Compra'm un cafè",
|
||||
"heading": "Dona a Reactive Curriculum vitae"
|
||||
},
|
||||
"github": "Codi font",
|
||||
"docs": "Documentació",
|
||||
"heading": "Enllaços"
|
||||
"heading": "Enllaços",
|
||||
"reddit": "Reddit"
|
||||
},
|
||||
"settings": {
|
||||
"global": {
|
||||
@ -294,6 +303,10 @@
|
||||
"primary": "Línia de trencament",
|
||||
"secondary": "Mostra una línia a totes les pàgines per marcar l'alçada d'una pàgina A4"
|
||||
},
|
||||
"format": {
|
||||
"primary": "Mida del paper",
|
||||
"secondary": "Determina les dimensions de les pàgines del vostre currículum"
|
||||
},
|
||||
"heading": "Pàgina",
|
||||
"orientation": {
|
||||
"disabled": "No té efecte quan només hi ha una pàgina",
|
||||
|
||||
@ -20,23 +20,24 @@
|
||||
"links": {
|
||||
"heading": "Enllaços",
|
||||
"links": {
|
||||
"docs": "Documentació",
|
||||
"donate": "Donar",
|
||||
"github": "Codi font",
|
||||
"docs": "Documentació",
|
||||
"privacy": "Política de privacitat",
|
||||
"reddit": "Reddit",
|
||||
"service": "Termes del servei"
|
||||
}
|
||||
},
|
||||
"screenshots": {
|
||||
"heading": "Captures de pantalla"
|
||||
},
|
||||
"testimonials": {
|
||||
"heading": "Testimonis",
|
||||
"body": "Bo o dolent, m'encantaria saber la teva opinió sobre Reactive Curriculum vitae i com ha estat l'experiència per a tu.<br/>Aquests són alguns dels missatges enviats pels usuaris de tot el món.",
|
||||
"contact": "Podeu posar-vos en contacte amb mi mitjançant <1>el meu correu electrònic</1> o mitjançant el formulari de contacte al <3>el meu lloc web</3> ."
|
||||
},
|
||||
"summary": {
|
||||
"body": "Reactive Resume és un creador de currículums gratuït i de codi obert que s'ha creat per fer que les tasques mundanes de crear, actualitzar i compartir el vostre currículum siguin tan fàcils com 1, 2, 3. Amb aquesta aplicació, podeu crear diversos currículums, compartir-los amb reclutadors o amics. mitjançant un enllaç únic i imprimiu-lo com a PDF, tot de franc, sense anuncis, sense seguiment, sense perdre la integritat i la privadesa de les vostres dades.",
|
||||
"heading": "Resum"
|
||||
},
|
||||
"testimonials": {
|
||||
"body": "Bo o dolent, m'encantaria saber la teva opinió sobre Reactive Curriculum vitae i com ha estat l'experiència per a tu.<br/>Aquests són alguns dels missatges enviats pels usuaris de tot el món.",
|
||||
"contact": "Podeu posar-vos en contacte amb mi mitjançant <1>el meu correu electrònic</1> o mitjançant el formulari de contacte al <3>el meu lloc web</3> .",
|
||||
"heading": "Testimonis"
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,6 +71,31 @@
|
||||
}
|
||||
},
|
||||
"heading": "Restablir la contrasenya"
|
||||
},
|
||||
"profile": {
|
||||
"heading": "El teu compte",
|
||||
"form": {
|
||||
"avatar": {
|
||||
"help-text": "Pots actualitzar la teva foto de perfil a <1>Gravatar</1>"
|
||||
},
|
||||
"name": {
|
||||
"label": "Nom complet"
|
||||
},
|
||||
"email": {
|
||||
"label": "Correu electrònic",
|
||||
"help-text": "No és possible actualitzar la vostra adreça de correu electrònic en aquest moment, si us plau, creeu un compte nou."
|
||||
}
|
||||
},
|
||||
"delete-account": {
|
||||
"heading": "Suprimeix el compte i les dades",
|
||||
"body": "Per eliminar el vostre compte, les vostres dades i tots els vostres currículums, escriviu \"{{keyword}}\" al quadre de text i feu clic al botó. Tingueu en compte que aquesta és una acció irreversible i que les vostres dades no es poden recuperar de nou.",
|
||||
"actions": {
|
||||
"delete": "Esborrar compte"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"save": "Guardar canvis"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"actions": {
|
||||
"add": "Přidat nový {{token}}",
|
||||
"delete": "Odstranit {{token}}",
|
||||
"duplicate": "Duplicitní sekce",
|
||||
"edit": "Upravit {{token}}"
|
||||
},
|
||||
"columns": {
|
||||
@ -80,13 +81,13 @@
|
||||
"center-artboard": "Vycentrovat Artboard",
|
||||
"copy-link": "Kopírovat odkaz na životopis",
|
||||
"export-pdf": "Exportovat PDF",
|
||||
"redo": "Přepracovat",
|
||||
"toggle-orientation": "Přepnout orientaci stránky",
|
||||
"toggle-page-break-line": "Přepnout řádek zalomení stránky",
|
||||
"toggle-sidebars": "Přepnout boční panely",
|
||||
"zoom-in": "Přiblížit",
|
||||
"zoom-out": "Oddálit",
|
||||
"undo": "Zrušit",
|
||||
"redo": "Přepracovat"
|
||||
"zoom-in": "Přiblížit",
|
||||
"zoom-out": "Oddálit"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
@ -114,6 +115,9 @@
|
||||
"actions": {
|
||||
"photo-filters": "Foto filtry"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "Datum narození"
|
||||
},
|
||||
"heading": "Základy",
|
||||
"headline": {
|
||||
"label": "Titulek"
|
||||
@ -121,9 +125,6 @@
|
||||
"name": {
|
||||
"label": "Celé jméno"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "Datum narození"
|
||||
},
|
||||
"photo-filters": {
|
||||
"effects": {
|
||||
"border": {
|
||||
@ -174,6 +175,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"experience": {
|
||||
"form": {
|
||||
"name": {
|
||||
"label": "Company Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"address": {
|
||||
"label": "Adresa"
|
||||
@ -264,14 +272,15 @@
|
||||
"button": "Hlášení chyb GitHub",
|
||||
"heading": "Chyby? Požadavky na funkce?"
|
||||
},
|
||||
"docs": "Dokumentace",
|
||||
"donate": {
|
||||
"body": "Pokud se vám líbilo používání Reactive Resume, zvažte prosím darování co největší částky na udržení aplikace v provozu, bez reklam a navždy zdarma.",
|
||||
"button": "Kupte mi kávu",
|
||||
"heading": "Přispějte na Reactive Resume"
|
||||
},
|
||||
"github": "Zdrojový kód",
|
||||
"docs": "Dokumentace",
|
||||
"heading": "Odkazy"
|
||||
"heading": "Odkazy",
|
||||
"reddit": "Reddit"
|
||||
},
|
||||
"settings": {
|
||||
"global": {
|
||||
@ -294,6 +303,10 @@
|
||||
"primary": "Nový řádek",
|
||||
"secondary": "Zobrazit čáru na všech stránkách pro označení výšky stránky A4"
|
||||
},
|
||||
"format": {
|
||||
"primary": "Velikost papíru",
|
||||
"secondary": "Určuje rozměry stránek vašeho životopisu"
|
||||
},
|
||||
"heading": "Stránka",
|
||||
"orientation": {
|
||||
"disabled": "Nemá žádný efekt, pokud existuje pouze jedna stránka",
|
||||
|
||||
@ -20,23 +20,24 @@
|
||||
"links": {
|
||||
"heading": "Odkazy",
|
||||
"links": {
|
||||
"docs": "Dokumentace",
|
||||
"donate": "Darovat",
|
||||
"github": "Zdrojový kód",
|
||||
"docs": "Dokumentace",
|
||||
"privacy": "Zásady ochrany osobních údajů",
|
||||
"reddit": "Reddit",
|
||||
"service": "Podmínky služby"
|
||||
}
|
||||
},
|
||||
"screenshots": {
|
||||
"heading": "Snímky obrazovky"
|
||||
},
|
||||
"testimonials": {
|
||||
"heading": "Posudky",
|
||||
"body": "Ať je to dobře nebo špatně, rád bych slyšel váš názor na Reactive Resume a jaké to bylo pro vás.<br/>Zde jsou některé zprávy zaslané uživateli z celého světa.",
|
||||
"contact": "Můžete mě kontaktovat prostřednictvím <1>mého e-mailu</1> nebo prostřednictvím kontaktního formuláře na <3>mých webových stránkách</3> ."
|
||||
},
|
||||
"summary": {
|
||||
"body": "Reactive Resume je bezplatný a open source tvůrce životopisů, který je vytvořen tak, aby zjednodušil každodenní úkoly vytváření, aktualizace a sdílení vašeho životopisu jako 1, 2, 3. Pomocí této aplikace můžete vytvořit více životopisů a sdílet je s náboráři nebo přáteli. prostřednictvím jedinečného odkazu a vytiskněte si jej jako PDF, vše zdarma, bez reklam, bez sledování, bez ztráty integrity a soukromí vašich dat.",
|
||||
"heading": "souhrn"
|
||||
},
|
||||
"testimonials": {
|
||||
"body": "Ať je to dobře nebo špatně, rád bych slyšel váš názor na Reactive Resume a jaké to bylo pro vás.<br/>Zde jsou některé zprávy zaslané uživateli z celého světa.",
|
||||
"contact": "Můžete mě kontaktovat prostřednictvím <1>mého e-mailu</1> nebo prostřednictvím kontaktního formuláře na <3>mých webových stránkách</3> .",
|
||||
"heading": "Posudky"
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,6 +71,31 @@
|
||||
}
|
||||
},
|
||||
"heading": "Obnovit heslo"
|
||||
},
|
||||
"profile": {
|
||||
"heading": "Váš účet",
|
||||
"form": {
|
||||
"avatar": {
|
||||
"help-text": "Svůj profilový obrázek si můžete aktualizovat na <1>Gravatar</1>"
|
||||
},
|
||||
"name": {
|
||||
"label": "Celé jméno"
|
||||
},
|
||||
"email": {
|
||||
"label": "Emailová adresa",
|
||||
"help-text": "V tuto chvíli není možné aktualizovat vaši e-mailovou adresu, vytvořte si místo toho nový účet."
|
||||
}
|
||||
},
|
||||
"delete-account": {
|
||||
"heading": "Smazat účet a data",
|
||||
"body": "Chcete-li smazat svůj účet, data a všechny své životopisy, zadejte do textového pole „{{keyword}}“ a klikněte na tlačítko. Upozorňujeme, že se jedná o nevratnou akci a vaše data nelze znovu získat.",
|
||||
"actions": {
|
||||
"delete": "Smazat účet"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"save": "Uložit změny"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"actions": {
|
||||
"add": "Tilføj ny {{token}}",
|
||||
"delete": "Slet {{token}}",
|
||||
"duplicate": "Dublet afsnit",
|
||||
"edit": "Rediger {{token}}"
|
||||
},
|
||||
"columns": {
|
||||
@ -80,13 +81,13 @@
|
||||
"center-artboard": "Centrer tegnebræt",
|
||||
"copy-link": "Kopier link til CV",
|
||||
"export-pdf": "Eksporter PDF",
|
||||
"redo": "Redo",
|
||||
"toggle-orientation": "Skift sideorientering",
|
||||
"toggle-page-break-line": "Skift sideskiftlinje",
|
||||
"toggle-sidebars": "Sidebjælke til/fra",
|
||||
"zoom-in": "Zoom ind",
|
||||
"zoom-out": "Zoom ud",
|
||||
"undo": "Fortryd",
|
||||
"redo": "Redo"
|
||||
"zoom-in": "Zoom ind",
|
||||
"zoom-out": "Zoom ud"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
@ -114,6 +115,9 @@
|
||||
"actions": {
|
||||
"photo-filters": "Fotofiltre"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "Fødselsdato"
|
||||
},
|
||||
"heading": "Grundlæggende",
|
||||
"headline": {
|
||||
"label": "Overskrift"
|
||||
@ -121,9 +125,6 @@
|
||||
"name": {
|
||||
"label": "Fulde navn"
|
||||
},
|
||||
"birthdate": {
|
||||
"label": "Fødselsdato"
|
||||
},
|
||||
"photo-filters": {
|
||||
"effects": {
|
||||
"border": {
|
||||
@ -174,6 +175,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"experience": {
|
||||
"form": {
|
||||
"name": {
|
||||
"label": "Company Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"address": {
|
||||
"label": "Adresse"
|
||||
@ -264,14 +272,15 @@
|
||||
"button": "GitHub-problemer",
|
||||
"heading": "Fejl? Ønsker til en ny funktion?"
|
||||
},
|
||||
"docs": "Dokumentation",
|
||||
"donate": {
|
||||
"body": "Hvis du kunne lide at bruge Reactive Resume, bedes du overveje at donere så meget som muligt til formålet med at holde appen oppe og køre, uden annoncer og gratis for evigt.",
|
||||
"button": "Købe mig en kop kaffe",
|
||||
"heading": "Donér til Reactive Resume"
|
||||
},
|
||||
"github": "Kildekode",
|
||||
"docs": "Dokumentation",
|
||||
"heading": "Links"
|
||||
"heading": "Links",
|
||||
"reddit": "Reddit"
|
||||
},
|
||||
"settings": {
|
||||
"global": {
|
||||
@ -294,6 +303,10 @@
|
||||
"primary": "Brudlinje",
|
||||
"secondary": "Vis en streg på alle sider for at markere højden på en A4-side"
|
||||
},
|
||||
"format": {
|
||||
"primary": "Papirstørrelse",
|
||||
"secondary": "Bestemmer dimensionerne på dine CV-sider"
|
||||
},
|
||||
"heading": "Side",
|
||||
"orientation": {
|
||||
"disabled": "Har ingen effekt, når der kun er én side",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user