mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-10 04:22:27 +10:00
Compare commits
715 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 4b2d9d7026 | |||
| 045145ed67 | |||
| ec27e5e6ab | |||
| 2faa15db5a | |||
| 2c2893d5fc | |||
| 19c7ebe8a4 | |||
| c24847ac0b | |||
| 7137694832 | |||
| 049de38da2 | |||
| 17019e446b | |||
| d73ee7b7f8 | |||
| 2c95dc2ac8 | |||
| e148dd3e82 | |||
| 0aa2d61c55 | |||
| 0b2c1ffd26 | |||
| a531e8cd89 | |||
| 152e386141 | |||
| 87189cd045 | |||
| 114b04a740 | |||
| 383cde53df | |||
| 9bf98d3c49 | |||
| e62f0a3f5e | |||
| 10fb7b143a | |||
| 67ba58e798 | |||
| 179cf99f83 | |||
| 81a51d487b | |||
| b41b50565a | |||
| 8cd073eb62 | |||
| f4f8502703 | |||
| 0d079d7b24 | |||
| 167f7c902f | |||
| 7c630df927 | |||
| b391c561e5 | |||
| 4dbe015fbf | |||
| bae35b2614 | |||
| 8b7719a198 | |||
| 39cf238de3 | |||
| 98855ae230 | |||
| ab92cbf21e | |||
| 388ab4e29a | |||
| bb18c59018 | |||
| 217ab6ab93 | |||
| 12690b33d7 | |||
| bff5173701 | |||
| 821813d90d | |||
| b1d3c4da5b | |||
| 39f962b440 | |||
| b1cfd4b7c8 | |||
| c98d4a6004 | |||
| a5ec1f8609 | |||
| b2c897660d | |||
| c1a7fe7354 | |||
| b628c4a21b | |||
| 5fb4935146 | |||
| ae5280435d | |||
| 6451609d8f | |||
| edfe79f580 | |||
| 5d7318d46d | |||
| 77428c1661 | |||
| a2e075df39 | |||
| 63af1d2b69 | |||
| 99c5016762 | |||
| 44ff6caf27 | |||
| 7d2981f7ce | |||
| fcc5dd4bad | |||
| a9fb995d39 | |||
| 31a85bfaa6 | |||
| 51151a601e | |||
| 9931b22313 | |||
| fdf6b76c21 | |||
| b4696301ed | |||
| 294d7b5dab | |||
| 0430920f56 | |||
| 5444b4f5ab | |||
| d649b7fc08 | |||
| 20b39c0b35 | |||
| 8b87b054ee | |||
| 5eb68e9e21 | |||
| ec2606d625 | |||
| 9055010f61 | |||
| 9763b5c270 | |||
| 75c3bfe9e5 | |||
| 7f39247655 | |||
| d6f11e7807 | |||
| 361a1e65d0 | |||
| 6fddbe0c59 | |||
| 3412711f27 | |||
| a4bfc17431 | |||
| 7c698ef9d2 | |||
| e929faf9b0 | |||
| e3ff18b6dd | |||
| 2734493ca4 | |||
| f0015143c6 | |||
| 8d97b195a0 | |||
| f30692196a | |||
| 242278edd1 | |||
| 162759c716 | |||
| f0c6bd16f5 | |||
| fac8a9d4ff | |||
| 9ff1ffb0b9 | |||
| 79d3ef1306 | |||
| f4a12285f5 | |||
| 120ad827ad | |||
| a129b2033f | |||
| 372e508936 | |||
| ce8ada2621 | |||
| d0563d2ec9 | |||
| dacd4e311c | |||
| d6d016ba5d | |||
| 407ac990ac | |||
| 895e9845fc | |||
| 859a44197d | |||
| 0b80f33d46 | |||
| f246a17038 | |||
| 10c13d54be | |||
| 66316b740b | |||
| ae94748abe | |||
| fe11be60d3 | |||
| 1d3e47adb2 | |||
| fdd5f373c4 | |||
| 0f99d6cdfb | |||
| 742865a66a | |||
| 12dcf04981 | |||
| c0d76eaf0e | |||
| 2f430a1d07 | |||
| 576b942027 | |||
| 7df777ad0c | |||
| e5e30f290a | |||
| dcb476c28b | |||
| e09f281461 | |||
| 293f008f0a | |||
| f9cd1c779f | |||
| d931590374 | |||
| 907ffacca0 | |||
| a263d54319 | |||
| 0d478e1286 | |||
| 08997a1728 | |||
| 688bb11844 | |||
| 0bf4e0b2ae | |||
| 60bbfb6703 | |||
| 706307b073 | |||
| 3be18636ff | |||
| b2ee2f9d09 | |||
| fe54a2388e | |||
| be170dd985 | |||
| 7fd26ad2c3 | |||
| ec30aff4d1 | |||
| 0b3023989b | |||
| 3b96348183 | |||
| 30080b23cd | |||
| b3ba1e5b56 | |||
| bf72b557ca | |||
| 344fcb1078 | |||
| ddd71567c1 | |||
| adc679a6e5 | |||
| 8f49536119 | |||
| 750fedbd74 | |||
| cd59ea7e9b | |||
| 5c1b44ddea | |||
| eb6450a9de | |||
| 0b620f41fc | |||
| 64e0e677d7 | |||
| 19ae1cf036 | |||
| c221cef77f | |||
| 77e3dc2b16 | |||
| 9c9368acd5 | |||
| 22b91d3f94 | |||
| d844092d0f | |||
| b3e118fb8b | |||
| fe37eb2791 | |||
| 7902f67f4f | |||
| 57dd110187 | |||
| 829375e87a | |||
| 0a15b4ebc9 | |||
| 2bff3fc20b | |||
| 1e997fe67c | |||
| dbf06455e4 | |||
| 42c7c9ade1 | |||
| 36c19bac3f | |||
| 44a9300aff | |||
| 610b5ba9d4 | |||
| 769e8811cd | |||
| 676fbcafe7 | |||
| 3935ae1e04 | |||
| ef6b765266 | |||
| 647dd6e682 | |||
| 43841e9962 | |||
| e2236c3207 | |||
| 7389d33ee5 | |||
| 4b21eabec9 | |||
| 1815b0fa21 | |||
| 6c4d3cbd56 | |||
| 8c2f3c8504 | |||
| 3aa7a98d9d | |||
| 5519ec898d | |||
| 1cd4c5d733 | |||
| 73d11c323f | |||
| 38812fcf25 | |||
| c22de12f12 | |||
| c94c971599 | |||
| c9a71a5917 | |||
| 8a29387470 | |||
| 592511b090 | |||
| af63fd38d4 | |||
| bf38b1b254 | |||
| 4a1c0079db | |||
| 5b6f6b7621 | |||
| 02587255fe | |||
| 9ef2a84ac2 | |||
| 77b1c5b536 | |||
| bf956fe18c | |||
| 4114f1e1dd | |||
| 668d39fa87 | |||
| 0d88a18757 | |||
| 0630369087 | |||
| 73af4a6859 | |||
| 99ddeb25a9 | |||
| 685aa06778 | |||
| 460abc6f1d | |||
| 04f02157ac | |||
| 828a4a8715 | |||
| 5b3141cd49 | |||
| 779d22101f | |||
| ef240b2110 | |||
| 32bb7354a4 | |||
| 0dcbad1f8a | |||
| a74921b27a | |||
| d4f47423c9 | |||
| 03f9a6543c | |||
| eb89cfcf5d | |||
| c52ef9ecb7 | |||
| c499abbb88 | |||
| 1a7ee88ecd | |||
| 16d19eb70f | |||
| 331346b99c | |||
| 95d265f672 | |||
| 315c7d6328 | |||
| 490e174564 | |||
| b5cde79f8b | |||
| d50f14bb78 | |||
| c13a751c1a | |||
| 5c37fc55d5 | |||
| 48a0f90597 | |||
| 05d3f1f06f | |||
| 4d43f6a642 | |||
| f7363ccdd7 | |||
| 07c91e9ac2 | |||
| cbe08f1d2c | |||
| c2617a8277 | |||
| fe72d2de41 | |||
| 23667e218f | |||
| 977fa72dde | |||
| 5197f954c0 | |||
| 58341e4cd2 | |||
| fc0b69796f | |||
| 1559703567 | |||
| 0a1fd50d07 | |||
| 1c19062c63 | |||
| 25cf594eb9 | |||
| 1c3beee6cd | |||
| 95c3d4c315 | |||
| 85df339e56 | |||
| d61ad44ebc | |||
| ccb1eff749 | |||
| bfb48e3aa7 | |||
| e2e08ad390 | |||
| f0dda06af3 | |||
| 4c4e77e21d | |||
| f364ae8929 | |||
| b52f292d89 | |||
| 8cac7f907c | |||
| a18a60679f | |||
| 5cc6a81b8c | |||
| 6ff212b698 | |||
| 56bcec5196 | |||
| 12019f90e9 | |||
| 7e6e69ed49 | |||
| a09a945e17 | |||
| df714dc8de | |||
| 28b63ef0c7 | |||
| 1b594dac61 | |||
| dd34a30ee0 | |||
| 0af398ceed | |||
| 04abd2cacc | |||
| a037a091e7 | |||
| f3a4c17cb4 | |||
| f06f7ad2e5 | |||
| aab2e5c8a9 | |||
| 4318dbe762 | |||
| ae3ff274ee | |||
| 164403c495 | |||
| 8595c92fb7 | |||
| 8f75f32f88 | |||
| 0d44189a5f | |||
| cd16a6d360 | |||
| 7b795bfaa4 | |||
| 8f78d47661 | |||
| 0b5e5a2ece | |||
| 9eade9514c | |||
| d744e06e96 | |||
| 9657c199d2 | |||
| 2dbe737b73 | |||
| f624699efa | |||
| e46f473754 | |||
| 767f4bf4bc | |||
| 1c5d025c15 | |||
| 8de8d89290 | |||
| 83662122a5 | |||
| 126482a760 | |||
| b04c22a27b | |||
| 63f88a3d1c | |||
| bd519db14f | |||
| a49aa42176 | |||
| 1a382db4d9 | |||
| c68f75dc8c | |||
| c12de0c013 | |||
| 4cafaf306a | |||
| 0238cf18a5 | |||
| 2f6072a7ba | |||
| 55dd2c5925 | |||
| a3e25f87fa | |||
| 9e82ea11c3 | |||
| 62fd63e41f | |||
| b91c175352 | |||
| 898e2314fc | |||
| bca2aa2fe5 | |||
| 427fdb717a | |||
| ee5b0187e2 | |||
| 94d05f33b4 | |||
| 35fe4e2774 | |||
| 317901a4d2 | |||
| 350ffcbc43 | |||
| 2c074a96c8 | |||
| 79f140b2d0 | |||
| 649c655ad5 | |||
| d5284a90d1 | |||
| bd18c53ab8 | |||
| 704c1ab7d4 | |||
| 1dbd7f221e | |||
| e1a47ffbe2 | |||
| 2add629970 | |||
| a48fcd9c97 | |||
| df7b00cb2c | |||
| 27fc939101 | |||
| 7c574d17e4 | |||
| 86a105f5a5 | |||
| 327bcc2b32 | |||
| a6cbd85010 | |||
| 371b820923 | |||
| 1d47fd0267 | |||
| 276fc95bb0 | |||
| 34c8861321 | |||
| 780b782579 | |||
| 9daa99fd5b | |||
| 76b3aa29cf | |||
| 25d4913fab | |||
| 0efeff3a4f | |||
| f56089925e | |||
| 5afae08f20 | |||
| 4bf114dfd6 | |||
| 23a3c2e624 | |||
| 71862f4354 | |||
| 6861c0f0fa | |||
| 9a18e74b90 | |||
| 4dd1b70079 | |||
| f9580fe716 | |||
| 3545f7939f | |||
| 9caad3bc0b | |||
| 5bdb92b1cf |
@ -1,5 +0,0 @@
|
||||
ARG VARIANT="lts-bullseye"
|
||||
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
|
||||
|
||||
RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
@ -1,27 +0,0 @@
|
||||
{
|
||||
"name": "Node.js",
|
||||
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"args": { "VARIANT": "16-bullseye" }
|
||||
},
|
||||
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "lokalise.i18n-ally"],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [80, 5432],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "pnpm install",
|
||||
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "node",
|
||||
|
||||
"features": {
|
||||
"docker-from-docker": "latest"
|
||||
}
|
||||
}
|
||||
@ -1,21 +1,26 @@
|
||||
# Android App
|
||||
/app
|
||||
|
||||
# Build Artifacts
|
||||
dist
|
||||
.next
|
||||
/server/dist
|
||||
/client/.next
|
||||
|
||||
# IDEs
|
||||
.vscode
|
||||
|
||||
# Project Metadata
|
||||
.crowdin.yml
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
SECURITY.md
|
||||
CHANGELOG.md
|
||||
CODE_OF_CONDUCT.md
|
||||
|
||||
# Project Dependencies
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
|
||||
# Android App
|
||||
/app
|
||||
19
.env.example
19
.env.example
@ -1,7 +1,7 @@
|
||||
# Server + Client
|
||||
TZ=UTC
|
||||
PUBLIC_URL=http://localhost:3000
|
||||
PUBLIC_SERVER_URL=http://localhost:3000/api
|
||||
PUBLIC_SERVER_URL=http://localhost:3100
|
||||
PUBLIC_GOOGLE_CLIENT_ID=
|
||||
|
||||
# Server + Database
|
||||
@ -11,23 +11,26 @@ POSTGRES_PASSWORD=postgres
|
||||
|
||||
# Server
|
||||
SECRET_KEY=
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_HOST=localhost
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_SSL_CERT=
|
||||
JWT_SECRET=
|
||||
JWT_EXPIRY_TIME=604800
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
GOOGLE_API_KEY=
|
||||
SENDGRID_API_KEY=
|
||||
SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID=
|
||||
SENDGRID_FROM_NAME=
|
||||
SENDGRID_FROM_EMAIL=
|
||||
MAIL_FROM_NAME=
|
||||
MAIL_FROM_EMAIL=
|
||||
MAIL_HOST=
|
||||
MAIL_PORT=
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
STORAGE_BUCKET=
|
||||
STORAGE_REGION=
|
||||
STORAGE_ENDPOINT=
|
||||
STORAGE_URL_PREFIX=
|
||||
STORAGE_ACCESS_KEY=
|
||||
STORAGE_SECRET_KEY=
|
||||
PDF_DELETION_TIME=345600000
|
||||
|
||||
# Flags (Client)
|
||||
PUBLIC_FLAG_DISABLE_SIGNUPS=false
|
||||
# Client
|
||||
PUBLIC_FLAG_DISABLE_SIGNUPS=false
|
||||
|
||||
@ -1,31 +1,23 @@
|
||||
{
|
||||
"root": true,
|
||||
"ignorePatterns": ["/app"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
|
||||
"plugins": ["@typescript-eslint/eslint-plugin", "simple-import-sort", "unused-imports"],
|
||||
"extends": ["plugin:@typescript-eslint/recommended"],
|
||||
"plugins": ["@typescript-eslint/eslint-plugin", "simple-import-sort"],
|
||||
"rules": {
|
||||
// TypeScript ESLint
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
// ESLint
|
||||
"no-unused-vars": "off",
|
||||
|
||||
// Simple Import Sort
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
// Unused Imports
|
||||
"no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "none",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
]
|
||||
|
||||
// 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"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1 +1,2 @@
|
||||
github: AmruthPillai
|
||||
custom: https://paypal.me/RajaRajanA
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/bug-report.md
vendored
17
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@ -1,36 +1,43 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Create a report to help improve
|
||||
title: "[BUG] "
|
||||
title: '[BUG] '
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**Product Flavor**
|
||||
|
||||
- [ ] Managed (https://rxresu.me)
|
||||
- [ ] Self Hosted
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
<!-- Steps to reproduce the behavior: -->
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Screenshots**
|
||||
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: <!--[e.g. iOS]-->
|
||||
- Browser <!--[e.g. chrome, safari]-->
|
||||
- Version <!--[e.g. 22]-->
|
||||
|
||||
- OS: <!--[e.g. iOS]-->
|
||||
- Browser <!--[e.g. chrome, safari]-->
|
||||
- Version <!--[e.g. 22]-->
|
||||
|
||||
**Additional context**
|
||||
|
||||
<!-- Add any other context about the problem here. -->
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/feature-request.md
vendored
7
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@ -1,20 +1,23 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEATURE] "
|
||||
title: '[FEATURE] '
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
|
||||
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
|
||||
|
||||
90
.github/workflows/docker-build-push.yml
vendored
90
.github/workflows/docker-build-push.yml
vendored
@ -1,94 +1,58 @@
|
||||
name: Build and Push Docker Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
client:
|
||||
name: Client
|
||||
build_matrix:
|
||||
name: Build and Push Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
image: [client, server]
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
uses: actions/checkout@v3.5.0
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
name: App Version
|
||||
uses: martinbeentjes/npm-get-version-action@v1.2.3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1.2.0
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
with:
|
||||
platforms: amd64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1.6.0
|
||||
- id: buildx
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Login to Docker
|
||||
uses: docker/login-action@v1.14.1
|
||||
- name: Login to Docker Hub
|
||||
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@v1.14.1
|
||||
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@v2.10.0
|
||||
- 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: linux/amd64
|
||||
file: ${{ matrix.image }}/Dockerfile
|
||||
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
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
|
||||
- id: version
|
||||
name: Get Version
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1.2.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1.6.0
|
||||
|
||||
- name: Login to Docker
|
||||
uses: docker/login-action@v1.14.1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1.14.1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: $GITHUB_REPOSITORY_OWNER
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Build and Push Server Image
|
||||
uses: docker/build-push-action@v2.10.0
|
||||
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 }}
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,10 +1,14 @@
|
||||
# Environment Variables
|
||||
.env
|
||||
.env.*
|
||||
!.env.gitpod
|
||||
!.env.example
|
||||
|
||||
# Project Dependencies
|
||||
node_modules
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
|
||||
# Intellij
|
||||
.idea
|
||||
|
||||
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
|
||||
@ -1,6 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
pnpm install
|
||||
pnpm run lint
|
||||
pnpm run format
|
||||
2
.npmrc
Normal file
2
.npmrc
Normal file
@ -0,0 +1,2 @@
|
||||
auto-install-peers=true
|
||||
strict-peer-dependencies=false
|
||||
@ -1,3 +1,6 @@
|
||||
# Android App
|
||||
/app
|
||||
|
||||
# Schema
|
||||
schema/dist
|
||||
|
||||
@ -18,15 +21,9 @@ CHANGELOG.md
|
||||
|
||||
# Project Dependencies
|
||||
node_modules
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
|
||||
# Android App
|
||||
/app
|
||||
|
||||
# Docs
|
||||
docs/build
|
||||
docs/.docusaurus
|
||||
docker-compose.yml
|
||||
8
.vscode/extensions.json
vendored
8
.vscode/extensions.json
vendored
@ -1,3 +1,7 @@
|
||||
{
|
||||
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "lokalise.i18n-ally"]
|
||||
}
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"lokalise.i18n-ally"
|
||||
]
|
||||
}
|
||||
26
.vscode/launch.json
vendored
26
.vscode/launch.json
vendored
@ -1,26 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Debug: Server",
|
||||
"port": 9229,
|
||||
"restart": true,
|
||||
"stopOnEntry": false,
|
||||
"protocol": "inspector"
|
||||
},
|
||||
{
|
||||
"name": "Debug: Client",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "pnpm run dev:client",
|
||||
"console": "integratedTerminal",
|
||||
"serverReadyAction": {
|
||||
"pattern": "started server on .+, url: (https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"action": "debugWithChrome"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
25
.vscode/settings.json
vendored
25
.vscode/settings.json
vendored
@ -1,25 +1,22 @@
|
||||
{
|
||||
"css.validate": false,
|
||||
"scss.validate": false,
|
||||
"editor.wordWrap": "on",
|
||||
"npm.packageManager": "pnpm",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.wordWrap": "on",
|
||||
"eslint.workingDirectories": [
|
||||
"schema",
|
||||
"client",
|
||||
"server"
|
||||
],
|
||||
"i18n-ally.enabledFrameworks": [
|
||||
"react"
|
||||
],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": [
|
||||
"client/public/locales"
|
||||
],
|
||||
"i18n-ally.namespace": true,
|
||||
"i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}",
|
||||
"i18n-ally.sortKeys": true,
|
||||
"scss.validate": false
|
||||
"conventionalCommits.scopes": [
|
||||
"client",
|
||||
"server",
|
||||
"docker",
|
||||
"dependencies"
|
||||
]
|
||||
}
|
||||
225
CHANGELOG.md
225
CHANGELOG.md
@ -1,225 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
### [3.4.0](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.3.3...v3.4.0) (2022-04-30)
|
||||
|
||||
### [3.3.4](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.3.3...v3.3.4) (2022-04-09)
|
||||
|
||||
### [3.3.3](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.3.2...v3.3.3) (2022-04-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **profile:** add XING profile icon ([1e59f73](https://github.com/AmruthPillai/Reactive-Resume/commit/1e59f73f79a91d0264c0d2108906ee89d4eb27f8)), closes [#821](https://github.com/AmruthPillai/Reactive-Resume/issues/821)
|
||||
* **s3:** implement non-ephemeral storage through S3/DO Spaces ([feb911a](https://github.com/AmruthPillai/Reactive-Resume/commit/feb911aea06bacf58ea933d2803a2a89fe36e57b))
|
||||
|
||||
### [3.3.2](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.3.1...v3.3.2) (2022-04-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **types/react:** downgrade to <18 ([fc77b54](https://github.com/AmruthPillai/Reactive-Resume/commit/fc77b548d8d61530b2d158ff83f088bed12d5080))
|
||||
|
||||
### [3.3.1](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.3.0...v3.3.1) (2022-04-08)
|
||||
|
||||
## [3.3.0](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.11...v3.3.0) (2022-04-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **upgrade:** changes to code to support new template ([1df7810](https://github.com/AmruthPillai/Reactive-Resume/commit/1df78100ca0667ce9b7834cf2c25384eb21c67c2))
|
||||
|
||||
### What's Changed
|
||||
* New Crowdin updates by @AmruthPillai in https://github.com/AmruthPillai/Reactive-Resume/pull/791
|
||||
* Bump org.jetbrains.kotlin.android from 1.6.10 to 1.6.20 in /app by @dependabot in https://github.com/AmruthPillai/Reactive-Resume/pull/812
|
||||
* New Crowdin updates by @AmruthPillai in https://github.com/AmruthPillai/Reactive-Resume/pull/806
|
||||
* A new template - Leafish by @klejejs in https://github.com/AmruthPillai/Reactive-Resume/pull/811
|
||||
* Automatic multi-platform Docker image build by @schklom in https://github.com/AmruthPillai/Reactive-Resume/pull/817
|
||||
|
||||
### New Contributors
|
||||
* @klejejs made their first contribution in https://github.com/AmruthPillai/Reactive-Resume/pull/811
|
||||
* @schklom made their first contribution in https://github.com/AmruthPillai/Reactive-Resume/pull/817
|
||||
|
||||
### [3.2.11](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.10...v3.2.11) (2022-03-28)
|
||||
|
||||
### [3.2.10](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.9...v3.2.10) (2022-03-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **i18n:** add portuguese (pt) language to i18n locales ([7f1c82c](https://github.com/AmruthPillai/Reactive-Resume/commit/7f1c82cd9185ebb44486a16132eb44d5c2fb747a))
|
||||
|
||||
### [3.2.9](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.8...v3.2.9) (2022-03-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **i18n:** add nl and ru i18n locales to app ([03cbf22](https://github.com/AmruthPillai/Reactive-Resume/commit/03cbf22c9bee96cac8f228830b67b44529b7ecee))
|
||||
|
||||
### [3.2.8](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.7...v3.2.8) (2022-03-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **client/theme:** add theme switcher to landing page ([8f5632c](https://github.com/AmruthPillai/Reactive-Resume/commit/8f5632c5ad0bc8a4b3028c2806365717fedd78c9))
|
||||
* **flags:** introduce flags, disable_user_signups ([b6267d0](https://github.com/AmruthPillai/Reactive-Resume/commit/b6267d07ba2dcaed0da3946d136a0a9a01c441d5)), closes [#698](https://github.com/AmruthPillai/Reactive-Resume/issues/698)
|
||||
* **i18n:** add Vietnamese language to i18n locales ([4390bcc](https://github.com/AmruthPillai/Reactive-Resume/commit/4390bccfb9764f2d2730ec3a124b7befb6792e9a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **client/create-rename-slug:** fix slug accepting apostrophes and other special characters ([1facd2a](https://github.com/AmruthPillai/Reactive-Resume/commit/1facd2ad111cd9d990c808b3956d3915e8711acd)), closes [#706](https://github.com/AmruthPillai/Reactive-Resume/issues/706)
|
||||
* **disable_user_signups:** hide create account link under flag ([80acfe9](https://github.com/AmruthPillai/Reactive-Resume/commit/80acfe97c74bfa05b719285b19144144f3f7c5ba))
|
||||
|
||||
### [3.2.7](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.6...v3.2.7) (2022-03-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **i18n:** add Malayalam (മലയാളം) language to i18n locales ([3a2e62b](https://github.com/AmruthPillai/Reactive-Resume/commit/3a2e62be4c9acc14f17277c060cc9ea2c417a478))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **printer/i18n:** fix dates not showing up in resume language when printing ([90321e1](https://github.com/AmruthPillai/Reactive-Resume/commit/90321e1284409ab9442883c04a9b4c591d36f95d)), closes [#729](https://github.com/AmruthPillai/Reactive-Resume/issues/729)
|
||||
|
||||
### [3.2.6](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.5...v3.2.6) (2022-03-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **client/auth/google:** disable google login/registration if GOOGLE_CLIENT_ID is not in ENV ([7f0ee40](https://github.com/AmruthPillai/Reactive-Resume/commit/7f0ee40af4acc7eb41514406ecee3218ace9e891)), closes [#724](https://github.com/AmruthPillai/Reactive-Resume/issues/724)
|
||||
* **i18n:** add arabic language to i18n locale ([39fa6da](https://github.com/AmruthPillai/Reactive-Resume/commit/39fa6da5dd77ce2e12e81530fa18c2eac722c1f2))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **i18n:** add missing languages to dayjs date wrapper locales ([9e6dafc](https://github.com/AmruthPillai/Reactive-Resume/commit/9e6dafc8cada5c01559894905996b81004bedaec)), closes [#719](https://github.com/AmruthPillai/Reactive-Resume/issues/719)
|
||||
* **json-export:** add mimeType and charset to JSON export ([b3ff780](https://github.com/AmruthPillai/Reactive-Resume/commit/b3ff7805cd856a52900d9acef0554867d8ce0b01)), closes [#726](https://github.com/AmruthPillai/Reactive-Resume/issues/726)
|
||||
* **linkedin:** fix skill modal crashing when importing from linkedin ([a02b85b](https://github.com/AmruthPillai/Reactive-Resume/commit/a02b85b4bb1c4a1499aacddeac7bc59bcb1f7adb)), closes [#718](https://github.com/AmruthPillai/Reactive-Resume/issues/718)
|
||||
|
||||
### [3.2.5](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.4...v3.2.5) (2022-03-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **i18n:** add danish, polish and turkish locales to i18n ([97e9432](https://github.com/AmruthPillai/Reactive-Resume/commit/97e9432d6bd887e666a3443fbfde9a92cef53965))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **client/templates:** fix text veering off of artboard in most templates ([b2f1fb3](https://github.com/AmruthPillai/Reactive-Resume/commit/b2f1fb3a5502988a49c5cd3e496d9d165f5c1792)), closes [#702](https://github.com/AmruthPillai/Reactive-Resume/issues/702)
|
||||
|
||||
### [3.2.4](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.3...v3.2.4) (2022-03-14)
|
||||
|
||||
### [3.2.3](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.2...v3.2.3) (2022-03-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **client/import:** implement import json from reactive resume v2 ([42408ce](https://github.com/AmruthPillai/Reactive-Resume/commit/42408ce8c5ce55904854f9f6e0481889a01edfb8))
|
||||
|
||||
### [3.2.2](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.1...v3.2.2) (2022-03-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **client/skills:** make skill level optional ([02e396b](https://github.com/AmruthPillai/Reactive-Resume/commit/02e396bfdbf07ae75661f1e7e4e55060cacee7d0))
|
||||
|
||||
### [3.2.1](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.2.0...v3.2.1) (2022-03-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **i18n:** add Chinese (Simplified) language to locales ([549363b](https://github.com/AmruthPillai/Reactive-Resume/commit/549363bbe5bdd781699dea9506bd4baedf5740d1))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **client/basics:** fix issue with overlapping photo filters on safari/webkit/iOS ([e6bda68](https://github.com/AmruthPillai/Reactive-Resume/commit/e6bda688ac3ba1c04e82721add92e755ea5386c3))
|
||||
* **docker:** fix docker-compose for production grade deployments ([57f7edc](https://github.com/AmruthPillai/Reactive-Resume/commit/57f7edc13432a038c907afc6cb74b5182a9b2333))
|
||||
|
||||
## [3.2.0](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.1.4...v3.2.0) (2022-03-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **i18n:** add Bengali, Italian and other languages ([21931bc](https://github.com/AmruthPillai/Reactive-Resume/commit/21931bc324b5e2440baaaaa2e52a93b4f2c766f8))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **app:** fix issue with external link redirection in android app ([b18120b](https://github.com/AmruthPillai/Reactive-Resume/commit/b18120b3f7223981e28c0441a6b7725787186edb))
|
||||
* **client:** fix issue with react-query cache ([ed75a85](https://github.com/AmruthPillai/Reactive-Resume/commit/ed75a858279047dfd43152e041c1a09a625417f5))
|
||||
|
||||
### [3.1.4](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.1.3...v3.1.4) (2022-03-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **client:** exported pdf did not contain "Present" keyword with translations ([cf670af](https://github.com/AmruthPillai/Reactive-Resume/commit/cf670af4035dc9b462cf5b1aad06ca089cf1d40c))
|
||||
* **client:** fix issues raised through lgtm alerts ([dfccb31](https://github.com/AmruthPillai/Reactive-Resume/commit/dfccb3130f889934d31196226be3d33e772f323b))
|
||||
|
||||
### [3.1.3](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.1.2...v3.1.3) (2022-03-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **server:** reform url for pdf generation and download ([6d55f91](https://github.com/AmruthPillai/Reactive-Resume/commit/6d55f917eab3cb2f5f3a90c5a18f03b625d60021)), closes [#661](https://github.com/AmruthPillai/Reactive-Resume/issues/661)
|
||||
|
||||
### [3.1.2](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.1.1...v3.1.2) (2022-03-12)
|
||||
|
||||
|
||||
### CI
|
||||
|
||||
* **docker:**: include traefik routing and proxy to ensure server connections pass in local ([11cb066](https://github.com/AmruthPillai/Reactive-Resume/commit/11cb066573c6917857b79c028b97fcda1acaf90a))
|
||||
|
||||
### [3.1.1](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.1.0...v3.1.1) (2022-03-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **client:** add product hunt announcement banner ([b515fc3](https://github.com/AmruthPillai/Reactive-Resume/commit/b515fc36e7f282db92e8eb509b6c5004a944fa95))
|
||||
|
||||
## [3.1.0](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.0.0...v3.1.0) (2022-03-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **client:** add "spanish (es)" language to i18n locales ([bf167f8](https://github.com/AmruthPillai/Reactive-Resume/commit/bf167f81a3659677dada55856f5eaf0fc469e697))
|
||||
* **client:** add mm/yyyy date option to date format options ([82bf44d](https://github.com/AmruthPillai/Reactive-Resume/commit/82bf44daa24422156779e9b38d3dc695176eaa09)), closes [#656](https://github.com/AmruthPillai/Reactive-Resume/issues/656)
|
||||
* **client:** add sitemap generation to next app ([2cbc582](https://github.com/AmruthPillai/Reactive-Resume/commit/2cbc582a12b72b3012246022d4b518ed657d4c08))
|
||||
* **client:** disable "Toggle Page Orientation" when there's only one page on the artboard ([01da1a0](https://github.com/AmruthPillai/Reactive-Resume/commit/01da1a06b802f1063a41d7a9a682e76b1daf9461)), closes [#655](https://github.com/AmruthPillai/Reactive-Resume/issues/655)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **client:** remove hard-coded "keywords:" in certain templates ([dda42b4](https://github.com/AmruthPillai/Reactive-Resume/commit/dda42b4c6b3bc359ac4f2bb91ca8118ddc84ec07)), closes [#650](https://github.com/AmruthPillai/Reactive-Resume/issues/650)
|
||||
* **client:** show "present" string if end date is not entered, also add to i18n locales ([b5cd6c4](https://github.com/AmruthPillai/Reactive-Resume/commit/b5cd6c412b5b6b6ca7bb43c3801762de451f06b4)), closes [#653](https://github.com/AmruthPillai/Reactive-Resume/issues/653)
|
||||
* **server:** photo uploads not working, fix save location and returned url ([799f208](https://github.com/AmruthPillai/Reactive-Resume/commit/799f20823e6d97a1ff0ba2c45c61d56304d0fa58)), closes [#658](https://github.com/AmruthPillai/Reactive-Resume/issues/658)
|
||||
|
||||
## [3.0.0](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.0.0-beta.6...v3.0.0) (2022-03-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **lang**: add German, Kannada and Tamil languages to the app ([3a524f9](https://github.com/AmruthPillai/Reactive-Resume/commit/3a524f9c9c7a0e446491265b2242ad3dfeae188c))
|
||||
* **docs:** add docusaurus workspace, initial setup of docs ([dc4aa0b](https://github.com/AmruthPillai/Reactive-Resume/commit/dc4aa0b496096bd59c45426bfcea6ba7db5f5c01))
|
||||
|
||||
## [3.0.0-beta.6](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.0.0-beta.5...v3.0.0-beta.6) (2022-03-11)
|
||||
|
||||
### Features
|
||||
|
||||
* **lang:** add language switcher on the landing page, in the footer ([8bc7d25](https://github.com/AmruthPillai/Reactive-Resume/commit/8bc7d2599ef6af7a07bfbe886c43844152b0d9f7))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **i18n:** add missing translation keys, update lang/locale logic ([7d8828a](https://github.com/AmruthPillai/Reactive-Resume/commit/7d8828a358d653bb162877a64c75028eb82678cd))
|
||||
* **webkit:** fix issue with webkit not supporting .at() ([2654cba](https://github.com/AmruthPillai/Reactive-Resume/commit/2654cba039eb73d33257c36fa90a52cabc9fda96))
|
||||
|
||||
## [3.0.0-beta.5](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.0.0-beta.4...v3.0.0-beta.5) (2022-03-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **app:** fix issue with using swipelayout ([972e8b1](https://github.com/AmruthPillai/Reactive-Resume/commit/972e8b1bcf9ad44d8915bf23d189711672937bc0))
|
||||
@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
|
||||
40
README.md
40
README.md
@ -1,4 +1,4 @@
|
||||
<img src="https://github.com/AmruthPillai/Reactive-Resume/blob/main/docs/static/logo.svg" alt="Reactive Resume" width="256px" height="256px" />
|
||||
<img src="https://rxresu.me/images/logos/logo.png" alt="Reactive Resume" width="256px" height="256px" />
|
||||
|
||||
# Reactive Resume
|
||||
|
||||
@ -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,7 +19,7 @@ You have complete control over what goes into your resume, how it looks, what co
|
||||
## Table of Contents
|
||||
|
||||
- [Reactive Resume](#reactive-resume)
|
||||
- [Go to App | [Docs](https://docs.rxresu.me)](#go-to-app--docs)
|
||||
- [Go to App | Docs](#go-to-app--docs)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Features](#features)
|
||||
- [Languages](#languages)
|
||||
@ -27,7 +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)
|
||||
- [💸 PayPal](#-paypal)
|
||||
- [GitHub Sponsor](#github-sponsor)
|
||||
- [PayPal](#paypal)
|
||||
- [Infrastructure](#infrastructure)
|
||||
- [Contributors Wall](#contributors-wall)
|
||||
- [License](#license)
|
||||
@ -54,28 +56,45 @@ You have complete control over what goes into your resume, how it looks, what co
|
||||
|
||||
## Languages
|
||||
|
||||
- Amharic (አማርኛ)
|
||||
- Arabic (اَلْعَرَبِيَّةُ)
|
||||
- Bengali (বাংলা)
|
||||
- Bulgarian (български)
|
||||
- Catalan (Valencian)
|
||||
- Chinese (中文)
|
||||
- Czech (čeština)
|
||||
- Danish (Dansk)
|
||||
- Dutch (Nederlands)
|
||||
- English
|
||||
- Finnish (Suomi)
|
||||
- French (Français)
|
||||
- German (Deutsch)
|
||||
- Greek (Ελληνικά)
|
||||
- Hebrew (Ivrit)
|
||||
- Hindi (हिन्दी)
|
||||
- Hungarian (Magyar)
|
||||
- Indonesian (Bahasa Indonesia)
|
||||
- Italian (Italiano)
|
||||
- Japanese (日本語)
|
||||
- Kannada (ಕನ್ನಡ)
|
||||
- Khmer (ភាសាខ្មែរ)
|
||||
- Korean (한국어)
|
||||
- Malayalam (മലയാളം)
|
||||
- Marathi (मराठी)
|
||||
- Nepali (नेपाली)
|
||||
- Norwegian (Norsk)
|
||||
- Odia (ଓଡ଼ିଆ)
|
||||
- Persian (فارسی)
|
||||
- Polish (Polski)
|
||||
- Portuguese (Português)
|
||||
- Romanian (limba română)
|
||||
- Russian (русский)
|
||||
- Serbian (српски језик)
|
||||
- Spanish (Español)
|
||||
- Swedish (Svenska)
|
||||
- Tamil (தமிழ்)
|
||||
- Turkish (Türkçe)
|
||||
- Ukrainian (Українська мова)
|
||||
- Vietnamese (Tiếng Việt)
|
||||
|
||||
Help by [translating Reactive Resume](https://translate.rxresu.me) to your language!
|
||||
@ -86,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
|
||||
|
||||
@ -108,7 +131,12 @@ Use the [GitHub Issues](https://github.com/AmruthPillai/Reactive-Resume/issues/n
|
||||
|
||||
Reactive Resume would be nothing without the folks who supported me and kept the project alive in the beginning, and your continued support is what keeps me going. If you found Reactive Resume to be useful, helpful or just insightful and appreciate the effort I took to make the project, please consider donating as little or as much as you can.
|
||||
|
||||
### [💸 PayPal](https://paypal.me/RajaRajanA)
|
||||
### [GitHub Sponsor](https://github.com/sponsors/AmruthPillai)
|
||||
### [PayPal](https://paypal.me/RajaRajanA)
|
||||
|
||||
## GitHub Star History
|
||||
|
||||
[](https://star-history.com/#AmruthPillai/Reactive-Resume&Date)
|
||||
|
||||
## Infrastructure
|
||||
|
||||
|
||||
@ -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.6.21' 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.20' 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
|
||||
|
||||
@ -1,9 +1,33 @@
|
||||
{
|
||||
"extends": ["../.eslintrc.json", "next/core-web-vitals"],
|
||||
"extends": ["../.eslintrc.json", "next/core-web-vitals", "plugin:tailwindcss/recommended"],
|
||||
"plugins": ["unused-imports"],
|
||||
"ignorePatterns": [".next", "__ENV.js"],
|
||||
"settings": {
|
||||
"next": {
|
||||
"rootDir": "client"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
// Next.js
|
||||
"@next/next/no-img-element": "off",
|
||||
"@next/next/no-sync-scripts": "off",
|
||||
"@next/next/no-html-link-for-pages": [2, "client/pages"]
|
||||
|
||||
// React Hooks
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
|
||||
// Unused Imports
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "none",
|
||||
"varsIgnorePattern": "^_",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
|
||||
// Tailwind CSS
|
||||
"tailwindcss/no-custom-classname": ["warn", { "whitelist": ["preview-mode", "printer-mode", "markdown"] }]
|
||||
}
|
||||
}
|
||||
|
||||
5
client/.gitignore
vendored
5
client/.gitignore
vendored
@ -36,4 +36,7 @@ yarn-error.log*
|
||||
*.tsbuildinfo
|
||||
|
||||
# react-env
|
||||
__ENV.js
|
||||
__ENV.js
|
||||
|
||||
# next-sitemap
|
||||
sitemap*.xml
|
||||
|
||||
@ -1,22 +1,19 @@
|
||||
FROM node:lts-alpine as dependencies
|
||||
|
||||
RUN apk add --no-cache curl g++ make python3 \
|
||||
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
FROM node:lts-alpine AS base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
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 ./
|
||||
COPY ./schema/package.json ./schema/package.json
|
||||
COPY ./client/package.json ./client/package.json
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
FROM node:lts-alpine as builder
|
||||
|
||||
RUN apk add --no-cache curl g++ make python3 \
|
||||
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
|
||||
WORKDIR /app
|
||||
FROM base as builder
|
||||
|
||||
COPY . .
|
||||
|
||||
@ -24,21 +21,15 @@ COPY --from=dependencies /app/node_modules ./node_modules
|
||||
COPY --from=dependencies /app/schema/node_modules ./schema/node_modules
|
||||
COPY --from=dependencies /app/client/node_modules ./client/node_modules
|
||||
|
||||
RUN pnpm run build:schema
|
||||
RUN pnpm run build:client
|
||||
RUN pnpm run --filter schema build \
|
||||
&& pnpm run --filter client build
|
||||
|
||||
FROM node:lts-alpine as production
|
||||
FROM base as production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache curl \
|
||||
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
|
||||
COPY --from=builder /app/pnpm-*.yaml ./
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/package.json /app/pnpm-*.yaml ./
|
||||
COPY --from=builder /app/client/package.json ./client/package.json
|
||||
|
||||
RUN pnpm install -F client --frozen-lockfile --prod
|
||||
RUN pnpm install --filter client --prod --frozen-lockfile --workspace-root
|
||||
|
||||
COPY --from=builder /app/client/.next ./client/.next
|
||||
COPY --from=builder /app/client/public ./client/public
|
||||
@ -50,6 +41,6 @@ EXPOSE 3000
|
||||
ENV PORT 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=20s --retries=3 --start-period=15s \
|
||||
CMD curl -fSs 127.0.0.1:3000 || exit 1
|
||||
CMD curl -fSs localhost:3000 || exit 1
|
||||
|
||||
CMD [ "pnpm", "run", "start:client" ]
|
||||
CMD [ "pnpm", "run", "start" ]
|
||||
@ -5,17 +5,21 @@ import {
|
||||
FilterCenterFocus,
|
||||
InsertPageBreak,
|
||||
Link,
|
||||
RedoOutlined,
|
||||
UndoOutlined,
|
||||
ViewSidebar,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
} 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';
|
||||
import { printResumeAsPdf, PrintResumeAsPdfParams } from '@/services/printer';
|
||||
@ -25,20 +29,24 @@ 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();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const isDesktop = useMediaQuery(theme.breakpoints.up('sm'));
|
||||
const pages = useAppSelector((state) => state.resume.metadata.layout);
|
||||
|
||||
const { past, present: resume, future } = useAppSelector((state) => state.resume);
|
||||
const pages = get(resume, 'metadata.layout');
|
||||
const { left, right } = useAppSelector((state) => state.build.sidebar);
|
||||
const orientation = useAppSelector((state) => state.build.page.orientation);
|
||||
|
||||
const { mutateAsync, isLoading } = useMutation<string, ServerError, PrintResumeAsPdfParams>(printResumeAsPdf);
|
||||
|
||||
const handleUndo = () => dispatch(ActionCreators.undo());
|
||||
const handleRedo = () => dispatch(ActionCreators.redo());
|
||||
|
||||
const handleTogglePageBreakLine = () => dispatch(togglePageBreakLine());
|
||||
|
||||
const handleTogglePageOrientation = () => dispatch(togglePageOrientation());
|
||||
@ -60,10 +68,11 @@ 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(`/api${url}`);
|
||||
download(url);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -75,6 +84,20 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
||||
})}
|
||||
>
|
||||
<div className={styles.controller}>
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.undo')}>
|
||||
<ButtonBase onClick={handleUndo} className={clsx({ 'pointer-events-none opacity-50': past.length < 2 })}>
|
||||
<UndoOutlined fontSize="medium" />
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.redo')}>
|
||||
<ButtonBase onClick={handleRedo} className={clsx({ 'pointer-events-none opacity-50': future.length === 0 })}>
|
||||
<RedoOutlined fontSize="medium" />
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.zoom-in')}>
|
||||
<ButtonBase onClick={() => zoomIn(0.25)}>
|
||||
<ZoomIn fontSize="medium" />
|
||||
@ -97,17 +120,18 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
||||
|
||||
{isDesktop && (
|
||||
<>
|
||||
{pages.length > 1 && (
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-orientation')}>
|
||||
<ButtonBase onClick={handleTogglePageOrientation}>
|
||||
{orientation === 'vertical' ? (
|
||||
<AlignHorizontalCenter fontSize="medium" />
|
||||
) : (
|
||||
<AlignVerticalCenter fontSize="medium" />
|
||||
)}
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-orientation')}>
|
||||
<ButtonBase
|
||||
onClick={handleTogglePageOrientation}
|
||||
className={clsx({ 'pointer-events-none opacity-50': pages.length === 1 })}
|
||||
>
|
||||
{orientation === 'vertical' ? (
|
||||
<AlignHorizontalCenter fontSize="medium" />
|
||||
) : (
|
||||
<AlignVerticalCenter fontSize="medium" />
|
||||
)}
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-page-break-line')}>
|
||||
<ButtonBase onClick={handleTogglePageBreakLine}>
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
@apply h-full w-full #{!important};
|
||||
@apply h-full w-full overflow-visible #{!important};
|
||||
}
|
||||
|
||||
.artboard {
|
||||
|
||||
@ -13,7 +13,7 @@ import Page from './Page';
|
||||
const Center = () => {
|
||||
const orientation = useAppSelector((state) => state.build.page.orientation);
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const layout: string[][][] = get(resume, 'metadata.layout');
|
||||
|
||||
if (isEmpty(resume)) return null;
|
||||
|
||||
@ -53,12 +53,11 @@ const Header = () => {
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const { mutateAsync: duplicateMutation } = useMutation<Resume, ServerError, DuplicateResumeParams>(duplicateResume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const { left, right } = useAppSelector((state) => state.build.sidebar);
|
||||
|
||||
const { mutateAsync: deleteMutation } = useMutation<void, ServerError, DeleteResumeParams>(deleteResume);
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const { left, right } = useAppSelector((state) => state.build.sidebar);
|
||||
const { mutateAsync: duplicateMutation } = useMutation<Resume, ServerError, DuplicateResumeParams>(duplicateResume);
|
||||
|
||||
const name = useMemo(() => get(resume, 'name'), [resume]);
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -27,6 +27,15 @@
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&.format-letter {
|
||||
width: 216mm;
|
||||
min-height: 279mm;
|
||||
|
||||
&.break::after {
|
||||
top: calc(279mm - 19px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pageNumber {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { CustomCSS, Theme, Typography } from '@reactive-resume/schema';
|
||||
import { CustomCSS, PageConfig, ThemeConfig, Typography } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
@ -20,25 +20,27 @@ type Props = PageProps & {
|
||||
const Page: React.FC<Props> = ({ page, showPageNumbers = false }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const breakLine: boolean = useAppSelector((state) => state.build.page.breakLine);
|
||||
|
||||
const theme: Theme = get(resume, 'metadata.theme');
|
||||
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 { 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';
|
||||
@ -25,7 +26,8 @@ const LeftSidebar = () => {
|
||||
|
||||
const isDesktop = useMediaQuery(theme.breakpoints.up('lg'));
|
||||
|
||||
const sections = useAppSelector((state) => state.resume.sections);
|
||||
const sections = useAppSelector((state) => state.resume.present.sections);
|
||||
|
||||
const { open } = useAppSelector((state) => state.build.sidebar.left);
|
||||
|
||||
const customSections = useMemo(() => getCustomSections(sections), [sections]);
|
||||
@ -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>
|
||||
<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>
|
||||
))}
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ const Basics = () => {
|
||||
<PhotoUpload />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 w-full sm:col-span-2">
|
||||
<div className="grid w-full gap-2 sm:col-span-2">
|
||||
<ResumeInput label={t<string>('builder.leftSidebar.sections.basics.name.label')} path="basics.name" />
|
||||
|
||||
<Button variant="outlined" startIcon={<PhotoFilter />} onClick={handleClick}>
|
||||
|
||||
@ -12,7 +12,7 @@ const PhotoFilters = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const photo: Photo = useAppSelector((state) => get(state.resume, 'basics.photo'));
|
||||
const photo: Photo = useAppSelector((state) => get(state.resume.present, 'basics.photo'));
|
||||
const size: number = get(photo, 'filters.size', 128);
|
||||
const shape: PhotoShape = get(photo, 'filters.shape', 'square');
|
||||
const grayscale: boolean = get(photo, 'filters.grayscale', false);
|
||||
|
||||
@ -21,8 +21,8 @@ const PhotoUpload: React.FC = () => {
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const id: number = useAppSelector((state) => get(state.resume, 'id'));
|
||||
const photo: Photo = useAppSelector((state) => get(state.resume, 'basics.photo'));
|
||||
const id: number = useAppSelector((state) => get(state.resume.present, 'id'));
|
||||
const photo: Photo = useAppSelector((state) => get(state.resume.present, 'basics.photo'));
|
||||
|
||||
const { mutateAsync: uploadMutation, isLoading } = useMutation<Resume, ServerError, UploadPhotoParams>(uploadPhoto);
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Add } from '@mui/icons-material';
|
||||
import { Button } from '@mui/material';
|
||||
import { ListItem } from '@reactive-resume/schema';
|
||||
import { ListItem, Section as SectionRecord, SectionType } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import get from 'lodash/get';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
@ -10,53 +10,77 @@ 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();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector<string>((state) => get(state.resume, `${path}.name`, name));
|
||||
const visibility = useAppSelector<boolean>((state) => get(state.resume, `${path}.visible`, true));
|
||||
const heading = useAppSelector<string>((state) => get(state.resume.present, `${path}.name`, name));
|
||||
const visibility = useAppSelector<boolean>((state) => get(state.resume.present, `${path}.visible`, true));
|
||||
|
||||
const handleAdd = () => {
|
||||
const id = path.split('.')[1];
|
||||
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`, 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>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -18,7 +18,7 @@ const SectionSettings: React.FC<Props> = ({ path }) => {
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const columns = useAppSelector<number>((state) => get(state.resume, `${path}.columns`, 2));
|
||||
const columns = useAppSelector<number>((state) => get(state.resume.present, `${path}.columns`, 2));
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
|
||||
@ -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>
|
||||
<nav className="overflow-y-auto">
|
||||
<div>
|
||||
<Avatar size={40} />
|
||||
<Divider />
|
||||
|
||||
@ -17,7 +17,9 @@ const CustomCSS = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const customCSS: CustomCSSType = useAppSelector((state) => get(state.resume, 'metadata.css', {}));
|
||||
const customCSS: CustomCSSType = useAppSelector((state) =>
|
||||
get(state.resume.present, 'metadata.css', {} 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';
|
||||
@ -13,7 +14,7 @@ import { useAppSelector } from '@/store/hooks';
|
||||
const Export = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
|
||||
const { mutateAsync, isLoading } = useMutation<string, ServerError, PrintResumeAsPdfParams>(printResumeAsPdf);
|
||||
|
||||
@ -45,10 +46,11 @@ 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(`/api${url}`);
|
||||
download(url);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { DragDropContext, Draggable, DraggableLocation, Droppable, DropResult } from '@hello-pangea/dnd';
|
||||
import { Add, Close, Restore } from '@mui/icons-material';
|
||||
import { Button, IconButton, Tooltip } from '@mui/material';
|
||||
import clsx from 'clsx';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import get from 'lodash/get';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { DragDropContext, Draggable, DraggableLocation, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||
|
||||
import Heading from '@/components/shared/Heading';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
@ -23,8 +23,8 @@ const Layout = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const layout = useAppSelector((state) => state.resume.metadata.layout);
|
||||
const resumeSections = useAppSelector((state) => state.resume.sections);
|
||||
const layout = useAppSelector((state) => state.resume.present.metadata.layout);
|
||||
const resumeSections = useAppSelector((state) => state.resume.present.sections);
|
||||
|
||||
const onDragEnd = (dropResult: DropResult) => {
|
||||
const { source: srcLoc, destination: destLoc } = dropResult;
|
||||
|
||||
@ -3,7 +3,7 @@ import { Button } from '@mui/material';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import Heading from '@/components/shared/Heading';
|
||||
import { 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';
|
||||
|
||||
@ -49,6 +49,18 @@ const Links = () => {
|
||||
{t<string>('builder.rightSidebar.sections.links.github')}
|
||||
</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')}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -10,12 +10,12 @@ import {
|
||||
Switch,
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import { DateConfig, Resume } from '@reactive-resume/schema';
|
||||
import { DateConfig, PageConfig, 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 } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import Heading from '@/components/shared/Heading';
|
||||
@ -36,9 +36,11 @@ const Settings = () => {
|
||||
|
||||
const { locale, ...router } = useRouter();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const [confirmReset, setConfirmReset] = useState(false);
|
||||
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const theme = useAppSelector((state) => state.build.theme);
|
||||
const pages = useAppSelector((state) => state.resume.metadata.layout);
|
||||
const pages = useAppSelector((state) => state.resume.present.metadata.layout);
|
||||
const breakLine = useAppSelector((state) => state.build.page.breakLine);
|
||||
const orientation = useAppSelector((state) => state.build.page.orientation);
|
||||
|
||||
@ -46,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().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>(
|
||||
@ -58,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 }));
|
||||
|
||||
@ -78,19 +84,24 @@ const Settings = () => {
|
||||
};
|
||||
|
||||
const handleResetResume = async () => {
|
||||
await resetResumeMutation({ id });
|
||||
if (!confirmReset) {
|
||||
return setConfirmReset(true);
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries(`resume/${username}/${slug}`);
|
||||
await resetResumeMutation({ id });
|
||||
await queryClient.invalidateQueries(`resume/${username}/${slug}`);
|
||||
|
||||
setConfirmReset(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading path="metadata.settings" name={t<string>('builder.rightSidebar.sections.settings.heading')} />
|
||||
|
||||
<List sx={{ padding: 0 }}>
|
||||
<List disablePadding>
|
||||
{/* Global Settings */}
|
||||
<>
|
||||
<ListSubheader className="rounded">
|
||||
<ListSubheader disableSticky className="rounded">
|
||||
{t<string>('builder.rightSidebar.sections.settings.global.heading')}
|
||||
</ListSubheader>
|
||||
|
||||
@ -111,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>
|
||||
|
||||
@ -127,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}
|
||||
@ -148,10 +159,27 @@ const Settings = () => {
|
||||
|
||||
{/* Page Settings */}
|
||||
<>
|
||||
<ListSubheader className="rounded">
|
||||
<ListSubheader disableSticky className="rounded">
|
||||
{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')}
|
||||
@ -180,11 +208,11 @@ const Settings = () => {
|
||||
|
||||
{/* Resume Settings */}
|
||||
<>
|
||||
<ListSubheader className="rounded">
|
||||
<ListSubheader disableSticky className="rounded">
|
||||
{t<string>('builder.rightSidebar.sections.settings.resume.heading')}
|
||||
</ListSubheader>
|
||||
|
||||
<ListItem>
|
||||
<ListItem disableGutters>
|
||||
<ListItemButton onClick={handleLoadSampleData}>
|
||||
<ListItemIcon>
|
||||
<Anchor />
|
||||
@ -196,13 +224,17 @@ const Settings = () => {
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItem disableGutters>
|
||||
<ListItemButton onClick={handleResetResume}>
|
||||
<ListItemIcon>
|
||||
<DeleteForever />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t<string>('builder.rightSidebar.sections.settings.resume.reset.primary')}
|
||||
primary={
|
||||
confirmReset
|
||||
? 'Are you sure?'
|
||||
: t<string>('builder.rightSidebar.sections.settings.resume.reset.primary')
|
||||
}
|
||||
secondary={t<string>('builder.rightSidebar.sections.settings.resume.reset.secondary')}
|
||||
/>
|
||||
</ListItemButton>
|
||||
|
||||
@ -17,7 +17,7 @@ const Sharing = () => {
|
||||
|
||||
const [showShortUrl, setShowShortUrl] = useState(false);
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
const isPublic = useMemo(() => get(resume, 'public'), [resume]);
|
||||
const url = useMemo(() => getResumeUrl(resume, { withHost: true }), [resume]);
|
||||
const shortUrl = useMemo(() => getResumeUrl(resume, { withHost: true, shortUrl: true }), [resume]);
|
||||
|
||||
@ -16,7 +16,7 @@ const Templates = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const currentTemplate: string = useAppSelector((state) => get(state.resume, 'metadata.template'));
|
||||
const currentTemplate: string = useAppSelector((state) => get(state.resume.present, 'metadata.template'));
|
||||
|
||||
const handleChange = (template: TemplateMeta) => {
|
||||
dispatch(setResumeState({ path: 'metadata.template', value: template.id }));
|
||||
@ -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" />
|
||||
<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,8 +1,8 @@
|
||||
.container {
|
||||
@apply grid sm:grid-cols-2 gap-4;
|
||||
@apply grid gap-4 sm:grid-cols-2;
|
||||
}
|
||||
|
||||
.colorOptions {
|
||||
@apply col-span-2 mb-4;
|
||||
@apply grid grid-cols-8 gap-y-2 justify-items-center;
|
||||
@apply grid grid-cols-8 justify-items-center gap-y-2;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Theme as ThemeType } from '@reactive-resume/schema';
|
||||
import { ThemeConfig } from '@reactive-resume/schema';
|
||||
import get from 'lodash/get';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
@ -16,7 +16,9 @@ const Theme = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { background, text, primary } = useAppSelector<ThemeType>((state) => get(state.resume, 'metadata.theme'));
|
||||
const { background, text, primary } = useAppSelector<ThemeConfig>((state) =>
|
||||
get(state.resume.present, 'metadata.theme')
|
||||
);
|
||||
|
||||
const handleChange = (property: string, color: string) => {
|
||||
dispatch(setResumeState({ path: `metadata.theme.${property}`, value: color[0] !== '#' ? `#${color}` : color }));
|
||||
|
||||
@ -33,7 +33,7 @@ const Widgets: React.FC<WidgetProps> = ({ label, category }) => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { family, size } = useAppSelector<TypographyType>((state) => get(state.resume, 'metadata.typography'));
|
||||
const { family, size } = useAppSelector<TypographyType>((state) => get(state.resume.present, 'metadata.typography'));
|
||||
|
||||
const { data: fonts } = useQuery(FONTS_QUERY, fetchFonts, {
|
||||
select: (fonts) => fonts.sort((a, b) => a.category.localeCompare(b.category)),
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
.testimony {
|
||||
@apply grid gap-2;
|
||||
@apply border-2 rounded p-4 dark:border-neutral-800;
|
||||
@apply rounded border-2 p-4 dark:border-neutral-800;
|
||||
|
||||
blockquote {
|
||||
@apply text-xs leading-normal text-justify opacity-90;
|
||||
@apply text-justify text-xs leading-normal opacity-90;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply sticky top-0 left-0 right-0 z-50 pt-6 bg-neutral-50 dark:bg-neutral-900;
|
||||
@apply sticky top-0 left-0 right-0 z-50 bg-neutral-50 pt-6 dark:bg-neutral-900;
|
||||
@apply flex items-center justify-between;
|
||||
@apply w-full border-b pb-5 dark:border-white/10;
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
}
|
||||
|
||||
.footer {
|
||||
@apply sticky bottom-0 left-0 right-0 z-50 pb-6 bg-neutral-50 dark:bg-neutral-900;
|
||||
@apply sticky bottom-0 left-0 right-0 z-50 bg-neutral-50 pb-6 dark:bg-neutral-900;
|
||||
@apply flex items-center justify-end gap-x-4;
|
||||
@apply w-full border-t pt-5 dark:border-white/10;
|
||||
}
|
||||
|
||||
@ -32,8 +32,8 @@ const Heading: React.FC<Props> = ({
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`, name));
|
||||
const visibility = useAppSelector((state) => get(state.resume, `${path}.visible`, true));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`, name));
|
||||
const visibility = useAppSelector((state) => get(state.resume.present, `${path}.visible`, true));
|
||||
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
|
||||
@ -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`, heading)}</h1>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@ -9,5 +9,5 @@
|
||||
}
|
||||
|
||||
.language {
|
||||
@apply py-2 px-4 cursor-pointer text-center hover:underline;
|
||||
@apply cursor-pointer py-2 px-4 text-center hover:underline;
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ const List: React.FC<Props> = ({
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const list: Array<ListItemType> = useAppSelector((state) => get(state.resume, path, []));
|
||||
const list: Array<ListItemType> = useAppSelector((state) => get(state.resume.present, path, []));
|
||||
|
||||
const handleEdit = (item: ListItemType) => {
|
||||
isFunction(onEdit) && onEdit(item);
|
||||
|
||||
@ -5,6 +5,7 @@ import styles from './Loading.module.scss';
|
||||
|
||||
const Loading: React.FC = () => {
|
||||
const { isReady } = useRouter();
|
||||
|
||||
const isFetching = useIsFetching();
|
||||
const isMutating = useIsMutating();
|
||||
|
||||
|
||||
@ -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 { DatePicker } from '@mui/lab';
|
||||
import { TextField } from '@mui/material';
|
||||
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';
|
||||
|
||||
@ -21,7 +20,7 @@ interface Props {
|
||||
const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, markdownSupported = false }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const stateValue = useAppSelector((state) => get(state.resume, path, ''));
|
||||
const stateValue = useAppSelector((state) => get(state.resume.present, path, ''));
|
||||
|
||||
useEffect(() => {
|
||||
setValue(stateValue);
|
||||
@ -58,12 +57,13 @@ const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, m
|
||||
<DatePicker
|
||||
openTo="year"
|
||||
label={label}
|
||||
value={value}
|
||||
value={dayjs(value)}
|
||||
views={['year', 'month', 'day']}
|
||||
renderInput={(params) => <TextField {...params} error={false} className={className} />}
|
||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||
isEmpty(keyboardInputValue) && onChangeValue('');
|
||||
date && dayjs(date).isValid() && onChangeValue(date.toISOString());
|
||||
slots={{
|
||||
textField: (params) => <TextField {...params} error={false} className={className} />,
|
||||
}}
|
||||
onChange={(date: dayjs.Dayjs | null) => {
|
||||
date && dayjs(date).isValid() && onChangeValue(dayjs(date).format('YYYY-MM-DD'));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -2,30 +2,49 @@ 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: 'فارسی', isRTL: true },
|
||||
{ code: 'fi', name: 'Finnish', localName: 'Suomi' },
|
||||
{ code: 'fr', name: 'French', localName: 'Français' },
|
||||
{ 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' },
|
||||
{ code: 'it', name: 'Italian', localName: 'Italiano' },
|
||||
{ code: 'ja', name: 'Japanese', localName: '日本語' },
|
||||
{ code: 'km', name: 'Khmer', localName: 'ភាសាខ្មែរ' },
|
||||
{ code: 'kn', name: 'Kannada', localName: 'ಕನ್ನಡ' },
|
||||
{ code: 'ko', name: 'Korean', localName: '한국어' },
|
||||
{ code: 'ml', name: 'Malayalam', localName: 'മലയാളം' },
|
||||
{ code: 'mr', name: 'Marathi', localName: 'मराठी' },
|
||||
{ code: 'ne', name: 'Nepali', localName: 'नेपाली' },
|
||||
{ code: 'nl', name: 'Dutch', localName: 'Nederlands' },
|
||||
{ code: 'no', name: 'Norwegian', localName: 'Norsk' },
|
||||
{ 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: 'српски језик' },
|
||||
{ code: 'sv', name: 'Swedish', localName: 'Svenska' },
|
||||
{ code: 'ta', name: 'Tamil', localName: 'தமிழ்' },
|
||||
{ code: 'tr', name: 'Turkish', localName: 'Türkçe' },
|
||||
{ code: 'uk', name: 'Ukranian', localName: 'Українська мова' },
|
||||
{ code: 'vi', name: 'Vietnamese', localName: 'Tiếng Việt' },
|
||||
{ code: 'zh', name: 'Chinese', localName: '中文' },
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
@ -23,7 +23,7 @@ import {
|
||||
VolunteerActivism,
|
||||
Work,
|
||||
} from '@mui/icons-material';
|
||||
import { Section as SectionRecord } from '@reactive-resume/schema';
|
||||
import { Section as SectionRecord, SectionType } from '@reactive-resume/schema';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
import Basics from '@/components/build/LeftSidebar/sections/Basics';
|
||||
@ -60,59 +60,136 @@ export const left: SidebarSection[] = [
|
||||
{
|
||||
id: 'work',
|
||||
icon: <Work />,
|
||||
component: <Section path="sections.work" titleKey="name" subtitleKey="position" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'work'}
|
||||
addMore={true}
|
||||
path="sections.work"
|
||||
titleKey="name"
|
||||
subtitleKey="position"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'education',
|
||||
icon: <School />,
|
||||
component: <Section path="sections.education" titleKey="institution" subtitleKey="area" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'education'}
|
||||
path="sections.education"
|
||||
titleKey="institution"
|
||||
subtitleKey="area"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'awards',
|
||||
icon: <EmojiEvents />,
|
||||
component: <Section path="sections.awards" titleKey="title" subtitleKey="awarder" isEditable isHideable />,
|
||||
component: (
|
||||
<Section type={'awards'} path="sections.awards" titleKey="title" subtitleKey="awarder" isEditable isHideable />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'certifications',
|
||||
icon: <CardGiftcard />,
|
||||
component: <Section path="sections.certifications" titleKey="name" subtitleKey="issuer" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'certifications'}
|
||||
path="sections.certifications"
|
||||
titleKey="name"
|
||||
subtitleKey="issuer"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'publications',
|
||||
icon: <MenuBook />,
|
||||
component: <Section path="sections.publications" titleKey="name" subtitleKey="publisher" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'publications'}
|
||||
path="sections.publications"
|
||||
titleKey="name"
|
||||
subtitleKey="publisher"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'skills',
|
||||
icon: <Architecture />,
|
||||
component: <Section path="sections.skills" titleKey="name" subtitleKey="level" isEditable isHideable />,
|
||||
component: (
|
||||
<Section type={'skills'} path="sections.skills" titleKey="name" subtitleKey="level" isEditable isHideable />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'languages',
|
||||
icon: <Language />,
|
||||
component: <Section path="sections.languages" titleKey="name" subtitleKey="level" isEditable isHideable />,
|
||||
component: (
|
||||
<Section type={'languages'} path="sections.languages" titleKey="name" subtitleKey="level" isEditable isHideable />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'interests',
|
||||
icon: <Sailing />,
|
||||
component: <Section path="sections.interests" titleKey="name" subtitleKey="keywords" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'interests'}
|
||||
path="sections.interests"
|
||||
titleKey="name"
|
||||
subtitleKey="keywords"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'volunteer',
|
||||
icon: <VolunteerActivism />,
|
||||
component: (
|
||||
<Section path="sections.volunteer" titleKey="organization" subtitleKey="position" isEditable isHideable />
|
||||
<Section
|
||||
type={'volunteer'}
|
||||
path="sections.volunteer"
|
||||
titleKey="organization"
|
||||
subtitleKey="position"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'projects',
|
||||
icon: <Coffee />,
|
||||
component: <Section path="sections.projects" titleKey="name" subtitleKey="description" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'projects'}
|
||||
path="sections.projects"
|
||||
titleKey="name"
|
||||
subtitleKey="description"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'references',
|
||||
icon: <Groups />,
|
||||
component: <Section path="sections.references" titleKey="name" subtitleKey="relationship" isEditable isHideable />,
|
||||
component: (
|
||||
<Section
|
||||
type={'references'}
|
||||
path="sections.references"
|
||||
titleKey="name"
|
||||
subtitleKey="relationship"
|
||||
isEditable
|
||||
isHideable
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@ -164,7 +241,19 @@ export const right: SidebarSection[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const getCustomSections = (sections: Record<string, SectionRecord>): Array<Required<SectionRecord>> => {
|
||||
export const getSectionsByType = (sections: Record<string, SectionRecord>, type: SectionType): SectionRecord[] => {
|
||||
if (isEmpty(sections)) return [];
|
||||
|
||||
return Object.entries(sections).reduce((acc, [id, section]) => {
|
||||
if (section.type.startsWith(type) && section.isDuplicated) {
|
||||
return [...acc, { ...section, id }];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as SectionRecord[]);
|
||||
};
|
||||
|
||||
export const getCustomSections = (sections: Record<string, SectionRecord>): SectionRecord[] => {
|
||||
if (isEmpty(sections)) return [];
|
||||
|
||||
return Object.entries(sections).reduce((acc, [id, section]) => {
|
||||
@ -173,7 +262,7 @@ export const getCustomSections = (sections: Record<string, SectionRecord>): Arra
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as Array<Required<SectionRecord>>);
|
||||
}, [] as SectionRecord[]);
|
||||
};
|
||||
|
||||
const sections = [...left, ...right];
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createTheme } from '@mui/material';
|
||||
import { createTheme, ThemeOptions } from '@mui/material/styles';
|
||||
|
||||
const theme = createTheme({
|
||||
const theme: ThemeOptions = {
|
||||
typography: {
|
||||
fontSize: 12,
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
@ -49,7 +49,7 @@ const theme = createTheme({
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const lightTheme = createTheme({
|
||||
...theme,
|
||||
|
||||
@ -9,9 +9,15 @@ export const VALID_URL_REGEX = /[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}
|
||||
export const FILENAME_TIMESTAMP = 'DDMMYYYYHHmmss';
|
||||
|
||||
// Links
|
||||
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.';
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import env from '@beam-australia/react-env';
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Google, Login, Visibility, VisibilityOff } from '@mui/icons-material';
|
||||
import { Login, Visibility, VisibilityOff } from '@mui/icons-material';
|
||||
import { Button, IconButton, InputAdornment, TextField } from '@mui/material';
|
||||
import { CredentialResponse, GoogleLogin } from '@react-oauth/google';
|
||||
import Joi from 'joi';
|
||||
import { isEmpty } from 'lodash';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
@ -17,8 +18,6 @@ import { ServerError } from '@/services/axios';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setModalState } from '@/store/modal/modalSlice';
|
||||
|
||||
declare const google: any;
|
||||
|
||||
type FormData = {
|
||||
identifier: string;
|
||||
password: string;
|
||||
@ -63,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();
|
||||
};
|
||||
@ -85,18 +77,16 @@ const LoginModal: React.FC = () => {
|
||||
dispatch(setModalState({ modal: 'auth.forgot', state: { open: true } }));
|
||||
};
|
||||
|
||||
const handleLoginWithGoogle = async () => {
|
||||
google.accounts.id.initialize({
|
||||
client_id: env('GOOGLE_CLIENT_ID'),
|
||||
callback: async (response: any) => {
|
||||
await loginWithGoogleMutation({ credential: response.credential });
|
||||
const handleLoginWithGoogle = async (response: CredentialResponse) => {
|
||||
if (response.credential) {
|
||||
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleGoogleLoginError });
|
||||
|
||||
handleClose();
|
||||
},
|
||||
auto_select: false,
|
||||
});
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
google.accounts.id.prompt();
|
||||
const handleGoogleLoginError = () => {
|
||||
toast.error("Google doesn't seem to be responding, please try logging in using email/password instead.");
|
||||
};
|
||||
|
||||
const PasswordVisibility = (): React.ReactElement => {
|
||||
@ -120,15 +110,7 @@ const LoginModal: React.FC = () => {
|
||||
footerChildren={
|
||||
<div className="flex gap-4">
|
||||
{!isEmpty(env('GOOGLE_CLIENT_ID')) && (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outlined"
|
||||
disabled={isLoading}
|
||||
startIcon={<Google />}
|
||||
onClick={handleLoginWithGoogle}
|
||||
>
|
||||
{t<string>('modals.auth.login.actions.google')}
|
||||
</Button>
|
||||
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleGoogleLoginError} />
|
||||
)}
|
||||
|
||||
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
||||
@ -173,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>
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import env from '@beam-australia/react-env';
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Google, HowToReg } from '@mui/icons-material';
|
||||
import { HowToReg } from '@mui/icons-material';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { CredentialResponse, GoogleLogin } from '@react-oauth/google';
|
||||
import Joi from 'joi';
|
||||
import { isEmpty } from 'lodash';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import BaseModal from '@/components/shared/BaseModal';
|
||||
@ -14,8 +16,6 @@ import { ServerError } from '@/services/axios';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { setModalState } from '@/store/modal/modalSlice';
|
||||
|
||||
declare const google: any;
|
||||
|
||||
type FormData = {
|
||||
name: string;
|
||||
username: string;
|
||||
@ -79,18 +79,16 @@ const RegisterModal: React.FC = () => {
|
||||
dispatch(setModalState({ modal: 'auth.login', state: { open: true } }));
|
||||
};
|
||||
|
||||
const handleLoginWithGoogle = async () => {
|
||||
google.accounts.id.initialize({
|
||||
client_id: env('GOOGLE_CLIENT_ID'),
|
||||
callback: async (response: any) => {
|
||||
await loginWithGoogleMutation({ credential: response.credential });
|
||||
const handleLoginWithGoogle = async (response: CredentialResponse) => {
|
||||
if (response.credential) {
|
||||
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleGoogleLoginError });
|
||||
|
||||
handleClose();
|
||||
},
|
||||
auto_select: false,
|
||||
});
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
google.accounts.id.prompt();
|
||||
const handleGoogleLoginError = () => {
|
||||
toast("Google doesn't seem to be responding, please try logging in using email/password instead.");
|
||||
};
|
||||
|
||||
return (
|
||||
@ -102,15 +100,7 @@ const RegisterModal: React.FC = () => {
|
||||
footerChildren={
|
||||
<div className="flex gap-4">
|
||||
{!isEmpty(env('GOOGLE_CLIENT_ID')) && (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outlined"
|
||||
disabled={isLoading}
|
||||
startIcon={<Google />}
|
||||
onClick={handleLoginWithGoogle}
|
||||
>
|
||||
{t<string>('modals.auth.register.actions.google')}
|
||||
</Button>
|
||||
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleGoogleLoginError} />
|
||||
)}
|
||||
|
||||
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
||||
|
||||
159
client/modals/auth/UserProfileModal.tsx
Normal file
159
client/modals/auth/UserProfileModal.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
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('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('modals.auth.profile.form.email.label')}
|
||||
error={!!fieldState.error}
|
||||
helperText={t('modals.auth.profile.form.email.help-text')}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button onClick={handleUpdate}>{t('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('modals.auth.profile.delete-account.heading')}</h5>
|
||||
</div>
|
||||
|
||||
<p className="text-xs opacity-75">{t('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('modals.auth.profile.delete-account.actions.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfileModal;
|
||||
@ -1,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import DatePicker from '@mui/lab/DatePicker';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { Award, SectionPath } from '@reactive-resume/schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
@ -44,7 +44,7 @@ const AwardModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
@ -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).isValid() && field.onChange(date.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,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import DatePicker from '@mui/lab/DatePicker';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { Certificate, SectionPath } from '@reactive-resume/schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
@ -44,7 +44,7 @@ const CertificateModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
|
||||
@ -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).isValid() && field.onChange(date.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,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import DatePicker from '@mui/lab/DatePicker';
|
||||
import { Button, Slider, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { Custom } from '@reactive-resume/schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
@ -60,13 +60,14 @@ const CustomModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${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).isValid() && field.onChange(date.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).isValid() && field.onChange(date.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,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import DatePicker from '@mui/lab/DatePicker';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { Education, SectionPath } from '@reactive-resume/schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
@ -57,7 +57,7 @@ const EducationModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
@ -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).isValid() && field.onChange(date.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).isValid() && field.onChange(date.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>
|
||||
);
|
||||
|
||||
@ -35,7 +35,7 @@ const InterestModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -36,7 +36,7 @@ const LanguageModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import DatePicker from '@mui/lab/DatePicker';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { Project, SectionPath } from '@reactive-resume/schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
@ -53,7 +53,7 @@ const ProjectModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
@ -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).isValid() && field.onChange(date.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).isValid() && field.onChange(date.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,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import DatePicker from '@mui/lab/DatePicker';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { Publication, SectionPath } from '@reactive-resume/schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
@ -44,7 +44,7 @@ const PublicationModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
@ -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).isValid() && field.onChange(date.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>
|
||||
);
|
||||
|
||||
@ -41,7 +41,7 @@ const ReferenceModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -39,7 +39,7 @@ const SkillModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
@ -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,7 +1,7 @@
|
||||
import { joiResolver } from '@hookform/resolvers/joi';
|
||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||
import DatePicker from '@mui/lab/DatePicker';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { SectionPath, Volunteer } from '@reactive-resume/schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
@ -50,7 +50,7 @@ const VolunteerModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
||||
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||
|
||||
const item: FormData = get(payload, 'item', null);
|
||||
@ -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).isValid() && field.onChange(date.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).isValid() && field.onChange(date.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 DatePicker from '@mui/lab/DatePicker';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { SectionPath, WorkExperience } from '@reactive-resume/schema';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { WorkExperience } from '@reactive-resume/schema';
|
||||
import dayjs from 'dayjs';
|
||||
import Joi from 'joi';
|
||||
import get from 'lodash/get';
|
||||
@ -20,8 +20,6 @@ import { addItem, editItem } from '@/store/resume/resumeSlice';
|
||||
|
||||
type FormData = WorkExperience;
|
||||
|
||||
const path: SectionPath = 'sections.work';
|
||||
|
||||
const defaultState: FormData = {
|
||||
name: '',
|
||||
position: '',
|
||||
@ -50,14 +48,23 @@ const WorkModal: React.FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const heading = useAppSelector((state) => get(state.resume, `${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`, heading) }),
|
||||
[t, heading]
|
||||
);
|
||||
const editText = useMemo(
|
||||
() =>
|
||||
t<string>('builder.common.actions.edit', { token: t<string>(`builder.leftSidebar.${path}.heading`, heading) }),
|
||||
[t, heading]
|
||||
);
|
||||
|
||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||
defaultValues: defaultState,
|
||||
@ -77,7 +84,7 @@ const WorkModal: React.FC = () => {
|
||||
const handleClose = () => {
|
||||
dispatch(
|
||||
setModalState({
|
||||
modal: `builder.${path}`,
|
||||
modal: 'builder.sections.work',
|
||||
state: { open: false },
|
||||
})
|
||||
);
|
||||
@ -99,7 +106,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}
|
||||
@ -134,21 +141,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).isValid() && field.onChange(date.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 +167,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).isValid() && field.onChange(date.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 +219,7 @@ const WorkModal: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<input type="submit" style={{ display: 'none' }} />
|
||||
</form>
|
||||
</BaseModal>
|
||||
);
|
||||
|
||||
@ -6,7 +6,6 @@ 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,14 +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);
|
||||
|
||||
queryClient.invalidateQueries(RESUMES_QUERY);
|
||||
handleClose();
|
||||
} catch (error: any) {
|
||||
toast.error(error.message);
|
||||
}
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
|
||||
@ -68,8 +68,8 @@ const ImportExternalModal: React.FC = () => {
|
||||
}
|
||||
|
||||
await mutateAsync({ integration, file });
|
||||
|
||||
queryClient.invalidateQueries(RESUMES_QUERY);
|
||||
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
@ -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,23 +4,45 @@ const i18nConfig = {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: [
|
||||
'am',
|
||||
'ar',
|
||||
'bg',
|
||||
'bn',
|
||||
'ca',
|
||||
'cs',
|
||||
'da',
|
||||
'de',
|
||||
'el',
|
||||
'en',
|
||||
'es',
|
||||
'fa',
|
||||
'fi',
|
||||
'fr',
|
||||
'he',
|
||||
'hi',
|
||||
'hu',
|
||||
'id',
|
||||
'it',
|
||||
'ja',
|
||||
'km',
|
||||
'kn',
|
||||
'ko',
|
||||
'ml',
|
||||
'mr',
|
||||
'ne',
|
||||
'nl',
|
||||
'no',
|
||||
'or',
|
||||
'pl',
|
||||
'pt',
|
||||
'pt-BR',
|
||||
'ro',
|
||||
'ru',
|
||||
'sr',
|
||||
'sv',
|
||||
'ta',
|
||||
'tr',
|
||||
'uk',
|
||||
'vi',
|
||||
'zh',
|
||||
],
|
||||
|
||||
@ -15,19 +15,6 @@ const nextConfig = {
|
||||
domains: ['cdn.rxresu.me', 'www.gravatar.com'],
|
||||
},
|
||||
|
||||
async rewrites() {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return [
|
||||
{
|
||||
source: '/api/:path*',
|
||||
destination: 'http://localhost:3100/:path*',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
|
||||
// Hack to make Tailwind darkMode 'class' strategy with CSS Modules
|
||||
// Ref: https://github.com/tailwindlabs/tailwindcss/issues/3258#issuecomment-968368156
|
||||
webpack: (config) => {
|
||||
|
||||
@ -2,77 +2,81 @@
|
||||
"name": "@reactive-resume/client",
|
||||
"scripts": {
|
||||
"dev": "react-env --prefix PUBLIC -- next dev",
|
||||
"lint": "next lint --fix",
|
||||
"build": "next build && npm run sitemap",
|
||||
"start": "react-env --prefix PUBLIC -- next start",
|
||||
"lint": "next lint --fix",
|
||||
"sitemap": "next-sitemap --config next-sitemap.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@beam-australia/react-env": "^3.1.1",
|
||||
"@emotion/css": "^11.9.0",
|
||||
"@emotion/react": "^11.9.0",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@hookform/resolvers": "2.8.8",
|
||||
"@monaco-editor/react": "^4.4.4",
|
||||
"@mui/icons-material": "^5.6.2",
|
||||
"@mui/lab": "^5.0.0-alpha.79",
|
||||
"@mui/material": "^5.6.3",
|
||||
"@reduxjs/toolkit": "^1.8.1",
|
||||
"axios": "^0.27.2",
|
||||
"clsx": "^1.1.1",
|
||||
"dayjs": "^1.11.1",
|
||||
"@emotion/css": "^11.10.6",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@hello-pangea/dnd": "^16.2.0",
|
||||
"@hookform/resolvers": "3.0.1",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@mui/icons-material": "^5.11.16",
|
||||
"@mui/lab": "^5.0.0-alpha.125",
|
||||
"@mui/material": "^5.11.16",
|
||||
"@mui/system": "^5.11.16",
|
||||
"@mui/x-date-pickers": "6.0.4",
|
||||
"@react-oauth/google": "^0.9.0",
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"axios": "^1.3.5",
|
||||
"clsx": "^1.2.1",
|
||||
"dayjs": "^1.11.7",
|
||||
"downloadjs": "^1.4.7",
|
||||
"joi": "^17.6.0",
|
||||
"joi": "^17.9.1",
|
||||
"lodash": "^4.17.21",
|
||||
"md5-hex": "^4.0.0",
|
||||
"monaco-editor": "^0.33.0",
|
||||
"nanoid": "^3.3.3",
|
||||
"next": "12.1.5",
|
||||
"next-i18next": "^11.0.0",
|
||||
"react": "^18",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-colorful": "^5.5.1",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.30.0",
|
||||
"react-hot-toast": "2.2.0",
|
||||
"react-hotkeys-hook": "^3.4.4",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-query": "^3.38.0",
|
||||
"react-redux": "^8.0.1",
|
||||
"react-zoom-pan-pinch": "^2.1.3",
|
||||
"redux": "^4.2.0",
|
||||
"monaco-editor": "^0.37.0",
|
||||
"nanoid": "3.3.4",
|
||||
"next": "13.2.4",
|
||||
"next-i18next": "^13.2.2",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dnd": "16.0.1",
|
||||
"react-dnd-html5-backend": "16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-hot-toast": "2.4.0",
|
||||
"react-icons": "^4.8.0",
|
||||
"react-markdown": "^8.0.6",
|
||||
"react-query": "^3.39.3",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-zoom-pan-pinch": "^3.0.6",
|
||||
"redux": "^4.2.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.1.3",
|
||||
"redux-saga": "^1.2.3",
|
||||
"redux-undo": "^1.0.1",
|
||||
"rehype-katex": "^6.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sharp": "^0.30.4",
|
||||
"uuid": "^8.3.2",
|
||||
"remark-math": "^5.1.1",
|
||||
"sharp": "^0.32.0",
|
||||
"uuid": "^9.0.0",
|
||||
"webfontloader": "^1.6.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.10",
|
||||
"@babel/core": "^7.21.4",
|
||||
"@reactive-resume/schema": "workspace:*",
|
||||
"@tailwindcss/typography": "^0.5.2",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/downloadjs": "^1.4.3",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/node": "17.0.30",
|
||||
"@types/react": "^18",
|
||||
"@types/react-beautiful-dnd": "^13.1.2",
|
||||
"@types/react-redux": "^7.1.24",
|
||||
"@types/tailwindcss": "^3.0.10",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/webfontloader": "^1.6.34",
|
||||
"autoprefixer": "^10.4.5",
|
||||
"csstype": "^3.0.11",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-config-next": "12.1.5",
|
||||
"next-sitemap": "^2.5.20",
|
||||
"postcss": "^8.4.13",
|
||||
"prettier": "^2.6.2",
|
||||
"sass": "^1.51.0",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"typescript": "^4.6.4"
|
||||
"@types/lodash": "^4.14.192",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/react": "^18.0.33",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@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.2.4",
|
||||
"eslint-plugin-tailwindcss": "^3.11.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"next-sitemap": "^4.0.6",
|
||||
"postcss": "^8.4.21",
|
||||
"sass": "^1.60.0",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"typescript": "^5.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { Download, Downloading } from '@mui/icons-material';
|
||||
import { ButtonBase } from '@mui/material';
|
||||
import { Resume } from '@reactive-resume/schema';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import download from 'downloadjs';
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
@ -14,6 +15,7 @@ import toast from 'react-hot-toast';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
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';
|
||||
@ -51,7 +53,7 @@ const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData && !isEmpty(initialData)) {
|
||||
@ -59,6 +61,16 @@ const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
|
||||
}
|
||||
}, [dispatch, initialData]);
|
||||
|
||||
useEffect(() => {
|
||||
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, router]);
|
||||
|
||||
useQuery<Resume>(`resume/${username}/${slug}`, () => fetchResumeByIdentifier({ username, slug }), {
|
||||
initialData,
|
||||
retry: false,
|
||||
@ -88,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');
|
||||
|
||||
download(`/api${url}`);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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: {
|
||||
@ -59,18 +59,20 @@ const Printer: NextPage<Props> = ({ resume: initialData, locale }) => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const resume = useAppSelector((state) => state.resume);
|
||||
const resume = useAppSelector((state) => state.resume.present);
|
||||
|
||||
useEffect(() => {
|
||||
if (router.locale !== locale) {
|
||||
const { pathname, asPath, query } = router;
|
||||
|
||||
router.push({ pathname, query }, asPath, { locale });
|
||||
}
|
||||
}, [router, locale]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData) dispatch(setResume(initialData));
|
||||
}, [dispatch, initialData]);
|
||||
|
||||
useEffect(() => {
|
||||
const { pathname, asPath, query } = router;
|
||||
|
||||
router.push({ pathname, query }, asPath, { locale });
|
||||
}, [router, locale]);
|
||||
|
||||
if (!resume || isEmpty(resume)) return null;
|
||||
|
||||
const layout: string[][][] = get(resume, 'metadata.layout', []);
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import '@/styles/globals.scss';
|
||||
|
||||
import DateAdapter from '@mui/lab/AdapterDayjs';
|
||||
import LocalizationProvider from '@mui/lab/LocalizationProvider';
|
||||
import env from '@beam-australia/react-env';
|
||||
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';
|
||||
import Script from 'next/script';
|
||||
import { appWithTranslation } from 'next-i18next';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { QueryClientProvider } from 'react-query';
|
||||
@ -17,23 +18,23 @@ import queryClient from '@/services/react-query';
|
||||
import store, { persistor } from '@/store/index';
|
||||
import WrapperRegistry from '@/wrappers/index';
|
||||
|
||||
const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Reactive Resume</title>
|
||||
const App = ({ Component, pageProps }: AppProps): JSX.Element => (
|
||||
<>
|
||||
<Head>
|
||||
<title>Reactive Resume | A free and open source resume builder</title>
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content="Reactive Resume is a free and open source resume builder that's built to make the mundane tasks of creating, updating and sharing your resume as easy as 1, 2, 3."
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
</Head>
|
||||
<meta
|
||||
name="description"
|
||||
content="Reactive Resume is a free and open source resume builder that's built to make the mundane tasks of creating, updating and sharing your resume as easy as 1, 2, 3."
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
</Head>
|
||||
|
||||
<ReduxProvider store={store}>
|
||||
<LocalizationProvider dateAdapter={DateAdapter}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<ReduxProvider store={store}>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<GoogleOAuthProvider clientId={env('GOOGLE_CLIENT_ID')}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<WrapperRegistry>
|
||||
<Loading />
|
||||
@ -50,13 +51,11 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
/>
|
||||
</WrapperRegistry>
|
||||
</QueryClientProvider>
|
||||
</PersistGate>
|
||||
</LocalizationProvider>
|
||||
</ReduxProvider>
|
||||
|
||||
<Script src="https://accounts.google.com/gsi/client" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
</GoogleOAuthProvider>
|
||||
</PersistGate>
|
||||
</LocalizationProvider>
|
||||
</ReduxProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
export default appWithTranslation(App);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -4,7 +4,9 @@ import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useEffect } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { ActionCreators } from 'redux-undo';
|
||||
|
||||
import ResumeCard from '@/components/dashboard/ResumeCard';
|
||||
import ResumePreview from '@/components/dashboard/ResumePreview';
|
||||
@ -12,21 +14,26 @@ import Avatar from '@/components/shared/Avatar';
|
||||
import Logo from '@/components/shared/Logo';
|
||||
import { RESUMES_QUERY } from '@/constants/index';
|
||||
import { fetchResumes } from '@/services/resume';
|
||||
import { useAppDispatch } from '@/store/hooks';
|
||||
import styles from '@/styles/pages/Dashboard.module.scss';
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({ locale = 'en' }) => {
|
||||
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();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { data } = useQuery(RESUMES_QUERY, fetchResumes);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(ActionCreators.clearHistory());
|
||||
}, []);
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
return (
|
||||
@ -39,9 +46,7 @@ const Dashboard: NextPage = () => {
|
||||
|
||||
<header>
|
||||
<Link href="/">
|
||||
<a>
|
||||
<Logo size={40} />
|
||||
</a>
|
||||
<Logo size={40} />
|
||||
</Link>
|
||||
|
||||
<Avatar size={40} />
|
||||
@ -49,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, 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>
|
||||
@ -170,6 +171,18 @@ const Home: NextPage = () => {
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<a href={DOCS_URL} target="_blank" rel="noreferrer">
|
||||
<Button variant="text" startIcon={<LinkIcon />}>
|
||||
{t<string>('landing.links.links.docs')}
|
||||
</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')}
|
||||
@ -180,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 { NextPage } from 'next';
|
||||
|
||||
const PrivacyPolicy: NextPage = () => (
|
||||
<div className="mx-auto my-12 prose dark:prose-invert">
|
||||
<div className="prose mx-auto my-12 dark:prose-invert">
|
||||
<h1>Privacy Policy</h1>
|
||||
|
||||
<p>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { NextPage } from 'next';
|
||||
|
||||
const TermsOfService: NextPage = () => (
|
||||
<div className="mx-auto my-12 prose dark:prose-invert">
|
||||
<div className="prose mx-auto my-12 dark:prose-invert">
|
||||
<h1>Terms of Service</h1>
|
||||
|
||||
<p>
|
||||
|
||||
@ -2,14 +2,15 @@ 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';
|
||||
import { GetServerSideProps, NextPage } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
import Page from '@/components/build/Center/Page';
|
||||
@ -35,6 +36,8 @@ export const getServerSideProps: GetServerSideProps<Props> = async ({ query, loc
|
||||
};
|
||||
|
||||
const Preview: NextPage<Props> = ({ shortId }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { data: resume } = useQuery<Resume>(`resume/${shortId}`, () => fetchResumeByShortId({ shortId }), {
|
||||
@ -52,18 +55,26 @@ const Preview: NextPage<Props> = ({ shortId }) => {
|
||||
if (resume) dispatch(setResume(resume));
|
||||
}, [resume, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resume && !isEmpty(resume) && router.locale !== resume.metadata.locale) {
|
||||
const { pathname, asPath, query } = router;
|
||||
|
||||
router.push({ pathname, query }, asPath, { locale: resume.metadata.locale });
|
||||
}
|
||||
}, [resume, router]);
|
||||
|
||||
if (!resume || isEmpty(resume)) return null;
|
||||
|
||||
const layout: string[][][] = get(resume, 'metadata.layout', []);
|
||||
|
||||
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(`/api${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 |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user