mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-11 04:52:36 +10:00
Compare commits
798 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 87d381fe8e | |||
| ccfb4d3cb0 | |||
| 763074a86c | |||
| 0f46895711 | |||
| aa736af0f5 | |||
| 1d9056f935 | |||
| 9cadd603f3 | |||
| b7b62d7bd0 | |||
| 820e6c90d3 | |||
| ea642d1b60 | |||
| ec006779a8 | |||
| 515be23c44 | |||
| c11aec8b44 | |||
| 3c2147e72c | |||
| 15a35e6243 | |||
| d53a5a492c | |||
| 0810e5ae6a | |||
| 881b183db5 | |||
| 15cea02872 | |||
| c195561df0 | |||
| fc725cfc0c | |||
| 9f54516e8c | |||
| 68a4cd9635 | |||
| ff01802f2f | |||
| bb900bc2e1 | |||
| 459f82b66b | |||
| 4b382243e4 | |||
| af074085d1 | |||
| 0c8c872668 | |||
| ddb29bb40d | |||
| aecb627ab7 | |||
| b8cd53cb59 | |||
| e61f6153c3 | |||
| 386e8ab902 | |||
| 5e8f02e3ca | |||
| f219562e72 | |||
| 29d94dfc14 | |||
| 622f5fc28c | |||
| 647f01e25c | |||
| 5a79c0e5c2 | |||
| 2a4c298572 | |||
| 1e59f73f79 | |||
| feb911aea0 | |||
| d0863d68c6 | |||
| 447d9b3ca1 | |||
| 86e66eb6a0 | |||
| b2c9515a63 | |||
| db04c5caee | |||
| 33526d5d13 | |||
| fc77b548d8 | |||
| bf7a168f2e | |||
| 17b1551bab | |||
| 8864243558 | |||
| 37aab7a16f | |||
| 86e1bdf7ea | |||
| 4547fd213d | |||
| 5aacec40cc | |||
| 1df78100ca | |||
| 9cd36fcb9b | |||
| 24b32eb917 | |||
| dec0e41fec | |||
| 42700ad2b2 | |||
| df51d79f6b | |||
| be1673a6a7 | |||
| 648f182e76 | |||
| 3aa56f0886 | |||
| b795534da7 | |||
| c67e2ac9f8 | |||
| beb418bd5d | |||
| 2b3d9533b0 | |||
| b061f139bd | |||
| ac569324cf | |||
| 357d197bb3 | |||
| 5eed1186ff | |||
| a87a9b3247 | |||
| 7f1c82cd91 | |||
| 048c1ed3ed | |||
| 9a2570d7e7 | |||
| 00b9c2156d | |||
| ff8b22274f | |||
| 786937f847 | |||
| c95efee8ec | |||
| 776d2f79a6 | |||
| 25a6b8cce6 | |||
| f6d7cae17b | |||
| 944a0b5fb1 | |||
| 7769653224 | |||
| ccdc5b5fae | |||
| 20158f573e | |||
| 87c60729b5 | |||
| a03a50b7c6 | |||
| fb85ccf501 | |||
| 3179442d8f | |||
| 33d3c52cd9 | |||
| 1d33e01a43 | |||
| 52ff221dd1 | |||
| 5afe178e23 | |||
| 9118b76084 | |||
| 5a62b527b9 | |||
| 2e9e14dc72 | |||
| 0a0b4893aa | |||
| 6277f81e26 | |||
| d550150787 | |||
| 7626b2153f | |||
| 6d17d1001d | |||
| 0273738d7a | |||
| 322df25ecc | |||
| ab3867d9a8 | |||
| 9bf8ec88f4 | |||
| 685f4d37a6 | |||
| f3b3fe8ac9 | |||
| d5fa49172a | |||
| b8303b9977 | |||
| 16d06c6356 | |||
| 79ddd887d9 | |||
| c394bc6725 | |||
| 9e6d7630f4 | |||
| e2fbdd3c2f | |||
| 849171af8f | |||
| 884975dda6 | |||
| 03cbf22c9b | |||
| a10cee2efa | |||
| 479c94a11d | |||
| c057f31e97 | |||
| d0bc9db6e5 | |||
| e2dd8dd1d7 | |||
| f2ff12faa6 | |||
| 50cc3d7da8 | |||
| 60b1f7a816 | |||
| 33d2bf043b | |||
| 86b20dcae6 | |||
| caf4936c9b | |||
| 7e864d2447 | |||
| ff324688f6 | |||
| efaeb1b341 | |||
| 488cb7f8a2 | |||
| 974fa08651 | |||
| 8f3312e8a8 | |||
| 57d5da0490 | |||
| daeb67319e | |||
| 213665bd1d | |||
| dfc48d6aa9 | |||
| d71d40453f | |||
| 635afbc892 | |||
| e90037e363 | |||
| a730359736 | |||
| 80acfe97c7 | |||
| b6267d07ba | |||
| 910f764823 | |||
| 7a8f302c21 | |||
| fb0c3b55c1 | |||
| f9579855a9 | |||
| 0dd1e2720a | |||
| 331d2d3d26 | |||
| f56554c2d4 | |||
| 98131b389c | |||
| 7cfe6288e1 | |||
| 84041ef2ff | |||
| 9a2af8079e | |||
| 633162d9af | |||
| 50baa0227d | |||
| 18da00f2e2 | |||
| f4f0b2c4b5 | |||
| b7d3007d31 | |||
| 67384981c1 | |||
| 4390bccfb9 | |||
| 8f5632c5ad | |||
| 1facd2ad11 | |||
| 0e1e2bbe4e | |||
| 3a2e62be4c | |||
| 697ceef8f2 | |||
| c8e81a456d | |||
| 2b334e5c5a | |||
| 90321e1284 | |||
| 9bcddb4b5c | |||
| 72fdc05f69 | |||
| e1d6540500 | |||
| 4b17719c69 | |||
| da056307dd | |||
| e4950728d8 | |||
| dac4e862b8 | |||
| 5fa45ef5bd | |||
| 9e6dafc8ca | |||
| a02b85b4bb | |||
| b3ff7805cd | |||
| 7f0ee40af4 | |||
| 39fa6da5dd | |||
| 7fd96a4540 | |||
| 8f5832b2ca | |||
| 58ce09ee06 | |||
| 3f5323d5a3 | |||
| d62482b280 | |||
| a609ea551a | |||
| 1f8e3647d3 | |||
| 76975ddc6c | |||
| 6ed0bb62b4 | |||
| 11d15d8dbb | |||
| 7cf92ddb81 | |||
| d907b36d59 | |||
| 307b626189 | |||
| f573e60079 | |||
| d3c52476f7 | |||
| 4f9d2ea846 | |||
| ec617d682e | |||
| 72d3d46e88 | |||
| 110797da9d | |||
| ab90a2e1dd | |||
| 1a3c950847 | |||
| 7fcc792255 | |||
| 97a13f9f41 | |||
| 29f1afac9a | |||
| c5d0abdc79 | |||
| 5a60c99df9 | |||
| c46b8fc162 |
@ -1,21 +1,27 @@
|
|||||||
|
# Android App
|
||||||
|
/app
|
||||||
|
|
||||||
# Build Artifacts
|
# Build Artifacts
|
||||||
dist
|
**/.turbo
|
||||||
.next
|
/server/dist
|
||||||
|
/client/.next
|
||||||
|
|
||||||
# IDEs
|
# IDEs
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
# Project Metadata
|
# Project Metadata
|
||||||
|
.crowdin.yml
|
||||||
|
|
||||||
|
# Documentation
|
||||||
README.md
|
README.md
|
||||||
|
SECURITY.md
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
|
CODE_OF_CONDUCT.md
|
||||||
|
|
||||||
# Project Dependencies
|
# Project Dependencies
|
||||||
node_modules
|
**/node_modules
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
Dockerfile
|
Dockerfile
|
||||||
.dockerignore
|
.dockerignore
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
|
||||||
# Android App
|
|
||||||
/app
|
|
||||||
50
.env.example
50
.env.example
@ -1,30 +1,36 @@
|
|||||||
# App
|
# Server + Client
|
||||||
TZ=UTC
|
TZ=UTC
|
||||||
SECRET_KEY=change-me
|
PUBLIC_URL=http://localhost:3000
|
||||||
|
PUBLIC_SERVER_URL=http://localhost:3100
|
||||||
|
PUBLIC_GOOGLE_CLIENT_ID=
|
||||||
|
|
||||||
# URLs
|
# Server + Database
|
||||||
PUBLIC_URL=http://<SERVER-IP>
|
POSTGRES_DB=postgres
|
||||||
PUBLIC_SERVER_URL=http://<SERVER-IP>/api
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_PASSWORD=postgres
|
||||||
|
|
||||||
# Database
|
# Server
|
||||||
|
SECRET_KEY=
|
||||||
POSTGRES_HOST=localhost
|
POSTGRES_HOST=localhost
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
POSTGRES_USERNAME=postgres
|
|
||||||
POSTGRES_PASSWORD=postgres
|
|
||||||
POSTGRES_DATABASE=postgres
|
|
||||||
POSTGRES_SSL_CERT=
|
POSTGRES_SSL_CERT=
|
||||||
|
JWT_SECRET=
|
||||||
# Auth
|
|
||||||
JWT_SECRET=change-me
|
|
||||||
JWT_EXPIRY_TIME=604800
|
JWT_EXPIRY_TIME=604800
|
||||||
|
GOOGLE_CLIENT_SECRET=
|
||||||
|
GOOGLE_API_KEY=
|
||||||
|
MAIL_FROM_NAME=
|
||||||
|
MAIL_FROM_EMAIL=
|
||||||
|
MAIL_HOST=
|
||||||
|
MAIL_PORT=
|
||||||
|
MAIL_USERNAME=
|
||||||
|
MAIL_PASSWORD=
|
||||||
|
STORAGE_BUCKET=
|
||||||
|
STORAGE_REGION=
|
||||||
|
STORAGE_ENDPOINT=
|
||||||
|
STORAGE_URL_PREFIX=
|
||||||
|
STORAGE_ACCESS_KEY=
|
||||||
|
STORAGE_SECRET_KEY=
|
||||||
|
PDF_DELETION_TIME=345600000
|
||||||
|
|
||||||
# Google
|
# Client
|
||||||
PUBLIC_GOOGLE_CLIENT_ID=change-me
|
PUBLIC_FLAG_DISABLE_SIGNUPS=false
|
||||||
GOOGLE_CLIENT_SECRET=change-me
|
|
||||||
GOOGLE_API_KEY=change-me
|
|
||||||
|
|
||||||
# SendGrid (Optional)
|
|
||||||
SENDGRID_API_KEY=
|
|
||||||
SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID=
|
|
||||||
SENDGRID_FROM_NAME=
|
|
||||||
SENDGRID_FROM_EMAIL=
|
|
||||||
|
|||||||
@ -1,31 +1,23 @@
|
|||||||
{
|
{
|
||||||
"root": true,
|
|
||||||
"ignorePatterns": ["/app"],
|
"ignorePatterns": ["/app"],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"extends": ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
|
"extends": ["plugin:@typescript-eslint/recommended"],
|
||||||
"plugins": ["@typescript-eslint/eslint-plugin", "simple-import-sort", "unused-imports"],
|
"plugins": ["@typescript-eslint/eslint-plugin", "simple-import-sort"],
|
||||||
"rules": {
|
"rules": {
|
||||||
// TypeScript ESLint
|
// ESLint
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"@typescript-eslint/interface-name-prefix": "off",
|
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
|
||||||
// Simple Import Sort
|
// Simple Import Sort
|
||||||
"simple-import-sort/imports": "error",
|
"simple-import-sort/imports": "error",
|
||||||
"simple-import-sort/exports": "error",
|
"simple-import-sort/exports": "error",
|
||||||
// Unused Imports
|
|
||||||
"no-unused-vars": "off",
|
// TypeScript ESLint
|
||||||
"unused-imports/no-unused-imports": "error",
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
"unused-imports/no-unused-vars": [
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"warn",
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
{
|
"@typescript-eslint/interface-name-prefix": "off",
|
||||||
"vars": "all",
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
"varsIgnorePattern": "^_",
|
"@typescript-eslint/explicit-module-boundary-types": "off"
|
||||||
"args": "none",
|
|
||||||
"argsIgnorePattern": "^_"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
|
|||||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1 +1,2 @@
|
|||||||
|
github: AmruthPillai
|
||||||
custom: https://paypal.me/RajaRajanA
|
custom: https://paypal.me/RajaRajanA
|
||||||
|
|||||||
35
.github/ISSUE_TEMPLATE/bug-report.md
vendored
35
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@ -1,38 +1,43 @@
|
|||||||
---
|
---
|
||||||
name: Bug Report
|
name: Bug Report
|
||||||
about: Create a report to help improve
|
about: Create a report to help improve
|
||||||
title: "[BUG] "
|
title: '[BUG] '
|
||||||
labels: bug
|
labels: bug
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
<!-- A clear and concise description of what the bug is. -->
|
||||||
|
|
||||||
|
**Product Flavor**
|
||||||
|
|
||||||
|
- [ ] Managed (https://rxresu.me)
|
||||||
|
- [ ] Self Hosted
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
|
||||||
|
<!-- Steps to reproduce the behavior: -->
|
||||||
|
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
4. See error
|
4. See error
|
||||||
|
|
||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
<!-- A clear and concise description of what you expected to happen. -->
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
**Desktop (please complete the following information):**
|
||||||
- OS: [e.g. iOS]
|
|
||||||
- Browser [e.g. chrome, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Smartphone (please complete the following information):**
|
- OS: <!--[e.g. iOS]-->
|
||||||
- Device: [e.g. iPhone6]
|
- Browser <!--[e.g. chrome, safari]-->
|
||||||
- OS: [e.g. iOS8.1]
|
- Version <!--[e.g. 22]-->
|
||||||
- Browser [e.g. stock browser, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
|
||||||
|
<!-- Add any other context about the problem here. -->
|
||||||
|
|||||||
15
.github/ISSUE_TEMPLATE/feature-request.md
vendored
15
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@ -1,20 +1,23 @@
|
|||||||
---
|
---
|
||||||
name: Feature Request
|
name: Feature Request
|
||||||
about: Suggest an idea for this project
|
about: Suggest an idea for this project
|
||||||
title: "[FEATURE] "
|
title: '[FEATURE] '
|
||||||
labels: enhancement
|
labels: enhancement
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
**Is your feature request related to a problem? Please describe.**
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
**Describe the solution you'd like**
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
<!-- A clear and concise description of what you want to happen. -->
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
**Describe alternatives you've considered**
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
|
<!-- Add any other context or screenshots about the feature request here. -->
|
||||||
|
|||||||
22
.github/dependabot.yml
vendored
Normal file
22
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: 'docker'
|
||||||
|
directory: '/server'
|
||||||
|
schedule:
|
||||||
|
interval: 'weekly'
|
||||||
|
|
||||||
|
- package-ecosystem: 'docker'
|
||||||
|
directory: '/client'
|
||||||
|
schedule:
|
||||||
|
interval: 'weekly'
|
||||||
|
|
||||||
|
- package-ecosystem: 'gradle'
|
||||||
|
directory: '/app'
|
||||||
|
schedule:
|
||||||
|
interval: 'weekly'
|
||||||
|
|
||||||
|
- package-ecosystem: 'github-actions'
|
||||||
|
directory: '/'
|
||||||
|
schedule:
|
||||||
|
interval: 'weekly'
|
||||||
16
.github/workflows/close-stale.yml
vendored
16
.github/workflows/close-stale.yml
vendored
@ -1,16 +0,0 @@
|
|||||||
name: 'Close stale issues and PRs'
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * *'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
stale:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/stale@v5.0.0
|
|
||||||
with:
|
|
||||||
stale-pr-message: 'This PR is stale because it has been open for 30 days with no activity. Remove the stale label or comment on this PR, otherwise it would be closed in 5 days.'
|
|
||||||
stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity. Remove the stale label or comment on this issue, otherwise it would be closed in 5 days.'
|
|
||||||
days-before-stale: 30
|
|
||||||
days-before-close: 5
|
|
||||||
13
.github/workflows/digitalocean-deploy.yml
vendored
13
.github/workflows/digitalocean-deploy.yml
vendored
@ -8,14 +8,21 @@ on:
|
|||||||
- completed
|
- completed
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
on-success:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Install DigitalOcean CLI
|
- name: Install DigitalOcean CLI
|
||||||
uses: digitalocean/action-doctl@v2.1.0
|
uses: digitalocean/action-doctl@v2.2.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.DIGITALOCEAN_TOKEN }}
|
token: ${{ secrets.DIGITALOCEAN_TOKEN }}
|
||||||
|
|
||||||
- name: Create Deployment with Latest Version
|
- name: Create Deployment with Latest Version
|
||||||
run: doctl apps create-deployment ${{ secrets.DIGITALOCEAN_APP_ID }} --wait --force-rebuild
|
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
|
||||||
|
|||||||
122
.github/workflows/docker-build-push.yml
vendored
122
.github/workflows/docker-build-push.yml
vendored
@ -1,120 +1,58 @@
|
|||||||
name: Build and Push Docker Image
|
name: Build and Push Docker Image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker_client:
|
build_matrix:
|
||||||
name: Docker (Client)
|
name: Build and Push Docker Image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
image: [client, server]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v3.0.0
|
uses: actions/checkout@v3.1.0
|
||||||
|
|
||||||
- id: version
|
- id: version
|
||||||
name: Get Version
|
name: App Version
|
||||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
uses: martinbeentjes/npm-get-version-action@v1.2.3
|
||||||
|
|
||||||
- name: Login to Docker
|
- name: Set up QEMU
|
||||||
uses: docker/login-action@v1.14.1
|
uses: docker/setup-qemu-action@v2.1.0
|
||||||
|
with:
|
||||||
|
platforms: amd64
|
||||||
|
|
||||||
|
- id: buildx
|
||||||
|
name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2.2.1
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Build and Push Client Image
|
|
||||||
uses: docker/build-push-action@v2.9.0
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
file: client/Dockerfile
|
|
||||||
tags: |
|
|
||||||
amruthpillai/reactive-resume:client-latest
|
|
||||||
amruthpillai/reactive-resume:client-${{ steps.version.outputs.tag }}
|
|
||||||
|
|
||||||
docker_server:
|
|
||||||
name: Docker (Server)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout the repository
|
|
||||||
uses: actions/checkout@v3.0.0
|
|
||||||
|
|
||||||
- id: version
|
|
||||||
name: Get Version
|
|
||||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
|
||||||
|
|
||||||
- name: Login to Docker
|
|
||||||
uses: docker/login-action@v1.14.1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Build and Push Server Image
|
|
||||||
uses: docker/build-push-action@v2.9.0
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
file: server/Dockerfile
|
|
||||||
tags: |
|
|
||||||
amruthpillai/reactive-resume:server-latest
|
|
||||||
amruthpillai/reactive-resume:server-${{ steps.version.outputs.tag }}
|
|
||||||
|
|
||||||
github_client:
|
|
||||||
name: GitHub (Client)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout the repository
|
|
||||||
uses: actions/checkout@v3.0.0
|
|
||||||
|
|
||||||
- id: version
|
|
||||||
name: Get Version
|
|
||||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v1.14.1
|
uses: docker/login-action@v2.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: $GITHUB_REPOSITORY_OWNER
|
username: $GITHUB_REPOSITORY_OWNER
|
||||||
password: ${{ secrets.GH_TOKEN }}
|
password: ${{ secrets.GH_TOKEN }}
|
||||||
|
|
||||||
- name: Build and Push Client Image
|
- name: Build and Push Docker Image
|
||||||
uses: docker/build-push-action@v2.9.0
|
uses: docker/build-push-action@v3.2.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
file: client/Dockerfile
|
platforms: linux/amd64
|
||||||
|
file: ${{ matrix.image }}/Dockerfile
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/amruthpillai/reactive-resume:client-latest
|
amruthpillai/reactive-resume:${{ matrix.image }}-latest
|
||||||
ghcr.io/amruthpillai/reactive-resume:client-${{ steps.version.outputs.tag }}
|
amruthpillai/reactive-resume:${{ matrix.image }}-${{ steps.version.outputs.current-version }}
|
||||||
|
ghcr.io/amruthpillai/reactive-resume:${{ matrix.image }}-latest
|
||||||
github_server:
|
ghcr.io/amruthpillai/reactive-resume:${{ matrix.image }}-${{ steps.version.outputs.current-version }}
|
||||||
name: GitHub (Server)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout the repository
|
|
||||||
uses: actions/checkout@v3.0.0
|
|
||||||
|
|
||||||
- id: version
|
|
||||||
name: Get Version
|
|
||||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v1.14.1
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: $GITHUB_REPOSITORY_OWNER
|
|
||||||
password: ${{ secrets.GH_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and Push Server Image
|
|
||||||
uses: docker/build-push-action@v2.9.0
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
file: server/Dockerfile
|
|
||||||
tags: |
|
|
||||||
ghcr.io/amruthpillai/reactive-resume:server-latest
|
|
||||||
ghcr.io/amruthpillai/reactive-resume:server-${{ steps.version.outputs.tag }}
|
|
||||||
|
|||||||
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,10 +1,17 @@
|
|||||||
# Environment Variables
|
# Environment Variables
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
!.env.gitpod
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
# Project Dependencies
|
# Project Dependencies
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Turbo
|
||||||
|
.turbo
|
||||||
|
|
||||||
|
# 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
|
||||||
schema/dist
|
schema/dist
|
||||||
|
|
||||||
@ -18,15 +21,9 @@ CHANGELOG.md
|
|||||||
|
|
||||||
# Project Dependencies
|
# Project Dependencies
|
||||||
node_modules
|
node_modules
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
Dockerfile
|
Dockerfile
|
||||||
.dockerignore
|
.dockerignore
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
|
||||||
# Android App
|
|
||||||
/app
|
|
||||||
|
|
||||||
# Docs
|
|
||||||
docs/build
|
|
||||||
docs/.docusaurus
|
|
||||||
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,
|
"css.validate": false,
|
||||||
|
"scss.validate": false,
|
||||||
|
"editor.wordWrap": "on",
|
||||||
|
"npm.packageManager": "pnpm",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true
|
"source.fixAll.eslint": true
|
||||||
},
|
},
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.wordWrap": "on",
|
|
||||||
"eslint.workingDirectories": [
|
"eslint.workingDirectories": [
|
||||||
"schema",
|
"schema",
|
||||||
"client",
|
"client",
|
||||||
"server"
|
"server"
|
||||||
],
|
],
|
||||||
"i18n-ally.enabledFrameworks": [
|
"conventionalCommits.scopes": [
|
||||||
"react"
|
"client",
|
||||||
],
|
"server",
|
||||||
"i18n-ally.keystyle": "nested",
|
"docker",
|
||||||
"i18n-ally.localesPaths": [
|
"dependencies"
|
||||||
"client/public/locales"
|
]
|
||||||
],
|
|
||||||
"i18n-ally.namespace": true,
|
|
||||||
"i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}",
|
|
||||||
"i18n-ally.sortKeys": true,
|
|
||||||
"scss.validate": false
|
|
||||||
}
|
}
|
||||||
128
CHANGELOG.md
128
CHANGELOG.md
@ -1,128 +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.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
|
Examples of behavior that contributes to a positive environment for our
|
||||||
community include:
|
community include:
|
||||||
|
|
||||||
* Demonstrating empathy and kindness toward other people
|
- Demonstrating empathy and kindness toward other people
|
||||||
* Being respectful of differing opinions, viewpoints, and experiences
|
- Being respectful of differing opinions, viewpoints, and experiences
|
||||||
* Giving and gracefully accepting constructive feedback
|
- Giving and gracefully accepting constructive feedback
|
||||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
and learning from the experience
|
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
|
overall community
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
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
|
advances of any kind
|
||||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
- Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or email
|
- Publishing others' private information, such as a physical or email
|
||||||
address, without their explicit permission
|
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
|
professional setting
|
||||||
|
|
||||||
## Enforcement Responsibilities
|
## Enforcement Responsibilities
|
||||||
@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
|||||||
### 4. Permanent Ban
|
### 4. Permanent Ban
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of community
|
**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.
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
|||||||
71
README.md
71
README.md
@ -1,11 +1,12 @@
|
|||||||
<img src="https://i.imgur.com/pc8Ingg.png" 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
|
# Reactive Resume
|
||||||
|
|
||||||

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

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

|
||||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2FAmruthPillai%2FReactive-Resume?ref=badge_shield)
|
[](https://app.fossa.com/projects/git%2Bgithub.com%2FAmruthPillai%2FReactive-Resume?ref=badge_shield)
|
||||||
|
|
||||||
## [Go to App](https://rxresu.me) | [Docs](https://docs.rxresu.me)
|
## [Go to App](https://rxresu.me) | [Docs](https://docs.rxresu.me)
|
||||||
@ -14,6 +15,24 @@ Reactive Resume is a free and open source resume builder that’s built to make
|
|||||||
|
|
||||||
You have complete control over what goes into your resume, how it looks, what colors, what templates, even the layout in which sections placed. Want a dark mode resume? It’s as easy as editing 3 values and you’re done. You don’t need to wait to see your changes either. Everything you type, everything you change, appears immediately on your resume and gets updated in real time.
|
You have complete control over what goes into your resume, how it looks, what colors, what templates, even the layout in which sections placed. Want a dark mode resume? It’s as easy as editing 3 values and you’re done. You don’t need to wait to see your changes either. Everything you type, everything you change, appears immediately on your resume and gets updated in real time.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Reactive Resume](#reactive-resume)
|
||||||
|
- [Go to App | Docs](#go-to-app--docs)
|
||||||
|
- [Table of Contents](#table-of-contents)
|
||||||
|
- [Features](#features)
|
||||||
|
- [Languages](#languages)
|
||||||
|
- [Tutorial](#tutorial)
|
||||||
|
- [Build from Source](#build-from-source)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
- [Report Bugs and Feature Requests](#report-bugs-and-feature-requests)
|
||||||
|
- [Donations](#donations)
|
||||||
|
- [GitHub Sponsor](#github-sponsor)
|
||||||
|
- [PayPal](#paypal)
|
||||||
|
- [Infrastructure](#infrastructure)
|
||||||
|
- [Contributors Wall](#contributors-wall)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Free, forever
|
- Free, forever
|
||||||
@ -36,19 +55,46 @@ You have complete control over what goes into your resume, how it looks, what co
|
|||||||
|
|
||||||
## Languages
|
## Languages
|
||||||
|
|
||||||
|
- Amharic (አማርኛ)
|
||||||
|
- Arabic (اَلْعَرَبِيَّةُ)
|
||||||
- Bengali (বাংলা)
|
- Bengali (বাংলা)
|
||||||
|
- Bulgarian (български)
|
||||||
|
- Catalan (Valencian)
|
||||||
- Chinese (中文)
|
- Chinese (中文)
|
||||||
|
- Czech (čeština)
|
||||||
- Danish (Dansk)
|
- Danish (Dansk)
|
||||||
|
- Dutch (Nederlands)
|
||||||
- English
|
- English
|
||||||
|
- Finnish (Suomi)
|
||||||
- French (Français)
|
- French (Français)
|
||||||
- German (Deutsch)
|
- German (Deutsch)
|
||||||
|
- Greek (Ελληνικά)
|
||||||
|
- Hebrew (Ivrit)
|
||||||
- Hindi (हिन्दी)
|
- Hindi (हिन्दी)
|
||||||
|
- Hungarian (Magyar)
|
||||||
|
- Indonesian (Bahasa Indonesia)
|
||||||
- Italian (Italiano)
|
- Italian (Italiano)
|
||||||
|
- Japanese (日本語)
|
||||||
- Kannada (ಕನ್ನಡ)
|
- Kannada (ಕನ್ನಡ)
|
||||||
|
- Khmer (ភាសាខ្មែរ)
|
||||||
|
- Korean (한국어)
|
||||||
|
- Malayalam (മലയാളം)
|
||||||
|
- Marathi (मराठी)
|
||||||
|
- Nepali (नेपाली)
|
||||||
|
- Norwegian (Norsk)
|
||||||
|
- Odia (ଓଡ଼ିଆ)
|
||||||
|
- Persian (فارسی)
|
||||||
- Polish (Polski)
|
- Polish (Polski)
|
||||||
|
- Portuguese (Português)
|
||||||
|
- Romanian (limba română)
|
||||||
|
- Russian (русский)
|
||||||
|
- Serbian (српски језик)
|
||||||
- Spanish (Español)
|
- Spanish (Español)
|
||||||
|
- Swedish (Svenska)
|
||||||
- Tamil (தமிழ்)
|
- Tamil (தமிழ்)
|
||||||
- Turkish (Türkçe)
|
- Turkish (Türkçe)
|
||||||
|
- Ukrainian (Українська мова)
|
||||||
|
- Vietnamese (Tiếng Việt)
|
||||||
|
|
||||||
Help by [translating Reactive Resume](https://translate.rxresu.me) to your language!
|
Help by [translating Reactive Resume](https://translate.rxresu.me) to your language!
|
||||||
|
|
||||||
@ -58,7 +104,11 @@ The docs include an extensive [Tutorial](https://docs.rxresu.me/tutorial) sectio
|
|||||||
|
|
||||||
## Build from Source
|
## 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
|
## Contributing
|
||||||
|
|
||||||
@ -72,7 +122,7 @@ This project makes use of [conventional commits](https://www.conventionalcommits
|
|||||||
|
|
||||||
NOTE: Be sure to merge the latest from `main` before making a pull request!
|
NOTE: Be sure to merge the latest from `main` before making a pull request!
|
||||||
|
|
||||||
## Bugs? Feature Requests?
|
## Report Bugs and Feature Requests
|
||||||
|
|
||||||
Use the [GitHub Issues](https://github.com/AmruthPillai/Reactive-Resume/issues/new/choose) platform to notify me about bugs or new features that you would like to see in Reactive Resume. Please check before creating new issues as there might already be one.
|
Use the [GitHub Issues](https://github.com/AmruthPillai/Reactive-Resume/issues/new/choose) platform to notify me about bugs or new features that you would like to see in Reactive Resume. Please check before creating new issues as there might already be one.
|
||||||
|
|
||||||
@ -80,7 +130,8 @@ 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.
|
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)
|
||||||
|
|
||||||
## Infrastructure
|
## Infrastructure
|
||||||
|
|
||||||
@ -96,6 +147,14 @@ Reactive Resume would be nothing without the folks who supported me and kept the
|
|||||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" width="200px" />
|
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" width="200px" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
## Contributors Wall
|
||||||
|
<a href="https://github.com/AmruthPillai/Reactive-Resume/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=AmruthPillai/Reactive-Resume" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
_Note: It may take up to 24h for the [contrib.rocks](https://contrib.rocks/image?repo=AmruthPillai/Reactive-Resume) plugin to update because it's refreshed once a day._
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Reactive Resume is packaged and distributed using the [MIT License](https://choosealicense.com/licenses/mit/) which allows for commercial use, distribution, modification and private use provided that all copies of the software contain the same license and copyright.
|
Reactive Resume is packaged and distributed using the [MIT License](https://choosealicense.com/licenses/mit/) which allows for commercial use, distribution, modification and private use provided that all copies of the software contain the same license and copyright.
|
||||||
|
|||||||
13
SECURITY.md
Normal file
13
SECURITY.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 3.x.x | :white_check_mark: |
|
||||||
|
| 2.x.x | :x: |
|
||||||
|
| 1.x.x | :x: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Create an issue on GitHub or send me an email through the contact form on my website at https://amruthpillai.com/
|
||||||
@ -1,7 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '7.1.2' apply false
|
id 'com.android.application' version '7.1.2' apply false
|
||||||
id 'com.android.library' version '7.1.2' apply false
|
id 'com.android.library' version '7.1.2' apply false
|
||||||
id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
|
id 'org.jetbrains.kotlin.android' version '1.7.22' apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
task clean(type: Delete) {
|
||||||
|
|||||||
@ -1,8 +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"],
|
"ignorePatterns": [".next", "__ENV.js"],
|
||||||
|
"settings": {
|
||||||
|
"next": {
|
||||||
|
"rootDir": "client"
|
||||||
|
}
|
||||||
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
|
// Next.js
|
||||||
"@next/next/no-img-element": "off",
|
"@next/next/no-img-element": "off",
|
||||||
"@next/next/no-sync-scripts": "off"
|
"@next/next/no-sync-scripts": "off",
|
||||||
|
|
||||||
|
// React Hooks
|
||||||
|
"react-hooks/exhaustive-deps": "off",
|
||||||
|
|
||||||
|
// 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
|
*.tsbuildinfo
|
||||||
|
|
||||||
# react-env
|
# react-env
|
||||||
__ENV.js
|
__ENV.js
|
||||||
|
|
||||||
|
# next-sitemap
|
||||||
|
sitemap*.xml
|
||||||
|
|||||||
@ -1,22 +1,19 @@
|
|||||||
FROM node:16-alpine as dependencies
|
FROM node:lts-alpine AS base
|
||||||
|
|
||||||
RUN apk add --no-cache curl g++ make python3 \
|
|
||||||
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json pnpm-*.yaml ./
|
RUN apk add --no-cache g++ git curl make python3 libc6-compat \
|
||||||
|
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||||
|
|
||||||
|
FROM base as dependencies
|
||||||
|
|
||||||
|
COPY package.json pnpm-*.yaml turbo.json ./
|
||||||
COPY ./schema/package.json ./schema/package.json
|
COPY ./schema/package.json ./schema/package.json
|
||||||
COPY ./client/package.json ./client/package.json
|
COPY ./client/package.json ./client/package.json
|
||||||
|
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
FROM node:16-alpine as builder
|
FROM base 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
|
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
@ -24,21 +21,14 @@ COPY --from=dependencies /app/node_modules ./node_modules
|
|||||||
COPY --from=dependencies /app/schema/node_modules ./schema/node_modules
|
COPY --from=dependencies /app/schema/node_modules ./schema/node_modules
|
||||||
COPY --from=dependencies /app/client/node_modules ./client/node_modules
|
COPY --from=dependencies /app/client/node_modules ./client/node_modules
|
||||||
|
|
||||||
RUN pnpm run build:schema
|
RUN pnpm run build --filter client
|
||||||
RUN pnpm run build:client
|
|
||||||
|
|
||||||
FROM node:16-alpine as production
|
FROM base as production
|
||||||
|
|
||||||
WORKDIR /app
|
COPY --from=builder /app/package.json /app/pnpm-*.yaml /app/turbo.json ./
|
||||||
|
|
||||||
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/client/package.json ./client/package.json
|
COPY --from=builder /app/client/package.json ./client/package.json
|
||||||
|
|
||||||
RUN pnpm install -F client --frozen-lockfile --prod
|
RUN pnpm install --filter client --prod --frozen-lockfile --workspace-root
|
||||||
|
|
||||||
COPY --from=builder /app/client/.next ./client/.next
|
COPY --from=builder /app/client/.next ./client/.next
|
||||||
COPY --from=builder /app/client/public ./client/public
|
COPY --from=builder /app/client/public ./client/public
|
||||||
@ -49,4 +39,7 @@ EXPOSE 3000
|
|||||||
|
|
||||||
ENV PORT 3000
|
ENV PORT 3000
|
||||||
|
|
||||||
CMD [ "pnpm", "run", "start:client" ]
|
HEALTHCHECK --interval=30s --timeout=20s --retries=3 --start-period=15s \
|
||||||
|
CMD curl -fSs localhost:3000 || exit 1
|
||||||
|
|
||||||
|
CMD [ "pnpm", "run", "start", "--filter", "client" ]
|
||||||
@ -5,17 +5,21 @@ import {
|
|||||||
FilterCenterFocus,
|
FilterCenterFocus,
|
||||||
InsertPageBreak,
|
InsertPageBreak,
|
||||||
Link,
|
Link,
|
||||||
|
RedoOutlined,
|
||||||
|
UndoOutlined,
|
||||||
ViewSidebar,
|
ViewSidebar,
|
||||||
ZoomIn,
|
ZoomIn,
|
||||||
ZoomOut,
|
ZoomOut,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { ButtonBase, Divider, Tooltip, useMediaQuery, useTheme } from '@mui/material';
|
import { ButtonBase, Divider, Tooltip, useMediaQuery, useTheme } from '@mui/material';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useMutation } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
import { ReactZoomPanPinchRef } from 'react-zoom-pan-pinch';
|
import { ReactZoomPanPinchRef } from 'react-zoom-pan-pinch';
|
||||||
|
import { ActionCreators } from 'redux-undo';
|
||||||
|
|
||||||
import { ServerError } from '@/services/axios';
|
import { ServerError } from '@/services/axios';
|
||||||
import { printResumeAsPdf, PrintResumeAsPdfParams } from '@/services/printer';
|
import { printResumeAsPdf, PrintResumeAsPdfParams } from '@/services/printer';
|
||||||
@ -31,14 +35,18 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const resume = useAppSelector((state) => state.resume);
|
|
||||||
const isDesktop = useMediaQuery(theme.breakpoints.up('sm'));
|
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 { left, right } = useAppSelector((state) => state.build.sidebar);
|
||||||
const orientation = useAppSelector((state) => state.build.page.orientation);
|
const orientation = useAppSelector((state) => state.build.page.orientation);
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useMutation<string, ServerError, PrintResumeAsPdfParams>(printResumeAsPdf);
|
const { mutateAsync, isLoading } = useMutation<string, ServerError, PrintResumeAsPdfParams>(printResumeAsPdf);
|
||||||
|
|
||||||
|
const handleUndo = () => dispatch(ActionCreators.undo());
|
||||||
|
const handleRedo = () => dispatch(ActionCreators.redo());
|
||||||
|
|
||||||
const handleTogglePageBreakLine = () => dispatch(togglePageBreakLine());
|
const handleTogglePageBreakLine = () => dispatch(togglePageBreakLine());
|
||||||
|
|
||||||
const handleTogglePageOrientation = () => dispatch(togglePageOrientation());
|
const handleTogglePageOrientation = () => dispatch(togglePageOrientation());
|
||||||
@ -52,7 +60,7 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
|||||||
const url = getResumeUrl(resume, { withHost: true });
|
const url = getResumeUrl(resume, { withHost: true });
|
||||||
await navigator.clipboard.writeText(url);
|
await navigator.clipboard.writeText(url);
|
||||||
|
|
||||||
toast.success(t('common.toast.success.resume-link-copied'));
|
toast.success(t<string>('common.toast.success.resume-link-copied'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExportPDF = async () => {
|
const handleExportPDF = async () => {
|
||||||
@ -60,10 +68,11 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
|||||||
|
|
||||||
const slug = get(resume, 'slug');
|
const slug = get(resume, 'slug');
|
||||||
const username = get(resume, 'user.username');
|
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 (
|
return (
|
||||||
@ -75,19 +84,33 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className={styles.controller}>
|
<div className={styles.controller}>
|
||||||
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.zoom-in') as string}>
|
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.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)}>
|
<ButtonBase onClick={() => zoomIn(0.25)}>
|
||||||
<ZoomIn fontSize="medium" />
|
<ZoomIn fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.zoom-out') as string}>
|
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.zoom-out')}>
|
||||||
<ButtonBase onClick={() => zoomOut(0.25)}>
|
<ButtonBase onClick={() => zoomOut(0.25)}>
|
||||||
<ZoomOut fontSize="medium" />
|
<ZoomOut fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.center-artboard') as string}>
|
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.center-artboard')}>
|
||||||
<ButtonBase onClick={() => centerView(0.95)}>
|
<ButtonBase onClick={() => centerView(0.95)}>
|
||||||
<FilterCenterFocus fontSize="medium" />
|
<FilterCenterFocus fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
@ -97,25 +120,26 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
|||||||
|
|
||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
<>
|
<>
|
||||||
{pages.length > 1 && (
|
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-orientation')}>
|
||||||
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.toggle-orientation') as string}>
|
<ButtonBase
|
||||||
<ButtonBase onClick={handleTogglePageOrientation}>
|
onClick={handleTogglePageOrientation}
|
||||||
{orientation === 'vertical' ? (
|
className={clsx({ 'pointer-events-none opacity-50': pages.length === 1 })}
|
||||||
<AlignHorizontalCenter fontSize="medium" />
|
>
|
||||||
) : (
|
{orientation === 'vertical' ? (
|
||||||
<AlignVerticalCenter fontSize="medium" />
|
<AlignHorizontalCenter fontSize="medium" />
|
||||||
)}
|
) : (
|
||||||
</ButtonBase>
|
<AlignVerticalCenter fontSize="medium" />
|
||||||
</Tooltip>
|
)}
|
||||||
)}
|
</ButtonBase>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.toggle-page-break-line') as string}>
|
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-page-break-line')}>
|
||||||
<ButtonBase onClick={handleTogglePageBreakLine}>
|
<ButtonBase onClick={handleTogglePageBreakLine}>
|
||||||
<InsertPageBreak fontSize="medium" />
|
<InsertPageBreak fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.toggle-sidebars') as string}>
|
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-sidebars')}>
|
||||||
<ButtonBase onClick={handleToggleSidebar}>
|
<ButtonBase onClick={handleToggleSidebar}>
|
||||||
<ViewSidebar fontSize="medium" />
|
<ViewSidebar fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
@ -125,13 +149,13 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.copy-link') as string}>
|
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.copy-link')}>
|
||||||
<ButtonBase onClick={handleCopyLink}>
|
<ButtonBase onClick={handleCopyLink}>
|
||||||
<Link fontSize="medium" />
|
<Link fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip arrow placement="top" title={t('builder.controller.tooltip.export-pdf') as string}>
|
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.export-pdf')}>
|
||||||
<ButtonBase onClick={handleExportPDF} disabled={isLoading}>
|
<ButtonBase onClick={handleExportPDF} disabled={isLoading}>
|
||||||
<Download fontSize="medium" />
|
<Download fontSize="medium" />
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
@apply h-full w-full #{!important};
|
@apply h-full w-full overflow-visible #{!important};
|
||||||
}
|
}
|
||||||
|
|
||||||
.artboard {
|
.artboard {
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import Page from './Page';
|
|||||||
const Center = () => {
|
const Center = () => {
|
||||||
const orientation = useAppSelector((state) => state.build.page.orientation);
|
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');
|
const layout: string[][][] = get(resume, 'metadata.layout');
|
||||||
|
|
||||||
if (isEmpty(resume)) return null;
|
if (isEmpty(resume)) return null;
|
||||||
|
|||||||
@ -57,7 +57,7 @@ const Header = () => {
|
|||||||
|
|
||||||
const { mutateAsync: deleteMutation } = useMutation<void, ServerError, DeleteResumeParams>(deleteResume);
|
const { mutateAsync: deleteMutation } = useMutation<void, ServerError, DeleteResumeParams>(deleteResume);
|
||||||
|
|
||||||
const resume = useAppSelector((state) => state.resume);
|
const resume = useAppSelector((state) => state.resume.present);
|
||||||
const { left, right } = useAppSelector((state) => state.build.sidebar);
|
const { left, right } = useAppSelector((state) => state.build.sidebar);
|
||||||
|
|
||||||
const name = useMemo(() => get(resume, 'name'), [resume]);
|
const name = useMemo(() => get(resume, 'name'), [resume]);
|
||||||
@ -133,7 +133,7 @@ const Header = () => {
|
|||||||
const url = getResumeUrl(resume, { withHost: true });
|
const url = getResumeUrl(resume, { withHost: true });
|
||||||
await navigator.clipboard.writeText(url);
|
await navigator.clipboard.writeText(url);
|
||||||
|
|
||||||
toast.success(t('common.toast.success.resume-link-copied'));
|
toast.success(t<string>('common.toast.success.resume-link-copied'));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -166,14 +166,14 @@ const Header = () => {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DriveFileRenameOutline className="scale-90" />
|
<DriveFileRenameOutline className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('builder.header.menu.rename')}</ListItemText>
|
<ListItemText>{t<string>('builder.header.menu.rename')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem onClick={handleDuplicate}>
|
<MenuItem onClick={handleDuplicate}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<CopyAll className="scale-90" />
|
<CopyAll className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('builder.header.menu.duplicate')}</ListItemText>
|
<ListItemText>{t<string>('builder.header.menu.duplicate')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
{resume.public ? (
|
{resume.public ? (
|
||||||
@ -181,27 +181,27 @@ const Header = () => {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LinkIcon className="scale-90" />
|
<LinkIcon className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('builder.header.menu.share-link')}</ListItemText>
|
<ListItemText>{t<string>('builder.header.menu.share-link')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip arrow placement="right" title={t('builder.header.menu.tooltips.share-link') as string}>
|
<Tooltip arrow placement="right" title={t<string>('builder.header.menu.tooltips.share-link')}>
|
||||||
<div>
|
<div>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LinkIcon className="scale-90" />
|
<LinkIcon className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('builder.header.menu.share-link')}</ListItemText>
|
<ListItemText>{t<string>('builder.header.menu.share-link')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Tooltip arrow placement="right" title={t('builder.header.menu.tooltips.delete') as string}>
|
<Tooltip arrow placement="right" title={t<string>('builder.header.menu.tooltips.delete')}>
|
||||||
<MenuItem onClick={handleDelete}>
|
<MenuItem onClick={handleDelete}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Delete className="scale-90" />
|
<Delete className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('builder.header.menu.delete')}</ListItemText>
|
<ListItemText>{t<string>('builder.header.menu.delete')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.break::after {
|
&.break::after {
|
||||||
content: 'A4 Page Break';
|
content: 'Page Break';
|
||||||
top: calc(297mm - 19px);
|
top: calc(297mm - 19px);
|
||||||
|
|
||||||
@apply absolute w-full border-b border-dashed border-neutral-800/75;
|
@apply absolute w-full border-b border-dashed border-neutral-800/75;
|
||||||
@ -27,6 +27,15 @@
|
|||||||
@apply hidden;
|
@apply hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.format-letter {
|
||||||
|
width: 216mm;
|
||||||
|
min-height: 279mm;
|
||||||
|
|
||||||
|
&.break::after {
|
||||||
|
top: calc(279mm - 19px);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pageNumber {
|
.pageNumber {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { css } from '@emotion/css';
|
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 clsx from 'clsx';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
@ -20,25 +20,27 @@ type Props = PageProps & {
|
|||||||
const Page: React.FC<Props> = ({ page, showPageNumbers = false }) => {
|
const Page: React.FC<Props> = ({ page, showPageNumbers = false }) => {
|
||||||
const { t } = useTranslation();
|
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 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 customCSS: CustomCSS = get(resume, 'metadata.css');
|
||||||
const template: string = get(resume, 'metadata.template');
|
const template: string = get(resume, 'metadata.template');
|
||||||
const typography: Typography = get(resume, 'metadata.typography');
|
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 themeCSS = useMemo(() => !isEmpty(theme) && generateThemeStyles(theme), [theme]);
|
||||||
const typographyCSS = useMemo(() => !isEmpty(typography) && generateTypographyStyles(typography), [typography]);
|
const typographyCSS = useMemo(() => !isEmpty(typography) && generateTypographyStyles(typography), [typography]);
|
||||||
const TemplatePage: React.FC<PageProps> | null = useMemo(() => templateMap[template].component, [template]);
|
const TemplatePage: React.FC<PageProps> | null = useMemo(() => templateMap[template].component, [template]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-page={page + 1} className={styles.container}>
|
<div className={styles.container} data-page={page + 1} data-format={pageConfig?.format || 'A4'}>
|
||||||
<div
|
<div
|
||||||
className={clsx({
|
className={clsx({
|
||||||
reset: true,
|
reset: true,
|
||||||
[styles.page]: true,
|
[styles.page]: true,
|
||||||
[styles.break]: breakLine,
|
[styles.break]: breakLine,
|
||||||
|
[styles['format-letter']]: pageConfig?.format === 'Letter',
|
||||||
[css(themeCSS)]: true,
|
[css(themeCSS)]: true,
|
||||||
[css(typographyCSS)]: true,
|
[css(typographyCSS)]: true,
|
||||||
[css(customCSS.value)]: customCSS.visible,
|
[css(customCSS.value)]: customCSS.visible,
|
||||||
@ -48,9 +50,7 @@ const Page: React.FC<Props> = ({ page, showPageNumbers = false }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showPageNumbers && (
|
{showPageNumbers && (
|
||||||
<h4 className={styles.pageNumber}>
|
<h4 className={styles.pageNumber}>{`${t<string>('builder.common.glossary.page')} ${page + 1}`}</h4>
|
||||||
{t('builder.common.glossary.page')} {page + 1}
|
|
||||||
</h4>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
import { Add, Star } from '@mui/icons-material';
|
import { Add, Star } from '@mui/icons-material';
|
||||||
import { Button, Divider, IconButton, SwipeableDrawer, Tooltip, useMediaQuery, useTheme } from '@mui/material';
|
import { Button, Divider, IconButton, SwipeableDrawer, Tooltip, useMediaQuery, useTheme } from '@mui/material';
|
||||||
import { Section as SectionRecord } from '@reactive-resume/schema';
|
import { Section as SectionRecord } from '@reactive-resume/schema';
|
||||||
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useMemo } from 'react';
|
import React, { ReactComponentElement, useMemo } from 'react';
|
||||||
import { validate } from 'uuid';
|
import { validate } from 'uuid';
|
||||||
|
|
||||||
import Logo from '@/components/shared/Logo';
|
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 { setSidebarState } from '@/store/build/buildSlice';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
import { addSection } from '@/store/resume/resumeSlice';
|
import { addSection } from '@/store/resume/resumeSlice';
|
||||||
@ -25,7 +26,7 @@ const LeftSidebar = () => {
|
|||||||
|
|
||||||
const isDesktop = useMediaQuery(theme.breakpoints.up('lg'));
|
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 { open } = useAppSelector((state) => state.build.sidebar.left);
|
||||||
|
|
||||||
const customSections = useMemo(() => getCustomSections(sections), [sections]);
|
const customSections = useMemo(() => getCustomSections(sections), [sections]);
|
||||||
@ -52,7 +53,49 @@ const LeftSidebar = () => {
|
|||||||
items: [],
|
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 (
|
return (
|
||||||
@ -65,12 +108,10 @@ const LeftSidebar = () => {
|
|||||||
variant={isDesktop ? 'persistent' : 'temporary'}
|
variant={isDesktop ? 'persistent' : 'temporary'}
|
||||||
>
|
>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<nav>
|
<nav className="overflow-y-scroll">
|
||||||
<div>
|
<div>
|
||||||
<Link href="/dashboard">
|
<Link href="/dashboard">
|
||||||
<a className="inline-flex">
|
<Logo size={40} />
|
||||||
<Logo size={40} />
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
<Divider />
|
<Divider />
|
||||||
</div>
|
</div>
|
||||||
@ -81,15 +122,15 @@ const LeftSidebar = () => {
|
|||||||
arrow
|
arrow
|
||||||
key={id}
|
key={id}
|
||||||
placement="right"
|
placement="right"
|
||||||
title={get(sections, `${id}.name`, t<string>(`builder.leftSidebar.sections.${id}.heading`))}
|
title={get(sections, `${id}.name`, t<string>(`builder.leftSidebar.sections.${id}.heading`)) as string}
|
||||||
>
|
>
|
||||||
<IconButton onClick={() => handleClick(id)}>{icon}</IconButton>
|
<IconButton onClick={() => handleClick(id)}>{icon}</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{customSections.map(({ id }) => (
|
{customSections.map(({ id }) => (
|
||||||
<Tooltip key={id} title={get(sections, `${id}.name`, '')} placement="right" arrow>
|
<Tooltip key={id} title={get(sections, `${id}.name`, '') as string} placement="right" arrow>
|
||||||
<IconButton onClick={() => handleClick(id)}>
|
<IconButton onClick={() => id && handleClick(id)}>
|
||||||
<Star />
|
<Star />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -100,21 +141,19 @@ const LeftSidebar = () => {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
{left.map(({ id, component }) => (
|
{sectionsList()}
|
||||||
<section key={id} id={id}>
|
|
||||||
{component}
|
|
||||||
</section>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{customSections.map(({ id }) => (
|
{customSections.map(({ id }) => (
|
||||||
<section key={id} id={`section-${id}`}>
|
<section key={id} id={`section-${id}`}>
|
||||||
<Section path={`sections.${id}`} isEditable isHideable isDeletable />
|
<Section path={`sections.${id}`} type="custom" isEditable isHideable isDeletable />
|
||||||
</section>
|
</section>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="py-6 text-right">
|
<div className="py-6 text-right">
|
||||||
<Button fullWidth variant="outlined" startIcon={<Add />} onClick={handleAddSection}>
|
<Button fullWidth variant="outlined" startIcon={<Add />} onClick={handleAddSection}>
|
||||||
{t('builder.common.actions.add', { token: t('builder.leftSidebar.sections.section.heading') })}
|
{t<string>('builder.common.actions.add', {
|
||||||
|
token: t<string>('builder.leftSidebar.sections.section.heading'),
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const Basics = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading path="sections.basics" name={t('builder.leftSidebar.sections.basics.heading')} />
|
<Heading path="sections.basics" name={t<string>('builder.leftSidebar.sections.basics.heading')} />
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
<div className="grid items-center gap-4 sm:col-span-2 sm:grid-cols-3">
|
<div className="grid items-center gap-4 sm:col-span-2 sm:grid-cols-3">
|
||||||
@ -32,11 +32,11 @@ const Basics = () => {
|
|||||||
<PhotoUpload />
|
<PhotoUpload />
|
||||||
</div>
|
</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('builder.leftSidebar.sections.basics.name.label')} path="basics.name" />
|
<ResumeInput label={t<string>('builder.leftSidebar.sections.basics.name.label')} path="basics.name" />
|
||||||
|
|
||||||
<Button variant="outlined" startIcon={<PhotoFilter />} onClick={handleClick}>
|
<Button variant="outlined" startIcon={<PhotoFilter />} onClick={handleClick}>
|
||||||
{t('builder.leftSidebar.sections.basics.actions.photo-filters')}
|
{t<string>('builder.leftSidebar.sections.basics.actions.photo-filters')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
@ -57,20 +57,30 @@ const Basics = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ResumeInput label={t('builder.common.form.email.label')} path="basics.email" className="sm:col-span-2" />
|
<ResumeInput
|
||||||
<ResumeInput label={t('builder.common.form.phone.label')} path="basics.phone" />
|
type="date"
|
||||||
<ResumeInput label={t('builder.common.form.url.label')} path="basics.website" />
|
label={t<string>('builder.leftSidebar.sections.basics.birthdate.label')}
|
||||||
|
path="basics.birthdate"
|
||||||
|
className="sm:col-span-2"
|
||||||
|
/>
|
||||||
|
<ResumeInput
|
||||||
|
label={t<string>('builder.common.form.email.label')}
|
||||||
|
path="basics.email"
|
||||||
|
className="sm:col-span-2"
|
||||||
|
/>
|
||||||
|
<ResumeInput label={t<string>('builder.common.form.phone.label')} path="basics.phone" />
|
||||||
|
<ResumeInput label={t<string>('builder.common.form.url.label')} path="basics.website" />
|
||||||
|
|
||||||
<Divider className="sm:col-span-2" />
|
<Divider className="sm:col-span-2" />
|
||||||
|
|
||||||
<ResumeInput
|
<ResumeInput
|
||||||
label={t('builder.leftSidebar.sections.basics.headline.label')}
|
label={t<string>('builder.leftSidebar.sections.basics.headline.label')}
|
||||||
path="basics.headline"
|
path="basics.headline"
|
||||||
className="sm:col-span-2"
|
className="sm:col-span-2"
|
||||||
/>
|
/>
|
||||||
<ResumeInput
|
<ResumeInput
|
||||||
type="textarea"
|
type="textarea"
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
path="basics.summary"
|
path="basics.summary"
|
||||||
className="sm:col-span-2"
|
className="sm:col-span-2"
|
||||||
markdownSupported
|
markdownSupported
|
||||||
|
|||||||
@ -8,19 +8,28 @@ const Location = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading path="sections.location" name={t('builder.leftSidebar.sections.location.heading')} />
|
<Heading path="sections.location" name={t<string>('builder.leftSidebar.sections.location.heading')} />
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
<ResumeInput
|
<ResumeInput
|
||||||
label={t('builder.leftSidebar.sections.location.address.label')}
|
label={t<string>('builder.leftSidebar.sections.location.address.label')}
|
||||||
path="basics.location.address"
|
path="basics.location.address"
|
||||||
className="sm:col-span-2"
|
className="sm:col-span-2"
|
||||||
/>
|
/>
|
||||||
<ResumeInput label={t('builder.leftSidebar.sections.location.city.label')} path="basics.location.city" />
|
|
||||||
<ResumeInput label={t('builder.leftSidebar.sections.location.region.label')} path="basics.location.region" />
|
|
||||||
<ResumeInput label={t('builder.leftSidebar.sections.location.country.label')} path="basics.location.country" />
|
|
||||||
<ResumeInput
|
<ResumeInput
|
||||||
label={t('builder.leftSidebar.sections.location.postal-code.label')}
|
label={t<string>('builder.leftSidebar.sections.location.city.label')}
|
||||||
|
path="basics.location.city"
|
||||||
|
/>
|
||||||
|
<ResumeInput
|
||||||
|
label={t<string>('builder.leftSidebar.sections.location.region.label')}
|
||||||
|
path="basics.location.region"
|
||||||
|
/>
|
||||||
|
<ResumeInput
|
||||||
|
label={t<string>('builder.leftSidebar.sections.location.country.label')}
|
||||||
|
path="basics.location.country"
|
||||||
|
/>
|
||||||
|
<ResumeInput
|
||||||
|
label={t<string>('builder.leftSidebar.sections.location.postal-code.label')}
|
||||||
path="basics.location.postalCode"
|
path="basics.location.postalCode"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -12,7 +12,7 @@ const PhotoFilters = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
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 size: number = get(photo, 'filters.size', 128);
|
||||||
const shape: PhotoShape = get(photo, 'filters.shape', 'square');
|
const shape: PhotoShape = get(photo, 'filters.shape', 'square');
|
||||||
const grayscale: boolean = get(photo, 'filters.grayscale', false);
|
const grayscale: boolean = get(photo, 'filters.grayscale', false);
|
||||||
@ -32,7 +32,7 @@ const PhotoFilters = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 p-5 dark:bg-neutral-800">
|
<div className="flex flex-col gap-2 p-5 dark:bg-neutral-800">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium">{t('builder.leftSidebar.sections.basics.photo-filters.size.heading')}</h4>
|
<h4 className="font-medium">{t<string>('builder.leftSidebar.sections.basics.photo-filters.size.heading')}</h4>
|
||||||
|
|
||||||
<div className="mx-2">
|
<div className="mx-2">
|
||||||
<Slider
|
<Slider
|
||||||
@ -54,18 +54,20 @@ const PhotoFilters = () => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium">{t('builder.leftSidebar.sections.basics.photo-filters.effects.heading')}</h4>
|
<h4 className="font-medium">
|
||||||
|
{t<string>('builder.leftSidebar.sections.basics.photo-filters.effects.heading')}
|
||||||
|
</h4>
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label={t('builder.leftSidebar.sections.basics.photo-filters.effects.grayscale.label') as string}
|
label={t<string>('builder.leftSidebar.sections.basics.photo-filters.effects.grayscale.label')}
|
||||||
control={
|
control={
|
||||||
<Checkbox color="secondary" checked={grayscale} onChange={(_, value) => handleSetGrayscale(value)} />
|
<Checkbox color="secondary" checked={grayscale} onChange={(_, value) => handleSetGrayscale(value)} />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label={t('builder.leftSidebar.sections.basics.photo-filters.effects.border.label') as string}
|
label={t<string>('builder.leftSidebar.sections.basics.photo-filters.effects.border.label')}
|
||||||
control={<Checkbox color="secondary" checked={border} onChange={(_, value) => handleSetBorder(value)} />}
|
control={<Checkbox color="secondary" checked={border} onChange={(_, value) => handleSetBorder(value)} />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -74,7 +76,7 @@ const PhotoFilters = () => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h4 className="font-medium">{t('builder.leftSidebar.sections.basics.photo-filters.shape.heading')}</h4>
|
<h4 className="font-medium">{t<string>('builder.leftSidebar.sections.basics.photo-filters.shape.heading')}</h4>
|
||||||
|
|
||||||
<ToggleButtonGroup exclusive value={shape} onChange={(_, value) => handleChangeShape(value)}>
|
<ToggleButtonGroup exclusive value={shape} onChange={(_, value) => handleChangeShape(value)}>
|
||||||
<ToggleButton size="small" value="square" className="w-14">
|
<ToggleButton size="small" value="square" className="w-14">
|
||||||
|
|||||||
@ -21,8 +21,8 @@ const PhotoUpload: React.FC = () => {
|
|||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const id: number = useAppSelector((state) => get(state.resume, 'id'));
|
const id: number = useAppSelector((state) => get(state.resume.present, 'id'));
|
||||||
const photo: Photo = useAppSelector((state) => get(state.resume, 'basics.photo'));
|
const photo: Photo = useAppSelector((state) => get(state.resume.present, 'basics.photo'));
|
||||||
|
|
||||||
const { mutateAsync: uploadMutation, isLoading } = useMutation<Resume, ServerError, UploadPhotoParams>(uploadPhoto);
|
const { mutateAsync: uploadMutation, isLoading } = useMutation<Resume, ServerError, UploadPhotoParams>(uploadPhoto);
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ const PhotoUpload: React.FC = () => {
|
|||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
|
|
||||||
if (file.size > FILE_UPLOAD_MAX_SIZE) {
|
if (file.size > FILE_UPLOAD_MAX_SIZE) {
|
||||||
toast.error(t('common.toast.error.upload-photo-size'));
|
toast.error(t<string>('common.toast.error.upload-photo-size'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,8 +67,8 @@ const PhotoUpload: React.FC = () => {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
isEmpty(photo.url)
|
isEmpty(photo.url)
|
||||||
? (t('builder.leftSidebar.sections.basics.photo-upload.tooltip.upload') as string)
|
? (t<string>('builder.leftSidebar.sections.basics.photo-upload.tooltip.upload') as string)
|
||||||
: (t('builder.leftSidebar.sections.basics.photo-upload.tooltip.remove') as string)
|
: (t<string>('builder.leftSidebar.sections.basics.photo-upload.tooltip.remove') as string)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Avatar sx={{ width: 96, height: 96 }} src={photo.url} />
|
<Avatar sx={{ width: 96, height: 96 }} src={photo.url} />
|
||||||
|
|||||||
@ -28,7 +28,7 @@ const Profiles = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading path="sections.profiles" name={t('builder.leftSidebar.sections.profiles.heading')} />
|
<Heading path="sections.profiles" name={t<string>('builder.leftSidebar.sections.profiles.heading')} />
|
||||||
|
|
||||||
<List
|
<List
|
||||||
path="basics.profiles"
|
path="basics.profiles"
|
||||||
@ -40,8 +40,8 @@ const Profiles = () => {
|
|||||||
|
|
||||||
<footer className="flex justify-end">
|
<footer className="flex justify-end">
|
||||||
<Button variant="outlined" startIcon={<Add />} onClick={handleAdd}>
|
<Button variant="outlined" startIcon={<Add />} onClick={handleAdd}>
|
||||||
{t('builder.common.actions.add', {
|
{t<string>('builder.common.actions.add', {
|
||||||
token: t('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
token: t<string>('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
||||||
})}
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Add } from '@mui/icons-material';
|
import { Add } from '@mui/icons-material';
|
||||||
import { Button } from '@mui/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 clsx from 'clsx';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
@ -10,53 +10,77 @@ import Heading from '@/components/shared/Heading';
|
|||||||
import List from '@/components/shared/List';
|
import List from '@/components/shared/List';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
import { ModalName, setModalState } from '@/store/modal/modalSlice';
|
import { ModalName, setModalState } from '@/store/modal/modalSlice';
|
||||||
import { duplicateItem } from '@/store/resume/resumeSlice';
|
import { duplicateItem, duplicateSection } from '@/store/resume/resumeSlice';
|
||||||
|
|
||||||
import SectionSettings from './SectionSettings';
|
import SectionSettings from './SectionSettings';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
path: `sections.${string}`;
|
path: `sections.${string}`;
|
||||||
|
type?: SectionType;
|
||||||
name?: string;
|
name?: string;
|
||||||
titleKey?: string;
|
titleKey?: string;
|
||||||
subtitleKey?: string;
|
subtitleKey?: string;
|
||||||
isEditable?: boolean;
|
isEditable?: boolean;
|
||||||
isHideable?: boolean;
|
isHideable?: boolean;
|
||||||
isDeletable?: boolean;
|
isDeletable?: boolean;
|
||||||
|
addMore?: boolean;
|
||||||
|
isDuplicated?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Section: React.FC<Props> = ({
|
const Section: React.FC<Props> = ({
|
||||||
path,
|
path,
|
||||||
name = 'Section Name',
|
name = 'Section Name',
|
||||||
|
type = 'basic',
|
||||||
titleKey = 'title',
|
titleKey = 'title',
|
||||||
subtitleKey = 'subtitle',
|
subtitleKey = 'subtitle',
|
||||||
isEditable = false,
|
isEditable = false,
|
||||||
isHideable = false,
|
isHideable = false,
|
||||||
isDeletable = false,
|
isDeletable = false,
|
||||||
|
addMore = false,
|
||||||
|
isDuplicated = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const heading = useAppSelector<string>((state) => get(state.resume, `${path}.name`, name));
|
const heading = useAppSelector<string>((state) => get(state.resume.present, `${path}.name`, name));
|
||||||
const visibility = useAppSelector<boolean>((state) => get(state.resume, `${path}.visible`, true));
|
const visibility = useAppSelector<boolean>((state) => get(state.resume.present, `${path}.visible`, true));
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
const id = path.split('.')[1];
|
const modal: ModalName = `builder.sections.${type}`;
|
||||||
const modal: ModalName = validate(id) ? 'builder.sections.custom' : `builder.${path}`;
|
|
||||||
|
|
||||||
dispatch(setModalState({ modal, state: { open: true, payload: { path } } }));
|
dispatch(setModalState({ modal, state: { open: true, payload: { path } } }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (item: ListItem) => {
|
const handleEdit = (item: ListItem) => {
|
||||||
const id = path.split('.')[1];
|
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 };
|
const payload = validate(id) ? { path, item } : { item };
|
||||||
|
|
||||||
|
if (isDuplicated) {
|
||||||
|
modal = `builder.sections.${type}`;
|
||||||
|
payload.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(setModalState({ modal, state: { open: true, payload } }));
|
dispatch(setModalState({ modal, state: { open: true, payload } }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDuplicate = (item: ListItem) => dispatch(duplicateItem({ path: `${path}.items`, value: item }));
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading path={path} name={name} isEditable={isEditable} isHideable={isHideable} isDeletable={isDeletable} />
|
<Heading path={path} name={name} isEditable={isEditable} isHideable={isHideable} isDeletable={isDeletable} />
|
||||||
@ -74,9 +98,19 @@ const Section: React.FC<Props> = ({
|
|||||||
<SectionSettings path={path} />
|
<SectionSettings path={path} />
|
||||||
|
|
||||||
<Button variant="outlined" startIcon={<Add />} onClick={handleAdd}>
|
<Button variant="outlined" startIcon={<Add />} onClick={handleAdd}>
|
||||||
{t('builder.common.actions.add', { token: heading })}
|
{t<string>('builder.common.actions.add', { token: heading })}
|
||||||
</Button>
|
</Button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
{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 [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>) => {
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
@ -32,7 +32,7 @@ const SectionSettings: React.FC<Props> = ({ path }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Tooltip title={t('builder.common.columns.tooltip') as string}>
|
<Tooltip title={t<string>('builder.common.columns.tooltip')}>
|
||||||
<ButtonBase onClick={handleClick} sx={{ padding: 1, borderRadius: 1 }} className="opacity-50 hover:opacity-75">
|
<ButtonBase onClick={handleClick} sx={{ padding: 1, borderRadius: 1 }} className="opacity-50 hover:opacity-75">
|
||||||
<ViewWeek /> <span className="ml-1.5 text-xs">{columns}</span>
|
<ViewWeek /> <span className="ml-1.5 text-xs">{columns}</span>
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
@ -48,7 +48,7 @@ const SectionSettings: React.FC<Props> = ({ path }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="p-5 dark:bg-neutral-800">
|
<div className="p-5 dark:bg-neutral-800">
|
||||||
<h4 className="mb-2 font-medium">{t('builder.common.columns.heading')}</h4>
|
<h4 className="mb-2 font-medium">{t<string>('builder.common.columns.heading')}</h4>
|
||||||
|
|
||||||
<ToggleButtonGroup exclusive value={columns} onChange={(_, value: number) => handleSetColumns(value)}>
|
<ToggleButtonGroup exclusive value={columns} onChange={(_, value: number) => handleSetColumns(value)}>
|
||||||
{[1, 2, 3, 4].map((index) => (
|
{[1, 2, 3, 4].map((index) => (
|
||||||
|
|||||||
@ -43,7 +43,7 @@ const RightSidebar = () => {
|
|||||||
variant={isDesktop ? 'persistent' : 'temporary'}
|
variant={isDesktop ? 'persistent' : 'temporary'}
|
||||||
>
|
>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<nav>
|
<nav className="overflow-y-scroll">
|
||||||
<div>
|
<div>
|
||||||
<Avatar size={40} />
|
<Avatar size={40} />
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|||||||
@ -17,7 +17,9 @@ const CustomCSS = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
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) => {
|
const handleChange = (value: string | undefined) => {
|
||||||
dispatch(setResumeState({ path: 'metadata.css.value', value }));
|
dispatch(setResumeState({ path: 'metadata.css.value', value }));
|
||||||
@ -25,7 +27,7 @@ const CustomCSS = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading path="metadata.css" name={t('builder.rightSidebar.sections.css.heading')} isHideable />
|
<Heading path="metadata.css" name={t<string>('builder.rightSidebar.sections.css.heading')} isHideable />
|
||||||
|
|
||||||
<Editor
|
<Editor
|
||||||
height="200px"
|
height="200px"
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { PictureAsPdf, Schema } from '@mui/icons-material';
|
import { PictureAsPdf, Schema } from '@mui/icons-material';
|
||||||
import { List, ListItem, ListItemButton, ListItemText } from '@mui/material';
|
import { List, ListItem, ListItemButton, ListItemText } from '@mui/material';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import pick from 'lodash/pick';
|
import pick from 'lodash/pick';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
@ -13,18 +14,18 @@ import { useAppSelector } from '@/store/hooks';
|
|||||||
const Export = () => {
|
const Export = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const resume = useAppSelector((state) => state.resume);
|
const resume = useAppSelector((state) => state.resume.present);
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useMutation<string, ServerError, PrintResumeAsPdfParams>(printResumeAsPdf);
|
const { mutateAsync, isLoading } = useMutation<string, ServerError, PrintResumeAsPdfParams>(printResumeAsPdf);
|
||||||
|
|
||||||
const pdfListItemText = {
|
const pdfListItemText = {
|
||||||
normal: {
|
normal: {
|
||||||
primary: t('builder.rightSidebar.sections.export.pdf.normal.primary'),
|
primary: t<string>('builder.rightSidebar.sections.export.pdf.normal.primary'),
|
||||||
secondary: t('builder.rightSidebar.sections.export.pdf.normal.secondary'),
|
secondary: t<string>('builder.rightSidebar.sections.export.pdf.normal.secondary'),
|
||||||
},
|
},
|
||||||
loading: {
|
loading: {
|
||||||
primary: t('builder.rightSidebar.sections.export.pdf.loading.primary'),
|
primary: t<string>('builder.rightSidebar.sections.export.pdf.loading.primary'),
|
||||||
secondary: t('builder.rightSidebar.sections.export.pdf.loading.secondary'),
|
secondary: t<string>('builder.rightSidebar.sections.export.pdf.loading.secondary'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -34,9 +35,10 @@ const Export = () => {
|
|||||||
|
|
||||||
const redactedResume = pick(resume, ['basics', 'sections', 'metadata', 'public']);
|
const redactedResume = pick(resume, ['basics', 'sections', 'metadata', 'public']);
|
||||||
const jsonString = JSON.stringify(redactedResume, null, 4);
|
const jsonString = JSON.stringify(redactedResume, null, 4);
|
||||||
|
const jsonBlob = new Blob([jsonString], { type: 'application/json;charset=utf-8' });
|
||||||
const filename = `RxResume_JSONExport_${nanoid()}.json`;
|
const filename = `RxResume_JSONExport_${nanoid()}.json`;
|
||||||
|
|
||||||
download(jsonString, filename, 'application/json');
|
download(jsonBlob, filename);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExportPDF = async () => {
|
const handleExportPDF = async () => {
|
||||||
@ -44,15 +46,16 @@ const Export = () => {
|
|||||||
|
|
||||||
const slug = get(resume, 'slug');
|
const slug = get(resume, 'slug');
|
||||||
const username = get(resume, 'user.username');
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading path="metadata.export" name={t('builder.rightSidebar.sections.export.heading')} />
|
<Heading path="metadata.export" name={t<string>('builder.rightSidebar.sections.export.heading')} />
|
||||||
|
|
||||||
<List sx={{ padding: 0 }}>
|
<List sx={{ padding: 0 }}>
|
||||||
<ListItem sx={{ padding: 0 }}>
|
<ListItem sx={{ padding: 0 }}>
|
||||||
@ -60,8 +63,8 @@ const Export = () => {
|
|||||||
<Schema />
|
<Schema />
|
||||||
|
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('builder.rightSidebar.sections.export.json.primary')}
|
primary={t<string>('builder.rightSidebar.sections.export.json.primary')}
|
||||||
secondary={t('builder.rightSidebar.sections.export.json.secondary')}
|
secondary={t<string>('builder.rightSidebar.sections.export.json.secondary')}
|
||||||
/>
|
/>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
|
import { DragDropContext, Draggable, DraggableLocation, Droppable, DropResult } from '@hello-pangea/dnd';
|
||||||
import { Add, Close, Restore } from '@mui/icons-material';
|
import { Add, Close, Restore } from '@mui/icons-material';
|
||||||
import { Button, IconButton, Tooltip } from '@mui/material';
|
import { Button, IconButton, Tooltip } from '@mui/material';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { DragDropContext, Draggable, DraggableLocation, Droppable, DropResult } from 'react-beautiful-dnd';
|
|
||||||
|
|
||||||
import Heading from '@/components/shared/Heading';
|
import Heading from '@/components/shared/Heading';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
@ -23,8 +23,8 @@ const Layout = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const layout = useAppSelector((state) => state.resume.metadata.layout);
|
const layout = useAppSelector((state) => state.resume.present.metadata.layout);
|
||||||
const resumeSections = useAppSelector((state) => state.resume.sections);
|
const resumeSections = useAppSelector((state) => state.resume.present.sections);
|
||||||
|
|
||||||
const onDragEnd = (dropResult: DropResult) => {
|
const onDragEnd = (dropResult: DropResult) => {
|
||||||
const { source: srcLoc, destination: destLoc } = dropResult;
|
const { source: srcLoc, destination: destLoc } = dropResult;
|
||||||
@ -60,9 +60,9 @@ const Layout = () => {
|
|||||||
<>
|
<>
|
||||||
<Heading
|
<Heading
|
||||||
path="metadata.layout"
|
path="metadata.layout"
|
||||||
name={t('builder.rightSidebar.sections.layout.heading')}
|
name={t<string>('builder.rightSidebar.sections.layout.heading')}
|
||||||
action={
|
action={
|
||||||
<Tooltip title={t('builder.rightSidebar.sections.layout.tooltip.reset-layout') as string}>
|
<Tooltip title={t<string>('builder.rightSidebar.sections.layout.tooltip.reset-layout')}>
|
||||||
<IconButton onClick={handleResetLayout}>
|
<IconButton onClick={handleResetLayout}>
|
||||||
<Restore />
|
<Restore />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@ -76,12 +76,16 @@ const Layout = () => {
|
|||||||
<div key={pageIndex} className={styles.page}>
|
<div key={pageIndex} className={styles.page}>
|
||||||
<div className="flex items-center justify-between pr-3">
|
<div className="flex items-center justify-between pr-3">
|
||||||
<p className={styles.heading}>
|
<p className={styles.heading}>
|
||||||
{t('builder.common.glossary.page')} {pageIndex + 1}
|
{t<string>('builder.common.glossary.page')} {pageIndex + 1}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className={clsx(styles.delete, { hidden: pageIndex === 0 })}>
|
<div className={clsx(styles.delete, { hidden: pageIndex === 0 })}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={t('builder.common.actions.delete', { token: t('builder.common.glossary.page') }) as string}
|
title={
|
||||||
|
t<string>('builder.common.actions.delete', {
|
||||||
|
token: t<string>('builder.common.glossary.page'),
|
||||||
|
}) as string
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<IconButton size="small" onClick={() => handleDeletePage(pageIndex)}>
|
<IconButton size="small" onClick={() => handleDeletePage(pageIndex)}>
|
||||||
<Close fontSize="small" />
|
<Close fontSize="small" />
|
||||||
@ -113,7 +117,7 @@ const Layout = () => {
|
|||||||
[styles.disabled]: !get(resumeSections, `${sectionId}.visible`, true),
|
[styles.disabled]: !get(resumeSections, `${sectionId}.visible`, true),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{get(resumeSections, `${sectionId}.name`)}
|
{get(resumeSections, `${sectionId}.name`, '') as string}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -132,7 +136,7 @@ const Layout = () => {
|
|||||||
|
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<Button variant="outlined" startIcon={<Add />} onClick={handleAddPage}>
|
<Button variant="outlined" startIcon={<Add />} onClick={handleAddPage}>
|
||||||
{t('builder.common.actions.add', { token: t('builder.common.glossary.page') })}
|
{t<string>('builder.common.actions.add', { token: t<string>('builder.common.glossary.page') })}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Button } from '@mui/material';
|
|||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
import Heading from '@/components/shared/Heading';
|
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 } from '@/constants/index';
|
||||||
|
|
||||||
import styles from './Links.module.scss';
|
import styles from './Links.module.scss';
|
||||||
|
|
||||||
@ -12,39 +12,47 @@ const Links = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading path="metadata.links" name={t('builder.rightSidebar.sections.links.heading')} />
|
<Heading path="metadata.links" name={t<string>('builder.rightSidebar.sections.links.heading')} />
|
||||||
|
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<h2>
|
<h2>
|
||||||
<Savings fontSize="small" />
|
<Savings fontSize="small" />
|
||||||
{t('builder.rightSidebar.sections.links.donate.heading')}
|
{t<string>('builder.rightSidebar.sections.links.donate.heading')}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p>{t('builder.rightSidebar.sections.links.donate.body')}</p>
|
<p>{t<string>('builder.rightSidebar.sections.links.donate.body')}</p>
|
||||||
|
|
||||||
<a href={DONATION_URL} target="_blank" rel="noreferrer">
|
<a href={DONATION_URL} target="_blank" rel="noreferrer">
|
||||||
<Button startIcon={<Coffee />}>{t('builder.rightSidebar.sections.links.donate.button')}</Button>
|
<Button startIcon={<Coffee />}>{t<string>('builder.rightSidebar.sections.links.donate.button')}</Button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<h2>
|
<h2>
|
||||||
<BugReport fontSize="small" />
|
<BugReport fontSize="small" />
|
||||||
{t('builder.rightSidebar.sections.links.bugs-features.heading')}
|
{t<string>('builder.rightSidebar.sections.links.bugs-features.heading')}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p>{t('builder.rightSidebar.sections.links.bugs-features.body')}</p>
|
<p>{t<string>('builder.rightSidebar.sections.links.bugs-features.body')}</p>
|
||||||
|
|
||||||
<a href={GITHUB_ISSUES_URL} target="_blank" rel="noreferrer">
|
<a href={GITHUB_ISSUES_URL} target="_blank" rel="noreferrer">
|
||||||
<Button startIcon={<GitHub />}>{t('builder.rightSidebar.sections.links.bugs-features.button')}</Button>
|
<Button startIcon={<GitHub />}>
|
||||||
|
{t<string>('builder.rightSidebar.sections.links.bugs-features.button')}
|
||||||
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<a href={GITHUB_URL} target="_blank" rel="noreferrer">
|
<a href={GITHUB_URL} target="_blank" rel="noreferrer">
|
||||||
<Button variant="text" startIcon={<Link />}>
|
<Button variant="text" startIcon={<Link />}>
|
||||||
{t('builder.rightSidebar.sections.links.github')}
|
{t<string>('builder.rightSidebar.sections.links.github')}
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href={DOCS_URL} target="_blank" rel="noreferrer">
|
||||||
|
<Button variant="text" startIcon={<Link />}>
|
||||||
|
{t<string>('builder.rightSidebar.sections.links.docs')}
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -10,12 +10,12 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
TextField,
|
TextField,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { DateConfig, Resume } from '@reactive-resume/schema';
|
import { DateConfig, PageConfig, Resume } from '@reactive-resume/schema';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useMutation } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
|
|
||||||
import Heading from '@/components/shared/Heading';
|
import Heading from '@/components/shared/Heading';
|
||||||
@ -36,9 +36,11 @@ const Settings = () => {
|
|||||||
|
|
||||||
const { locale, ...router } = useRouter();
|
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 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 breakLine = useAppSelector((state) => state.build.page.breakLine);
|
||||||
const orientation = useAppSelector((state) => state.build.page.orientation);
|
const orientation = useAppSelector((state) => state.build.page.orientation);
|
||||||
|
|
||||||
@ -46,9 +48,10 @@ const Settings = () => {
|
|||||||
const slug: string = useMemo(() => get(resume, 'slug'), [resume]);
|
const slug: string = useMemo(() => get(resume, 'slug'), [resume]);
|
||||||
const username: string = useMemo(() => get(resume, 'user.username'), [resume]);
|
const username: string = useMemo(() => get(resume, 'user.username'), [resume]);
|
||||||
const dateConfig: DateConfig = useMemo(() => get(resume, 'metadata.date'), [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 isDarkMode = useMemo(() => theme === 'dark', [theme]);
|
||||||
const exampleString = useMemo(() => `Eg. ${dayjs().format(dateConfig.format)}`, [dateConfig.format]);
|
const exampleDateString = useMemo(() => `Eg. ${dayjs().utc().format(dateConfig.format)}`, [dateConfig.format]);
|
||||||
const themeString = useMemo(() => (isDarkMode ? 'Matte Black Everything' : 'As bright as your future'), [isDarkMode]);
|
const themeString = useMemo(() => (isDarkMode ? 'Matte Black Everything' : 'As bright as your future'), [isDarkMode]);
|
||||||
|
|
||||||
const { mutateAsync: loadSampleDataMutation } = useMutation<Resume, ServerError, LoadSampleDataParams>(
|
const { mutateAsync: loadSampleDataMutation } = useMutation<Resume, ServerError, LoadSampleDataParams>(
|
||||||
@ -58,6 +61,9 @@ const Settings = () => {
|
|||||||
|
|
||||||
const handleSetTheme = (value: boolean) => dispatch(setTheme({ theme: value ? 'dark' : 'light' }));
|
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) =>
|
const handleChangeDateFormat = (value: string | null) =>
|
||||||
dispatch(setResumeState({ path: 'metadata.date.format', value }));
|
dispatch(setResumeState({ path: 'metadata.date.format', value }));
|
||||||
|
|
||||||
@ -78,20 +84,25 @@ const Settings = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleResetResume = async () => {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading path="metadata.settings" name={t('builder.rightSidebar.sections.settings.heading')} />
|
<Heading path="metadata.settings" name={t<string>('builder.rightSidebar.sections.settings.heading')} />
|
||||||
|
|
||||||
<List sx={{ padding: 0 }}>
|
<List disablePadding>
|
||||||
{/* Global Settings */}
|
{/* Global Settings */}
|
||||||
<>
|
<>
|
||||||
<ListSubheader className="rounded">
|
<ListSubheader disableSticky className="rounded">
|
||||||
{t('builder.rightSidebar.sections.settings.global.heading')}
|
{t<string>('builder.rightSidebar.sections.settings.global.heading')}
|
||||||
</ListSubheader>
|
</ListSubheader>
|
||||||
|
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@ -99,7 +110,7 @@ const Settings = () => {
|
|||||||
<Palette />
|
<Palette />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('builder.rightSidebar.sections.settings.global.theme.primary')}
|
primary={t<string>('builder.rightSidebar.sections.settings.global.theme.primary')}
|
||||||
secondary={themeString}
|
secondary={themeString}
|
||||||
/>
|
/>
|
||||||
<ThemeSwitch checked={isDarkMode} onChange={(_, value: boolean) => handleSetTheme(value)} />
|
<ThemeSwitch checked={isDarkMode} onChange={(_, value: boolean) => handleSetTheme(value)} />
|
||||||
@ -108,26 +119,26 @@ const Settings = () => {
|
|||||||
<ListItem className="flex-col">
|
<ListItem className="flex-col">
|
||||||
<ListItemText
|
<ListItemText
|
||||||
className="w-full"
|
className="w-full"
|
||||||
primary={t('builder.rightSidebar.sections.settings.global.date.primary')}
|
primary={t<string>('builder.rightSidebar.sections.settings.global.date.primary')}
|
||||||
secondary={t('builder.rightSidebar.sections.settings.global.date.secondary')}
|
secondary={t<string>('builder.rightSidebar.sections.settings.global.date.secondary')}
|
||||||
/>
|
/>
|
||||||
<Autocomplete<string, false, boolean, false>
|
<Autocomplete<string, false, true, false>
|
||||||
disableClearable
|
disableClearable
|
||||||
className="my-2 w-full"
|
className="my-2 w-full"
|
||||||
options={dateFormatOptions}
|
options={dateFormatOptions}
|
||||||
value={dateConfig.format}
|
value={dateConfig.format}
|
||||||
onChange={(_, value) => handleChangeDateFormat(value)}
|
onChange={(_, value) => handleChangeDateFormat(value)}
|
||||||
renderInput={(params) => <TextField {...params} helperText={exampleString} />}
|
renderInput={(params) => <TextField {...params} helperText={exampleDateString} />}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem className="flex-col">
|
<ListItem className="flex-col">
|
||||||
<ListItemText
|
<ListItemText
|
||||||
className="w-full"
|
className="w-full"
|
||||||
primary={t('builder.rightSidebar.sections.settings.global.language.primary')}
|
primary={t<string>('builder.rightSidebar.sections.settings.global.language.primary')}
|
||||||
secondary={t('builder.rightSidebar.sections.settings.global.language.secondary')}
|
secondary={t<string>('builder.rightSidebar.sections.settings.global.language.secondary')}
|
||||||
/>
|
/>
|
||||||
<Autocomplete<Language, false, boolean, false>
|
<Autocomplete<Language, false, true, false>
|
||||||
disableClearable
|
disableClearable
|
||||||
className="my-2 w-full"
|
className="my-2 w-full"
|
||||||
options={languages}
|
options={languages}
|
||||||
@ -148,15 +159,34 @@ const Settings = () => {
|
|||||||
|
|
||||||
{/* Page Settings */}
|
{/* Page Settings */}
|
||||||
<>
|
<>
|
||||||
<ListSubheader className="rounded">{t('builder.rightSidebar.sections.settings.page.heading')}</ListSubheader>
|
<ListSubheader disableSticky className="rounded">
|
||||||
|
{t<string>('builder.rightSidebar.sections.settings.page.heading')}
|
||||||
|
</ListSubheader>
|
||||||
|
|
||||||
|
<ListItem 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>
|
<ListItem>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('builder.rightSidebar.sections.settings.page.orientation.primary')}
|
primary={t<string>('builder.rightSidebar.sections.settings.page.orientation.primary')}
|
||||||
secondary={
|
secondary={
|
||||||
pages.length === 1
|
pages.length === 1
|
||||||
? t('builder.rightSidebar.sections.settings.page.orientation.disabled')
|
? t<string>('builder.rightSidebar.sections.settings.page.orientation.disabled')
|
||||||
: t('builder.rightSidebar.sections.settings.page.orientation.secondary')
|
: t<string>('builder.rightSidebar.sections.settings.page.orientation.secondary')
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
@ -169,8 +199,8 @@ const Settings = () => {
|
|||||||
|
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('builder.rightSidebar.sections.settings.page.break-line.primary')}
|
primary={t<string>('builder.rightSidebar.sections.settings.page.break-line.primary')}
|
||||||
secondary={t('builder.rightSidebar.sections.settings.page.break-line.secondary')}
|
secondary={t<string>('builder.rightSidebar.sections.settings.page.break-line.secondary')}
|
||||||
/>
|
/>
|
||||||
<Switch color="secondary" checked={breakLine} onChange={() => dispatch(togglePageBreakLine())} />
|
<Switch color="secondary" checked={breakLine} onChange={() => dispatch(togglePageBreakLine())} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@ -178,30 +208,34 @@ const Settings = () => {
|
|||||||
|
|
||||||
{/* Resume Settings */}
|
{/* Resume Settings */}
|
||||||
<>
|
<>
|
||||||
<ListSubheader className="rounded">
|
<ListSubheader disableSticky className="rounded">
|
||||||
{t('builder.rightSidebar.sections.settings.resume.heading')}
|
{t<string>('builder.rightSidebar.sections.settings.resume.heading')}
|
||||||
</ListSubheader>
|
</ListSubheader>
|
||||||
|
|
||||||
<ListItem>
|
<ListItem disableGutters>
|
||||||
<ListItemButton onClick={handleLoadSampleData}>
|
<ListItemButton onClick={handleLoadSampleData}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Anchor />
|
<Anchor />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('builder.rightSidebar.sections.settings.resume.sample.primary')}
|
primary={t<string>('builder.rightSidebar.sections.settings.resume.sample.primary')}
|
||||||
secondary={t('builder.rightSidebar.sections.settings.resume.sample.secondary')}
|
secondary={t<string>('builder.rightSidebar.sections.settings.resume.sample.secondary')}
|
||||||
/>
|
/>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem>
|
<ListItem disableGutters>
|
||||||
<ListItemButton onClick={handleResetResume}>
|
<ListItemButton onClick={handleResetResume}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DeleteForever />
|
<DeleteForever />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('builder.rightSidebar.sections.settings.resume.reset.primary')}
|
primary={
|
||||||
secondary={t('builder.rightSidebar.sections.settings.resume.reset.secondary')}
|
confirmReset
|
||||||
|
? 'Are you sure?'
|
||||||
|
: t<string>('builder.rightSidebar.sections.settings.resume.reset.primary')
|
||||||
|
}
|
||||||
|
secondary={t<string>('builder.rightSidebar.sections.settings.resume.reset.secondary')}
|
||||||
/>
|
/>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const Sharing = () => {
|
|||||||
|
|
||||||
const [showShortUrl, setShowShortUrl] = useState(false);
|
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 isPublic = useMemo(() => get(resume, 'public'), [resume]);
|
||||||
const url = useMemo(() => getResumeUrl(resume, { withHost: true }), [resume]);
|
const url = useMemo(() => getResumeUrl(resume, { withHost: true }), [resume]);
|
||||||
const shortUrl = useMemo(() => getResumeUrl(resume, { withHost: true, shortUrl: true }), [resume]);
|
const shortUrl = useMemo(() => getResumeUrl(resume, { withHost: true, shortUrl: true }), [resume]);
|
||||||
@ -29,19 +29,19 @@ const Sharing = () => {
|
|||||||
|
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
|
|
||||||
toast.success(t('common.toast.success.resume-link-copied'));
|
toast.success(t<string>('common.toast.success.resume-link-copied'));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading path="metadata.sharing" name={t('builder.rightSidebar.sections.sharing.heading')} />
|
<Heading path="metadata.sharing" name={t<string>('builder.rightSidebar.sections.sharing.heading')} />
|
||||||
|
|
||||||
<List sx={{ padding: 0 }}>
|
<List sx={{ padding: 0 }}>
|
||||||
<ListItem className="flex flex-col" sx={{ padding: 0 }}>
|
<ListItem className="flex flex-col" sx={{ padding: 0 }}>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('builder.rightSidebar.sections.sharing.visibility.title')}
|
primary={t<string>('builder.rightSidebar.sections.sharing.visibility.title')}
|
||||||
secondary={t('builder.rightSidebar.sections.sharing.visibility.subtitle')}
|
secondary={t<string>('builder.rightSidebar.sections.sharing.visibility.subtitle')}
|
||||||
/>
|
/>
|
||||||
<Switch color="secondary" checked={isPublic} onChange={(_, value) => handleSetVisibility(value)} />
|
<Switch color="secondary" checked={isPublic} onChange={(_, value) => handleSetVisibility(value)} />
|
||||||
</div>
|
</div>
|
||||||
@ -63,7 +63,7 @@ const Sharing = () => {
|
|||||||
|
|
||||||
<div className="mt-1 flex w-full">
|
<div className="mt-1 flex w-full">
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label={t('builder.rightSidebar.sections.sharing.short-url.label') as string}
|
label={t<string>('builder.rightSidebar.sections.sharing.short-url.label')}
|
||||||
control={
|
control={
|
||||||
<Checkbox className="mr-1" checked={showShortUrl} onChange={(_, value) => setShowShortUrl(value)} />
|
<Checkbox className="mr-1" checked={showShortUrl} onChange={(_, value) => setShowShortUrl(value)} />
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ const Templates = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
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) => {
|
const handleChange = (template: TemplateMeta) => {
|
||||||
dispatch(setResumeState({ path: 'metadata.template', value: template.id }));
|
dispatch(setResumeState({ path: 'metadata.template', value: template.id }));
|
||||||
@ -24,14 +24,21 @@ const Templates = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading path="metadata.templates" name={t('builder.rightSidebar.sections.templates.heading')} />
|
<Heading path="metadata.templates" name={t<string>('builder.rightSidebar.sections.templates.heading')} />
|
||||||
|
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
{Object.values(templateMap).map((template) => (
|
{Object.values(templateMap).map((template) => (
|
||||||
<div key={template.id} className={styles.template}>
|
<div key={template.id} className={styles.template}>
|
||||||
<div className={clsx(styles.preview, { [styles.selected]: template.id === currentTemplate })}>
|
<div className={clsx(styles.preview, { [styles.selected]: template.id === currentTemplate })}>
|
||||||
<ButtonBase onClick={() => handleChange(template)}>
|
<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>
|
</ButtonBase>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
.container {
|
.container {
|
||||||
@apply grid sm:grid-cols-2 gap-4;
|
@apply grid gap-4 sm:grid-cols-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.colorOptions {
|
.colorOptions {
|
||||||
@apply col-span-2 mb-4;
|
@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 get from 'lodash/get';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
@ -16,15 +16,17 @@ const Theme = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
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) => {
|
const handleChange = (property: string, color: string) => {
|
||||||
dispatch(setResumeState({ path: `metadata.theme.${property}`, value: color }));
|
dispatch(setResumeState({ path: `metadata.theme.${property}`, value: color[0] !== '#' ? `#${color}` : color }));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading path="metadata.theme" name={t('builder.rightSidebar.sections.theme.heading')} />
|
<Heading path="metadata.theme" name={t<string>('builder.rightSidebar.sections.theme.heading')} />
|
||||||
|
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.colorOptions}>
|
<div className={styles.colorOptions}>
|
||||||
@ -34,18 +36,18 @@ const Theme = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label={t('builder.rightSidebar.sections.theme.form.primary.label')}
|
label={t<string>('builder.rightSidebar.sections.theme.form.primary.label')}
|
||||||
color={primary}
|
color={primary}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
onChange={(color) => handleChange('primary', color)}
|
onChange={(color) => handleChange('primary', color)}
|
||||||
/>
|
/>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label={t('builder.rightSidebar.sections.theme.form.background.label')}
|
label={t<string>('builder.rightSidebar.sections.theme.form.background.label')}
|
||||||
color={background}
|
color={background}
|
||||||
onChange={(color) => handleChange('background', color)}
|
onChange={(color) => handleChange('background', color)}
|
||||||
/>
|
/>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label={t('builder.rightSidebar.sections.theme.form.text.label')}
|
label={t<string>('builder.rightSidebar.sections.theme.form.text.label')}
|
||||||
color={text}
|
color={text}
|
||||||
onChange={(color) => handleChange('text', color)}
|
onChange={(color) => handleChange('text', color)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -33,7 +33,7 @@ const Widgets: React.FC<WidgetProps> = ({ label, category }) => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
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, {
|
const { data: fonts } = useQuery(FONTS_QUERY, fetchFonts, {
|
||||||
select: (fonts) => fonts.sort((a, b) => a.category.localeCompare(b.category)),
|
select: (fonts) => fonts.sort((a, b) => a.category.localeCompare(b.category)),
|
||||||
@ -64,7 +64,7 @@ const Widgets: React.FC<WidgetProps> = ({ label, category }) => {
|
|||||||
step={1}
|
step={1}
|
||||||
marks={[
|
marks={[
|
||||||
{ value: 12, label: '12px' },
|
{ value: 12, label: '12px' },
|
||||||
{ value: 24, label: t('builder.rightSidebar.sections.typography.form.font-size.label') },
|
{ value: 24, label: t<string>('builder.rightSidebar.sections.typography.form.font-size.label') },
|
||||||
{ value: 36, label: '36px' },
|
{ value: 36, label: '36px' },
|
||||||
]}
|
]}
|
||||||
valueLabelDisplay="auto"
|
valueLabelDisplay="auto"
|
||||||
@ -82,7 +82,10 @@ const Widgets: React.FC<WidgetProps> = ({ label, category }) => {
|
|||||||
value={fonts.find((font) => font.family === family[category])}
|
value={fonts.find((font) => font.family === family[category])}
|
||||||
onChange={(_, font: Font | null) => handleChange('family', font)}
|
onChange={(_, font: Font | null) => handleChange('family', font)}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField {...params} label={t('builder.rightSidebar.sections.typography.form.font-family.label')} />
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label={t<string>('builder.rightSidebar.sections.typography.form.font-family.label')}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -95,10 +98,13 @@ const Typography = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading path="metadata.typography" name={t('builder.rightSidebar.sections.typography.heading')} />
|
<Heading path="metadata.typography" name={t<string>('builder.rightSidebar.sections.typography.heading')} />
|
||||||
|
|
||||||
<Widgets label={t('builder.rightSidebar.sections.typography.widgets.headings.label')} category="heading" />
|
<Widgets
|
||||||
<Widgets label={t('builder.rightSidebar.sections.typography.widgets.body.label')} category="body" />
|
label={t<string>('builder.rightSidebar.sections.typography.widgets.headings.label')}
|
||||||
|
category="heading"
|
||||||
|
/>
|
||||||
|
<Widgets label={t<string>('builder.rightSidebar.sections.typography.widgets.body.label')} category="body" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,9 +16,7 @@ type Props = {
|
|||||||
const ResumeCard: React.FC<Props> = ({ modal, icon: Icon, title, subtitle }) => {
|
const ResumeCard: React.FC<Props> = ({ modal, icon: Icon, title, subtitle }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => dispatch(setModalState({ modal, state: { open: true } }));
|
||||||
dispatch(setModalState({ modal, state: { open: true } }));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.resume}>
|
<section className={styles.resume}>
|
||||||
|
|||||||
@ -94,7 +94,7 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
|||||||
const url = getResumeUrl(resume, { withHost: true });
|
const url = getResumeUrl(resume, { withHost: true });
|
||||||
await navigator.clipboard.writeText(url);
|
await navigator.clipboard.writeText(url);
|
||||||
|
|
||||||
toast.success(t('common.toast.success.resume-link-copied'));
|
toast.success(t<string>('common.toast.success.resume-link-copied'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
@ -115,16 +115,14 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ButtonBase className={styles.preview}>
|
<ButtonBase className={styles.preview}>
|
||||||
{resume.image ? (
|
{resume.image ? <Image src={resume.image} alt={resume.name} priority width={400} height={0} /> : null}
|
||||||
<Image src={resume.image} alt={resume.name} objectFit="cover" layout="fill" priority />
|
|
||||||
) : null}
|
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<div className={styles.meta}>
|
<div className={styles.meta}>
|
||||||
<p>{resume.name}</p>
|
<p>{resume.name}</p>
|
||||||
<p>{t('dashboard.resume.timestamp', { timestamp: getRelativeTime(resume.updatedAt) })}</p>
|
<p>{t<string>('dashboard.resume.timestamp', { timestamp: getRelativeTime(resume.updatedAt) })}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ButtonBase className={styles.menu} onClick={handleOpenMenu}>
|
<ButtonBase className={styles.menu} onClick={handleOpenMenu}>
|
||||||
@ -136,21 +134,21 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<OpenInNew className="scale-90" />
|
<OpenInNew className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('dashboard.resume.menu.open')}</ListItemText>
|
<ListItemText>{t<string>('dashboard.resume.menu.open')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem onClick={handleRename}>
|
<MenuItem onClick={handleRename}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DriveFileRenameOutline className="scale-90" />
|
<DriveFileRenameOutline className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('dashboard.resume.menu.rename')}</ListItemText>
|
<ListItemText>{t<string>('dashboard.resume.menu.rename')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem onClick={handleDuplicate}>
|
<MenuItem onClick={handleDuplicate}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<ContentCopy className="scale-90" />
|
<ContentCopy className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('dashboard.resume.menu.duplicate')}</ListItemText>
|
<ListItemText>{t<string>('dashboard.resume.menu.duplicate')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
{resume.public ? (
|
{resume.public ? (
|
||||||
@ -158,27 +156,27 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LinkIcon className="scale-90" />
|
<LinkIcon className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('dashboard.resume.menu.share-link')}</ListItemText>
|
<ListItemText>{t<string>('dashboard.resume.menu.share-link')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip arrow placement="right" title={t('dashboard.resume.menu.tooltips.share-link') as string}>
|
<Tooltip arrow placement="right" title={t<string>('dashboard.resume.menu.tooltips.share-link')}>
|
||||||
<div>
|
<div>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LinkIcon className="scale-90" />
|
<LinkIcon className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('dashboard.resume.menu.share-link')}</ListItemText>
|
<ListItemText>{t<string>('dashboard.resume.menu.share-link')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Tooltip arrow placement="right" title={t('dashboard.resume.menu.tooltips.delete') as string}>
|
<Tooltip arrow placement="right" title={t<string>('dashboard.resume.menu.tooltips.delete')}>
|
||||||
<MenuItem onClick={handleDelete}>
|
<MenuItem onClick={handleDelete}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DeleteOutline className="scale-90" />
|
<DeleteOutline className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('dashboard.resume.menu.delete')}</ListItemText>
|
<ListItemText>{t<string>('dashboard.resume.menu.delete')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
.testimony {
|
.testimony {
|
||||||
@apply grid gap-2;
|
@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 {
|
blockquote {
|
||||||
@apply text-xs leading-normal text-justify opacity-90;
|
@apply text-justify text-xs leading-normal opacity-90;
|
||||||
}
|
}
|
||||||
|
|
||||||
figcaption {
|
figcaption {
|
||||||
|
|||||||
@ -8,14 +8,14 @@ import styles from './ArrayInput.module.scss';
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: string;
|
label: string;
|
||||||
value: string[];
|
value?: string[];
|
||||||
className?: string;
|
className?: string;
|
||||||
onChange: (event: any) => void;
|
onChange: (event: any) => void;
|
||||||
errors?: FieldError | FieldError[];
|
errors?: FieldError | FieldError[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const ArrayInput: React.FC<Props> = ({ value, label, onChange, errors, className }) => {
|
const ArrayInput: React.FC<Props> = ({ value, label, onChange, errors, className }) => {
|
||||||
const [items, setItems] = useState<string[]>(value);
|
const [items, setItems] = useState<string[]>(value || []);
|
||||||
|
|
||||||
const onAdd = () => setItems([...items, '']);
|
const onAdd = () => setItems([...items, '']);
|
||||||
|
|
||||||
|
|||||||
@ -47,21 +47,21 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
|||||||
<Image
|
<Image
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
alt={user?.name}
|
|
||||||
className={styles.avatar}
|
className={styles.avatar}
|
||||||
src={getGravatarUrl(email, size)}
|
src={getGravatarUrl(email, size)}
|
||||||
|
alt={user?.name ?? 'User Avatar'}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Menu anchorEl={anchorEl} onClose={handleClose} open={Boolean(anchorEl)}>
|
<Menu anchorEl={anchorEl} onClose={handleClose} open={Boolean(anchorEl)}>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-xs opacity-50">{t('common.avatar.menu.greeting')}</span>
|
<span className="text-xs opacity-50">{t<string>('common.avatar.menu.greeting')}</span>
|
||||||
<p>{user?.name}</p>
|
<p>{user?.name}</p>
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
<MenuItem onClick={handleLogout}>{t('common.avatar.menu.logout')}</MenuItem>
|
<MenuItem onClick={handleLogout}>{t<string>('common.avatar.menu.logout')}</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
.content {
|
.content {
|
||||||
@apply rounded p-6 text-sm shadow lg:w-1/2 xl:w-2/5;
|
@apply rounded px-6 text-sm shadow lg:w-1/2 xl:w-2/5;
|
||||||
@apply absolute inset-4 sm:inset-x-4 sm:inset-y-auto lg:inset-auto;
|
@apply absolute inset-4 sm:inset-x-4 sm:inset-y-auto lg:inset-auto;
|
||||||
@apply overflow-scroll bg-neutral-50 dark:bg-neutral-900 lg:overflow-auto;
|
@apply overflow-scroll bg-neutral-50 dark:bg-neutral-900 lg:overflow-auto;
|
||||||
|
@apply max-h-[90vh] min-h-fit;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
@apply sticky top-0 left-0 right-0 z-50 bg-neutral-50 pt-6 dark:bg-neutral-900;
|
||||||
@apply flex items-center justify-between;
|
@apply flex items-center justify-between;
|
||||||
@apply w-full border-b pb-5 dark:border-white/10;
|
@apply w-full border-b pb-5 dark:border-white/10;
|
||||||
|
|
||||||
@ -27,6 +33,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
|
@apply sticky bottom-0 left-0 right-0 z-50 bg-neutral-50 pb-6 dark:bg-neutral-900;
|
||||||
@apply flex items-center justify-end gap-x-4;
|
@apply flex items-center justify-end gap-x-4;
|
||||||
@apply w-full border-t pt-5 dark:border-white/10;
|
@apply w-full border-t pt-5 dark:border-white/10;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,11 +5,12 @@ import { useRouter } from 'next/router';
|
|||||||
import styles from './BaseModal.module.scss';
|
import styles from './BaseModal.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
icon?: React.ReactNode;
|
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
heading: string;
|
heading: string;
|
||||||
handleClose: () => void;
|
icon?: React.ReactNode;
|
||||||
|
children?: React.ReactNode;
|
||||||
footerChildren?: React.ReactNode;
|
footerChildren?: React.ReactNode;
|
||||||
|
handleClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BaseModal: React.FC<Props> = ({ icon, isOpen, heading, children, handleClose, footerChildren }) => {
|
const BaseModal: React.FC<Props> = ({ icon, isOpen, heading, children, handleClose, footerChildren }) => {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const Footer: React.FC<Props> = ({ className }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('text-xs', className)}>
|
<div className={clsx('text-xs', className)}>
|
||||||
<p>{t('common.footer.license')}</p>
|
<p>{t<string>('common.footer.license')}</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<Trans t={t} i18nKey="common.footer.credit">
|
<Trans t={t} i18nKey="common.footer.credit">
|
||||||
|
|||||||
@ -32,8 +32,8 @@ const Heading: React.FC<Props> = ({
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`, name));
|
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`, name));
|
||||||
const visibility = useAppSelector((state) => get(state.resume, `${path}.visible`, true));
|
const visibility = useAppSelector((state) => get(state.resume.present, `${path}.visible`, true));
|
||||||
|
|
||||||
const [editMode, setEditMode] = useState(false);
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
|
||||||
@ -72,19 +72,19 @@ const Heading: React.FC<Props> = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isEditable && (
|
{isEditable && (
|
||||||
<Tooltip title={t('builder.common.tooltip.rename-section') as string}>
|
<Tooltip title={t<string>('builder.common.tooltip.rename-section')}>
|
||||||
<IconButton onClick={toggleEditMode}>{editMode ? <Check /> : <DriveFileRenameOutline />}</IconButton>
|
<IconButton onClick={toggleEditMode}>{editMode ? <Check /> : <DriveFileRenameOutline />}</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isHideable && (
|
{isHideable && (
|
||||||
<Tooltip title={t('builder.common.tooltip.toggle-visibility') as string}>
|
<Tooltip title={t<string>('builder.common.tooltip.toggle-visibility')}>
|
||||||
<IconButton onClick={toggleVisibility}>{visibility ? <Visibility /> : <VisibilityOff />}</IconButton>
|
<IconButton onClick={toggleVisibility}>{visibility ? <Visibility /> : <VisibilityOff />}</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isDeletable && (
|
{isDeletable && (
|
||||||
<Tooltip title={t('builder.common.tooltip.delete-section') as string}>
|
<Tooltip title={t<string>('builder.common.tooltip.delete-section')}>
|
||||||
<IconButton onClick={handleDelete}>
|
<IconButton onClick={handleDelete}>
|
||||||
<Delete />
|
<Delete />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@ -9,5 +9,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.language {
|
.language {
|
||||||
@apply py-2 px-4 cursor-pointer text-center hover:underline;
|
@apply cursor-pointer py-2 px-4 text-center hover:underline;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,70 +1,51 @@
|
|||||||
import { Language } from '@mui/icons-material';
|
import { Language } from '@mui/icons-material';
|
||||||
import { IconButton, Popover } from '@mui/material';
|
import { IconButton, Menu, MenuItem } from '@mui/material';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { MouseEvent, useState } from 'react';
|
import { MouseEvent, useState } from 'react';
|
||||||
|
|
||||||
import { languages } from '@/config/languages';
|
import { languages } from '@/config/languages';
|
||||||
import { useAppDispatch } from '@/store/hooks';
|
import { TRANSLATE_URL } from '@/constants/index';
|
||||||
import { setResumeState } from '@/store/resume/resumeSlice';
|
|
||||||
|
|
||||||
import styles from './LanguageSwitcher.module.scss';
|
|
||||||
|
|
||||||
const LanguageSwitcher = () => {
|
const LanguageSwitcher = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
const handleClick = (event: MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget);
|
const handleClick = (event: MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget);
|
||||||
|
|
||||||
const handleClose = () => setAnchorEl(null);
|
const handleClose = () => setAnchorEl(null);
|
||||||
|
|
||||||
const handleChangeLanguage = (locale: string) => {
|
const handleChange = (locale: string) => {
|
||||||
const { pathname, asPath, query } = router;
|
const { pathname, asPath, query } = router;
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
|
||||||
document.cookie = `NEXT_LOCALE=${locale}; path=/; expires=2147483647`;
|
document.cookie = `NEXT_LOCALE=${locale}; path=/; expires=2147483647`;
|
||||||
dispatch(setResumeState({ path: 'metadata.locale', value: locale }));
|
|
||||||
|
|
||||||
router.push({ pathname, query }, asPath, { locale });
|
router.push({ pathname, query }, asPath, { locale });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddLanguage = () => window.open(TRANSLATE_URL, '_blank');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<IconButton onClick={handleClick}>
|
<IconButton onClick={handleClick}>
|
||||||
<Language />
|
<Language />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Popover
|
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleClose}>
|
||||||
anchorEl={anchorEl}
|
{languages.map(({ code, name, localName }) => (
|
||||||
open={Boolean(anchorEl)}
|
<MenuItem key={code} onClick={() => handleChange(code)}>
|
||||||
onClose={handleClose}
|
{name} {localName && `(${localName})`}
|
||||||
anchorOrigin={{
|
</MenuItem>
|
||||||
vertical: 'top',
|
))}
|
||||||
horizontal: 'right',
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'right',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={styles.popover}>
|
|
||||||
<div className={styles.container}>
|
|
||||||
{languages.map(({ code, name, localName }) => (
|
|
||||||
<p key={code} className={styles.language} onClick={() => handleChangeLanguage(code)}>
|
|
||||||
{name} {localName && `(${localName})`}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<a href="https://translate.rxresu.me" target="_blank" rel="noreferrer" className={styles.language}>
|
<MenuItem>
|
||||||
{t('common.footer.language.missing')}
|
<span className="font-bold" onClick={handleAddLanguage}>
|
||||||
</a>
|
Add your language
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</MenuItem>
|
||||||
</Popover>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -36,7 +36,7 @@ const List: React.FC<Props> = ({
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
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) => {
|
const handleEdit = (item: ListItemType) => {
|
||||||
isFunction(onEdit) && onEdit(item);
|
isFunction(onEdit) && onEdit(item);
|
||||||
@ -66,7 +66,7 @@ const List: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<div className={clsx(styles.container, className)}>
|
<div className={clsx(styles.container, className)}>
|
||||||
{isEmpty(list) && <div className={styles.empty}>{t('builder.common.list.empty-text')}</div>}
|
{isEmpty(list) && <div className={styles.empty}>{t<string>('builder.common.list.empty-text')}</div>}
|
||||||
|
|
||||||
{list.map((item, index) => {
|
{list.map((item, index) => {
|
||||||
const title = get(item, titleKey, '');
|
const title = get(item, titleKey, '');
|
||||||
@ -76,6 +76,7 @@ const List: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
|
path={path}
|
||||||
item={item}
|
item={item}
|
||||||
index={index}
|
index={index}
|
||||||
title={title}
|
title={title}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ interface DragItem {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
item: ListItemType;
|
item: ListItemType;
|
||||||
|
path: string;
|
||||||
index: number;
|
index: number;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
@ -26,14 +27,14 @@ type Props = {
|
|||||||
onDuplicate?: (item: ListItemType) => void;
|
onDuplicate?: (item: ListItemType) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ListItem: React.FC<Props> = ({ item, index, title, subtitle, onMove, onEdit, onDelete, onDuplicate }) => {
|
const ListItem: React.FC<Props> = ({ item, path, index, title, subtitle, onMove, onEdit, onDelete, onDuplicate }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
|
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
|
||||||
|
|
||||||
const [{ handlerId }, drop] = useDrop<DragItem, any, any>({
|
const [{ handlerId }, drop] = useDrop<DragItem, any, any>({
|
||||||
accept: 'ListItem',
|
accept: path,
|
||||||
collect(monitor) {
|
collect(monitor) {
|
||||||
return { handlerId: monitor.getHandlerId() };
|
return { handlerId: monitor.getHandlerId() };
|
||||||
},
|
},
|
||||||
@ -68,7 +69,7 @@ const ListItem: React.FC<Props> = ({ item, index, title, subtitle, onMove, onEdi
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [{ isDragging }, drag] = useDrag({
|
const [{ isDragging }, drag] = useDrag({
|
||||||
type: 'ListItem',
|
type: path,
|
||||||
item: () => {
|
item: () => {
|
||||||
return { id: item.id, index };
|
return { id: item.id, index };
|
||||||
},
|
},
|
||||||
@ -125,25 +126,25 @@ const ListItem: React.FC<Props> = ({ item, index, title, subtitle, onMove, onEdi
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DriveFileRenameOutline className="scale-90" />
|
<DriveFileRenameOutline className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('builder.common.list.actions.edit')}</ListItemText>
|
<ListItemText>{t<string>('builder.common.list.actions.edit')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem onClick={() => handleDuplicate(item)}>
|
<MenuItem onClick={() => handleDuplicate(item)}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<FileCopy className="scale-90" />
|
<FileCopy className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('builder.common.list.actions.duplicate')}</ListItemText>
|
<ListItemText>{t<string>('builder.common.list.actions.duplicate')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip arrow placement="right" title={t('builder.common.tooltip.delete-item') as string}>
|
<Tooltip arrow placement="right" title={t<string>('builder.common.tooltip.delete-item')}>
|
||||||
<div>
|
<div>
|
||||||
<MenuItem onClick={() => handleDelete(item)}>
|
<MenuItem onClick={() => handleDelete(item)}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DeleteOutline className="scale-90" />
|
<DeleteOutline className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('builder.common.list.actions.delete')}</ListItemText>
|
<ListItemText>{t<string>('builder.common.list.actions.delete')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import styles from './Loading.module.scss';
|
|||||||
|
|
||||||
const Loading: React.FC = () => {
|
const Loading: React.FC = () => {
|
||||||
const { isReady } = useRouter();
|
const { isReady } = useRouter();
|
||||||
|
|
||||||
const isFetching = useIsFetching();
|
const isFetching = useIsFetching();
|
||||||
const isMutating = useIsMutating();
|
const isMutating = useIsMutating();
|
||||||
|
|
||||||
|
|||||||
@ -4,8 +4,8 @@ type Props = {
|
|||||||
size?: 256 | 64 | 48 | 40 | 32;
|
size?: 256 | 64 | 48 | 40 | 32;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Logo: React.FC<Props> = ({ size = 64 }) => {
|
const Logo: React.FC<Props> = ({ size = 64 }) => (
|
||||||
return <Image alt="Reactive Resume" src="/images/logos/logo.svg" className="rounded" width={size} height={size} />;
|
<Image alt="Reactive Resume" src="/images/logos/logo.svg" className="rounded" width={size} height={size} priority />
|
||||||
};
|
);
|
||||||
|
|
||||||
export default Logo;
|
export default Logo;
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children?: string;
|
children?: string;
|
||||||
@ -12,7 +11,7 @@ const Markdown: React.FC<Props> = ({ className, children }) => {
|
|||||||
if (!children || isEmpty(children)) return null;
|
if (!children || isEmpty(children)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]} className={clsx('markdown', className)}>
|
<ReactMarkdown remarkPlugins={[]} className={clsx('markdown', className)}>
|
||||||
{children}
|
{children}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
const NoSSR: React.FC = ({ children }) => <>{children}</>;
|
|
||||||
|
|
||||||
export default dynamic(() => Promise.resolve(NoSSR), { ssr: false });
|
|
||||||
@ -1,4 +1,7 @@
|
|||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
|
import { DatePicker } from '@mui/x-date-pickers';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@ -8,7 +11,7 @@ import { setResumeState } from '@/store/resume/resumeSlice';
|
|||||||
import MarkdownSupported from './MarkdownSupported';
|
import MarkdownSupported from './MarkdownSupported';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type?: 'text' | 'textarea';
|
type?: 'text' | 'textarea' | 'date';
|
||||||
label: string;
|
label: string;
|
||||||
path: string;
|
path: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -18,7 +21,8 @@ interface Props {
|
|||||||
const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, markdownSupported = false }) => {
|
const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, markdownSupported = false }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const stateValue = useAppSelector((state) => get(state.resume, path, ''));
|
const stateValue = useAppSelector((state) => get(state.resume.present, path, ''));
|
||||||
|
const dateFormat = useAppSelector((state) => state.resume.present.metadata.date.format);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue(stateValue);
|
setValue(stateValue);
|
||||||
@ -31,6 +35,11 @@ const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, m
|
|||||||
dispatch(setResumeState({ path, value: event.target.value }));
|
dispatch(setResumeState({ path, value: event.target.value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onChangeValue = (value: string) => {
|
||||||
|
setValue(value);
|
||||||
|
dispatch(setResumeState({ path, value }));
|
||||||
|
};
|
||||||
|
|
||||||
if (type === 'textarea') {
|
if (type === 'textarea') {
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
@ -45,6 +54,24 @@ const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, m
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'date') {
|
||||||
|
return (
|
||||||
|
<DatePicker
|
||||||
|
showToolbar
|
||||||
|
openTo="year"
|
||||||
|
label={label}
|
||||||
|
value={value}
|
||||||
|
toolbarFormat={dateFormat}
|
||||||
|
views={['year', 'month', 'day']}
|
||||||
|
renderInput={(params) => <TextField {...params} error={false} className={className} />}
|
||||||
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
|
isEmpty(keyboardInputValue) && onChangeValue('');
|
||||||
|
date && dayjs(date).isValid() && onChangeValue(dayjs(date).format('YYYY-MM-DD'));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <TextField type={type} label={label} value={value} onChange={onChange} className={className} />;
|
return <TextField type={type} label={label} value={value} onChange={onChange} className={className} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -2,21 +2,49 @@ export type Language = {
|
|||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
localName?: string;
|
localName?: string;
|
||||||
|
isRTL?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const languages: Language[] = [
|
export const languages: Language[] = [
|
||||||
|
{ code: 'am', name: 'Amharic', localName: 'አማርኛ' },
|
||||||
|
{ code: 'ar', name: 'Arabic', localName: 'اَلْعَرَبِيَّةُ', isRTL: true },
|
||||||
|
{ code: 'bg', name: 'Bulgarian', localName: 'български' },
|
||||||
{ code: 'bn', name: 'Bengali', 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: '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: 'en', name: 'English' },
|
||||||
{ code: 'es', name: 'Spanish', localName: 'Español' },
|
{ 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: 'fr', name: 'French', localName: 'Français' },
|
||||||
|
{ code: 'he', name: 'Hebrew', localName: 'Ivrit', isRTL: true },
|
||||||
{ code: 'hi', name: 'Hindi', localName: 'हिन्दी' },
|
{ code: 'hi', name: 'Hindi', localName: 'हिन्दी' },
|
||||||
|
{ code: 'hu', name: 'Hungarian', localName: 'Magyar' },
|
||||||
|
{ code: 'id', name: 'Indonesian', localName: 'Bahasa Indonesia' },
|
||||||
{ code: 'it', name: 'Italian', localName: 'Italiano' },
|
{ code: 'it', name: 'Italian', localName: 'Italiano' },
|
||||||
|
{ code: 'ja', name: 'Japanese', localName: '日本語' },
|
||||||
|
{ code: 'km', name: 'Khmer', localName: 'ភាសាខ្មែរ' },
|
||||||
{ code: 'kn', name: 'Kannada', 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: 'pl', name: 'Polish', localName: 'Polski' },
|
||||||
|
{ code: 'pt', name: 'Portuguese', localName: 'Português' },
|
||||||
|
{ 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: 'ta', name: 'Tamil', localName: 'தமிழ்' },
|
||||||
{ code: 'tr', name: 'Turkish', localName: 'Türkçe' },
|
{ 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: '中文' },
|
{ code: 'zh', name: 'Chinese', localName: '中文' },
|
||||||
].sort((a, b) => a.name.localeCompare(b.name));
|
].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import {
|
|||||||
VolunteerActivism,
|
VolunteerActivism,
|
||||||
Work,
|
Work,
|
||||||
} from '@mui/icons-material';
|
} 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 isEmpty from 'lodash/isEmpty';
|
||||||
|
|
||||||
import Basics from '@/components/build/LeftSidebar/sections/Basics';
|
import Basics from '@/components/build/LeftSidebar/sections/Basics';
|
||||||
@ -60,59 +60,136 @@ export const left: SidebarSection[] = [
|
|||||||
{
|
{
|
||||||
id: 'work',
|
id: 'work',
|
||||||
icon: <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',
|
id: 'education',
|
||||||
icon: <School />,
|
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',
|
id: 'awards',
|
||||||
icon: <EmojiEvents />,
|
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',
|
id: 'certifications',
|
||||||
icon: <CardGiftcard />,
|
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',
|
id: 'publications',
|
||||||
icon: <MenuBook />,
|
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',
|
id: 'skills',
|
||||||
icon: <Architecture />,
|
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',
|
id: 'languages',
|
||||||
icon: <Language />,
|
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',
|
id: 'interests',
|
||||||
icon: <Sailing />,
|
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',
|
id: 'volunteer',
|
||||||
icon: <VolunteerActivism />,
|
icon: <VolunteerActivism />,
|
||||||
component: (
|
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',
|
id: 'projects',
|
||||||
icon: <Coffee />,
|
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',
|
id: 'references',
|
||||||
icon: <Groups />,
|
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 [];
|
if (isEmpty(sections)) return [];
|
||||||
|
|
||||||
return Object.entries(sections).reduce((acc, [id, section]) => {
|
return Object.entries(sections).reduce((acc, [id, section]) => {
|
||||||
@ -173,7 +262,7 @@ export const getCustomSections = (sections: Record<string, SectionRecord>): Arra
|
|||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, [] as Array<Required<SectionRecord>>);
|
}, [] as SectionRecord[]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sections = [...left, ...right];
|
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: {
|
typography: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontFamily: 'Inter, sans-serif',
|
fontFamily: 'Inter, sans-serif',
|
||||||
@ -49,7 +49,7 @@ const theme = createTheme({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
export const lightTheme = createTheme({
|
export const lightTheme = createTheme({
|
||||||
...theme,
|
...theme,
|
||||||
|
|||||||
3
client/constants/flags.ts
Normal file
3
client/constants/flags.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import env from '@beam-australia/react-env';
|
||||||
|
|
||||||
|
export const FLAG_DISABLE_SIGNUPS = env('FLAG_DISABLE_SIGNUPS') === 'true';
|
||||||
@ -9,7 +9,10 @@ export const VALID_URL_REGEX = /[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}
|
|||||||
export const FILENAME_TIMESTAMP = 'DDMMYYYYHHmmss';
|
export const FILENAME_TIMESTAMP = 'DDMMYYYYHHmmss';
|
||||||
|
|
||||||
// Links
|
// Links
|
||||||
|
export const DOCS_URL = 'https://docs.rxresu.me';
|
||||||
export const DONATION_URL = 'https://paypal.me/RajaRajanA';
|
export const DONATION_URL = 'https://paypal.me/RajaRajanA';
|
||||||
|
export const TRANSLATE_URL = 'https://translate.rxresu.me/';
|
||||||
|
export const DIGITALOCEAN_URL = 'https://pillai.xyz/digitalocean';
|
||||||
export const GITHUB_URL = 'https://github.com/AmruthPillai/Reactive-Resume';
|
export const GITHUB_URL = 'https://github.com/AmruthPillai/Reactive-Resume';
|
||||||
export const PRODUCT_HUNT_URL = 'https://www.producthunt.com/posts/reactive-resume-v3';
|
export const PRODUCT_HUNT_URL = 'https://www.producthunt.com/posts/reactive-resume-v3';
|
||||||
export const GITHUB_ISSUES_URL = 'https://github.com/AmruthPillai/Reactive-Resume/issues/new/choose';
|
export const GITHUB_ISSUES_URL = 'https://github.com/AmruthPillai/Reactive-Resume/issues/new/choose';
|
||||||
|
|||||||
@ -54,16 +54,16 @@ const ForgotPasswordModal: React.FC = () => {
|
|||||||
<BaseModal
|
<BaseModal
|
||||||
icon={<Password />}
|
icon={<Password />}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
heading={t('modals.auth.forgot-password.heading')}
|
heading={t<string>('modals.auth.forgot-password.heading')}
|
||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
footerChildren={
|
footerChildren={
|
||||||
<Button type="submit" disabled={isLoading} onClick={handleSubmit(onSubmit)}>
|
<Button type="submit" disabled={isLoading} onClick={handleSubmit(onSubmit)}>
|
||||||
{t('modals.auth.forgot-password.actions.send-email')}
|
{t<string>('modals.auth.forgot-password.actions.send-email')}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<p>{t('modals.auth.forgot-password.body')}</p>
|
<p>{t<string>('modals.auth.forgot-password.body')}</p>
|
||||||
|
|
||||||
<form className="grid gap-4 xl:w-2/3">
|
<form className="grid gap-4 xl:w-2/3">
|
||||||
<Controller
|
<Controller
|
||||||
@ -72,7 +72,7 @@ const ForgotPasswordModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('modals.auth.forgot-password.form.email.label')}
|
label={t<string>('modals.auth.forgot-password.form.email.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -81,7 +81,7 @@ const ForgotPasswordModal: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p className="text-xs">{t('modals.auth.forgot-password.help-text')}</p>
|
<p className="text-xs">{t<string>('modals.auth.forgot-password.help-text')}</p>
|
||||||
</div>
|
</div>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
import env from '@beam-australia/react-env';
|
import env from '@beam-australia/react-env';
|
||||||
import { joiResolver } from '@hookform/resolvers/joi';
|
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 { Button, IconButton, InputAdornment, TextField } from '@mui/material';
|
||||||
|
import { CredentialResponse, GoogleLogin } from '@react-oauth/google';
|
||||||
import Joi from 'joi';
|
import Joi from 'joi';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
import { Trans, useTranslation } from 'next-i18next';
|
import { Trans, useTranslation } from 'next-i18next';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { GoogleLoginResponse, GoogleLoginResponseOffline, useGoogleLogin } from 'react-google-login';
|
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useIsMutating, useMutation } from 'react-query';
|
import { useIsMutating, useMutation } from 'react-query';
|
||||||
|
|
||||||
import BaseModal from '@/components/shared/BaseModal';
|
import BaseModal from '@/components/shared/BaseModal';
|
||||||
|
import { FLAG_DISABLE_SIGNUPS } from '@/constants/flags';
|
||||||
import { login, LoginParams, loginWithGoogle, LoginWithGoogleParams } from '@/services/auth';
|
import { login, LoginParams, loginWithGoogle, LoginWithGoogleParams } from '@/services/auth';
|
||||||
import { ServerError } from '@/services/axios';
|
import { ServerError } from '@/services/axios';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
@ -54,15 +56,6 @@ const LoginModal: React.FC = () => {
|
|||||||
loginWithGoogle
|
loginWithGoogle
|
||||||
);
|
);
|
||||||
|
|
||||||
const { signIn } = useGoogleLogin({
|
|
||||||
clientId: env('GOOGLE_CLIENT_ID'),
|
|
||||||
onSuccess: async (response: GoogleLoginResponse | GoogleLoginResponseOffline) => {
|
|
||||||
await loginWithGoogleMutation({ accessToken: (response as GoogleLoginResponse).accessToken });
|
|
||||||
|
|
||||||
handleClose();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
dispatch(setModalState({ modal: 'auth.login', state: { open: false } }));
|
dispatch(setModalState({ modal: 'auth.login', state: { open: false } }));
|
||||||
reset();
|
reset();
|
||||||
@ -91,8 +84,16 @@ const LoginModal: React.FC = () => {
|
|||||||
dispatch(setModalState({ modal: 'auth.forgot', state: { open: true } }));
|
dispatch(setModalState({ modal: 'auth.forgot', state: { open: true } }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoginWithGoogle = () => {
|
const handleLoginWithGoogle = async (response: CredentialResponse) => {
|
||||||
signIn();
|
if (response.credential) {
|
||||||
|
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleLoginWithGoogleError });
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoginWithGoogleError = () => {
|
||||||
|
toast("Please try logging in using email/password, or use another browser that supports Google's One Tap API.");
|
||||||
};
|
};
|
||||||
|
|
||||||
const PasswordVisibility = (): React.ReactElement => {
|
const PasswordVisibility = (): React.ReactElement => {
|
||||||
@ -111,27 +112,21 @@ const LoginModal: React.FC = () => {
|
|||||||
<BaseModal
|
<BaseModal
|
||||||
icon={<Login />}
|
icon={<Login />}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
heading={t('modals.auth.login.heading')}
|
heading={t<string>('modals.auth.login.heading')}
|
||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
footerChildren={
|
footerChildren={
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<Button
|
{!isEmpty(env('GOOGLE_CLIENT_ID')) && (
|
||||||
type="submit"
|
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleLoginWithGoogleError} />
|
||||||
variant="outlined"
|
)}
|
||||||
disabled={isLoading}
|
|
||||||
startIcon={<Google />}
|
|
||||||
onClick={handleLoginWithGoogle}
|
|
||||||
>
|
|
||||||
{t('modals.auth.login.actions.google')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
||||||
{t('modals.auth.login.actions.login')}
|
{t<string>('modals.auth.login.actions.login')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<p>{t('modals.auth.login.body')}</p>
|
<p>{t<string>('modals.auth.login.body')}</p>
|
||||||
|
|
||||||
<form className="grid gap-4 xl:w-2/3">
|
<form className="grid gap-4 xl:w-2/3">
|
||||||
<Controller
|
<Controller
|
||||||
@ -140,9 +135,9 @@ const LoginModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('modals.auth.login.form.username.label')}
|
label={t<string>('modals.auth.login.form.username.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || t('modals.auth.login.form.username.help-text')}
|
helperText={fieldState.error?.message || t<string>('modals.auth.login.form.username.help-text')}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -154,7 +149,7 @@ const LoginModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
label={t('modals.auth.login.form.password.label')}
|
label={t<string>('modals.auth.login.form.password.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
InputProps={{ endAdornment: <PasswordVisibility /> }}
|
InputProps={{ endAdornment: <PasswordVisibility /> }}
|
||||||
@ -164,16 +159,18 @@ const LoginModal: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p className="text-xs">
|
{!FLAG_DISABLE_SIGNUPS && (
|
||||||
<Trans t={t} i18nKey="modals.auth.login.register-text">
|
<p className="text-xs">
|
||||||
If you don't have one, you can <a onClick={handleCreateAccount}>create an account</a> here.
|
<Trans t={t} i18nKey="modals.auth.login.register-text">
|
||||||
</Trans>
|
If you don't have one, you can <a onClick={handleCreateAccount}>create an account here.</a>
|
||||||
</p>
|
</Trans>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<p className="text-xs">
|
<p className="text-xs">
|
||||||
<Trans t={t} i18nKey="modals.auth.login.recover-text">
|
<Trans t={t} i18nKey="modals.auth.login.recover-text">
|
||||||
In case you have forgotten your password, you can <a onClick={handleRecoverAccount}>recover your account</a>
|
In case you have forgotten your password, you can
|
||||||
here.
|
<a onClick={handleRecoverAccount}>recover your account here.</a>
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import env from '@beam-australia/react-env';
|
import env from '@beam-australia/react-env';
|
||||||
import { joiResolver } from '@hookform/resolvers/joi';
|
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 { Button, TextField } from '@mui/material';
|
||||||
|
import { CredentialResponse, GoogleLogin } from '@react-oauth/google';
|
||||||
import Joi from 'joi';
|
import Joi from 'joi';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
import { Trans, useTranslation } from 'next-i18next';
|
import { Trans, useTranslation } from 'next-i18next';
|
||||||
import { GoogleLoginResponse, GoogleLoginResponseOffline, useGoogleLogin } from 'react-google-login';
|
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
import { useMutation } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
|
|
||||||
import BaseModal from '@/components/shared/BaseModal';
|
import BaseModal from '@/components/shared/BaseModal';
|
||||||
@ -62,15 +64,6 @@ const RegisterModal: React.FC = () => {
|
|||||||
loginWithGoogle
|
loginWithGoogle
|
||||||
);
|
);
|
||||||
|
|
||||||
const { signIn } = useGoogleLogin({
|
|
||||||
clientId: env('GOOGLE_CLIENT_ID'),
|
|
||||||
onSuccess: async (response: GoogleLoginResponse | GoogleLoginResponseOffline) => {
|
|
||||||
await loginWithGoogleMutation({ accessToken: (response as GoogleLoginResponse).accessToken });
|
|
||||||
|
|
||||||
handleClose();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
dispatch(setModalState({ modal: 'auth.register', state: { open: false } }));
|
dispatch(setModalState({ modal: 'auth.register', state: { open: false } }));
|
||||||
reset();
|
reset();
|
||||||
@ -86,35 +79,37 @@ const RegisterModal: React.FC = () => {
|
|||||||
dispatch(setModalState({ modal: 'auth.login', state: { open: true } }));
|
dispatch(setModalState({ modal: 'auth.login', state: { open: true } }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoginWithGoogle = () => {
|
const handleLoginWithGoogle = async (response: CredentialResponse) => {
|
||||||
signIn();
|
if (response.credential) {
|
||||||
|
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleLoginWithGoogleError });
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoginWithGoogleError = () => {
|
||||||
|
toast("Please try logging in using email/password, or use another browser that supports Google's One Tap API.");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseModal
|
<BaseModal
|
||||||
icon={<HowToReg />}
|
icon={<HowToReg />}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
heading={t('modals.auth.register.heading')}
|
heading={t<string>('modals.auth.register.heading')}
|
||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
footerChildren={
|
footerChildren={
|
||||||
<>
|
<div className="flex gap-4">
|
||||||
<Button
|
{!isEmpty(env('GOOGLE_CLIENT_ID')) && (
|
||||||
type="submit"
|
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleLoginWithGoogleError} />
|
||||||
variant="outlined"
|
)}
|
||||||
disabled={isLoading}
|
|
||||||
startIcon={<Google />}
|
|
||||||
onClick={handleLoginWithGoogle}
|
|
||||||
>
|
|
||||||
{t('modals.auth.register.actions.google')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
|
||||||
{t('modals.auth.register.actions.register')}
|
{t<string>('modals.auth.register.actions.register')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<p>{t('modals.auth.register.body')}</p>
|
<p>{t<string>('modals.auth.register.body')}</p>
|
||||||
|
|
||||||
<form className="grid gap-4 md:grid-cols-2">
|
<form className="grid gap-4 md:grid-cols-2">
|
||||||
<Controller
|
<Controller
|
||||||
@ -123,7 +118,7 @@ const RegisterModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('modals.auth.register.form.name.label')}
|
label={t<string>('modals.auth.register.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -136,7 +131,7 @@ const RegisterModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('modals.auth.register.form.username.label')}
|
label={t<string>('modals.auth.register.form.username.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -150,7 +145,7 @@ const RegisterModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
type="email"
|
type="email"
|
||||||
label={t('modals.auth.register.form.email.label')}
|
label={t<string>('modals.auth.register.form.email.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
@ -165,7 +160,7 @@ const RegisterModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
type="password"
|
type="password"
|
||||||
label={t('modals.auth.register.form.password.label')}
|
label={t<string>('modals.auth.register.form.password.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -179,7 +174,7 @@ const RegisterModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
type="password"
|
type="password"
|
||||||
label={t('modals.auth.register.form.confirm-password.label')}
|
label={t<string>('modals.auth.register.form.confirm-password.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
|
|||||||
@ -65,15 +65,15 @@ const ResetPasswordModal: React.FC = () => {
|
|||||||
<BaseModal
|
<BaseModal
|
||||||
icon={<LockReset />}
|
icon={<LockReset />}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
heading={t('modals.auth.reset-password.heading')}
|
heading={t<string>('modals.auth.reset-password.heading')}
|
||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
footerChildren={
|
footerChildren={
|
||||||
<Button type="submit" disabled={isLoading} onClick={handleSubmit(onSubmit)}>
|
<Button type="submit" disabled={isLoading} onClick={handleSubmit(onSubmit)}>
|
||||||
{t('modals.auth.reset-password.actions.set-password')}
|
{t<string>('modals.auth.reset-password.actions.set-password')}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<p>{t('modals.auth.reset-password.body')}</p>
|
<p>{t<string>('modals.auth.reset-password.body')}</p>
|
||||||
|
|
||||||
<form className="grid gap-4 md:grid-cols-2">
|
<form className="grid gap-4 md:grid-cols-2">
|
||||||
<Controller
|
<Controller
|
||||||
@ -83,7 +83,7 @@ const ResetPasswordModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
type="password"
|
type="password"
|
||||||
label={t('modals.auth.reset-password.form.password.label')}
|
label={t<string>('modals.auth.reset-password.form.password.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -97,7 +97,7 @@ const ResetPasswordModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
type="password"
|
type="password"
|
||||||
label={t('modals.auth.reset-password.form.confirm-password.label')}
|
label={t<string>('modals.auth.reset-password.form.confirm-password.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { joiResolver } from '@hookform/resolvers/joi';
|
import { joiResolver } from '@hookform/resolvers/joi';
|
||||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||||
import DatePicker from '@mui/lab/DatePicker';
|
|
||||||
import { Button, TextField } from '@mui/material';
|
import { Button, TextField } from '@mui/material';
|
||||||
|
import { DatePicker } from '@mui/x-date-pickers';
|
||||||
import { Award, SectionPath } from '@reactive-resume/schema';
|
import { Award, SectionPath } from '@reactive-resume/schema';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import Joi from 'joi';
|
import Joi from 'joi';
|
||||||
@ -44,14 +44,14 @@ const AwardModal: React.FC = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
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 { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||||
|
|
||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -93,7 +93,7 @@ const AwardModal: React.FC = () => {
|
|||||||
heading={isEditMode ? editText : addText}
|
heading={isEditMode ? editText : addText}
|
||||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
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
|
<Controller
|
||||||
name="title"
|
name="title"
|
||||||
control={control}
|
control={control}
|
||||||
@ -101,7 +101,7 @@ const AwardModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.title.label')}
|
label={t<string>('builder.common.form.title.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -115,7 +115,7 @@ const AwardModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.leftSidebar.sections.awards.form.awarder.label')}
|
label={t<string>('builder.leftSidebar.sections.awards.form.awarder.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -130,11 +130,11 @@ const AwardModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.date.label')}
|
label={t<string>('builder.common.form.date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
@ -152,7 +152,7 @@ const AwardModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t<string>('builder.common.form.url.label')}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
@ -169,7 +169,7 @@ const AwardModal: React.FC = () => {
|
|||||||
multiline
|
multiline
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||||
@ -177,6 +177,7 @@ const AwardModal: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<input type="submit" style={{ display: 'none' }} />
|
||||||
</form>
|
</form>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { joiResolver } from '@hookform/resolvers/joi';
|
import { joiResolver } from '@hookform/resolvers/joi';
|
||||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||||
import DatePicker from '@mui/lab/DatePicker';
|
|
||||||
import { Button, TextField } from '@mui/material';
|
import { Button, TextField } from '@mui/material';
|
||||||
|
import { DatePicker } from '@mui/x-date-pickers';
|
||||||
import { Certificate, SectionPath } from '@reactive-resume/schema';
|
import { Certificate, SectionPath } from '@reactive-resume/schema';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import Joi from 'joi';
|
import Joi from 'joi';
|
||||||
@ -44,14 +44,14 @@ const CertificateModal: React.FC = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
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 { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
|
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -93,7 +93,7 @@ const CertificateModal: React.FC = () => {
|
|||||||
heading={isEditMode ? editText : addText}
|
heading={isEditMode ? editText : addText}
|
||||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
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
|
<Controller
|
||||||
name="name"
|
name="name"
|
||||||
control={control}
|
control={control}
|
||||||
@ -101,7 +101,7 @@ const CertificateModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.name.label')}
|
label={t<string>('builder.common.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -115,7 +115,7 @@ const CertificateModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.leftSidebar.sections.certifications.form.issuer.label')}
|
label={t<string>('builder.leftSidebar.sections.certifications.form.issuer.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -130,11 +130,11 @@ const CertificateModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.date.label')}
|
label={t<string>('builder.common.form.date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
@ -152,7 +152,7 @@ const CertificateModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t<string>('builder.common.form.url.label')}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
@ -169,7 +169,7 @@ const CertificateModal: React.FC = () => {
|
|||||||
multiline
|
multiline
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||||
@ -177,6 +177,7 @@ const CertificateModal: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<input type="submit" style={{ display: 'none' }} />
|
||||||
</form>
|
</form>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { joiResolver } from '@hookform/resolvers/joi';
|
import { joiResolver } from '@hookform/resolvers/joi';
|
||||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||||
import DatePicker from '@mui/lab/DatePicker';
|
|
||||||
import { Button, Slider, TextField } from '@mui/material';
|
import { Button, Slider, TextField } from '@mui/material';
|
||||||
|
import { DatePicker } from '@mui/x-date-pickers';
|
||||||
import { Custom } from '@reactive-resume/schema';
|
import { Custom } from '@reactive-resume/schema';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import Joi from 'joi';
|
import Joi from 'joi';
|
||||||
@ -60,15 +60,16 @@ const CustomModal: React.FC = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
|
||||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal['builder.sections.custom']);
|
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 item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
|
||||||
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -110,7 +111,7 @@ const CustomModal: React.FC = () => {
|
|||||||
heading={isEditMode ? editText : addText}
|
heading={isEditMode ? editText : addText}
|
||||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
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
|
<Controller
|
||||||
name="title"
|
name="title"
|
||||||
control={control}
|
control={control}
|
||||||
@ -118,7 +119,7 @@ const CustomModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.title.label')}
|
label={t<string>('builder.common.form.title.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -131,7 +132,7 @@ const CustomModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.subtitle.label')}
|
label={t<string>('builder.common.form.subtitle.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -146,11 +147,11 @@ const CustomModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.start-date.label')}
|
label={t<string>('builder.common.form.start-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
@ -170,11 +171,11 @@ const CustomModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.end-date.label')}
|
label={t<string>('builder.common.form.end-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
@ -192,7 +193,7 @@ const CustomModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t<string>('builder.common.form.url.label')}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
@ -207,7 +208,7 @@ const CustomModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.level.label')}
|
label={t<string>('builder.common.form.level.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
@ -221,7 +222,7 @@ const CustomModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<h4 className="mb-3 font-semibold">{t('builder.common.form.levelNum.label')}</h4>
|
<h4 className="mb-3 font-semibold">{t<string>('builder.common.form.levelNum.label')}</h4>
|
||||||
|
|
||||||
<div className="px-10">
|
<div className="px-10">
|
||||||
<Slider
|
<Slider
|
||||||
@ -245,7 +246,7 @@ const CustomModal: React.FC = () => {
|
|||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
valueLabelDisplay="auto"
|
valueLabelDisplay="auto"
|
||||||
aria-label={t('builder.common.form.levelNum.label')}
|
aria-label={t<string>('builder.common.form.levelNum.label')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -260,9 +261,9 @@ const CustomModal: React.FC = () => {
|
|||||||
multiline
|
multiline
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
label={t('builder.common.form.summary.label')}
|
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@ -274,7 +275,7 @@ const CustomModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<ArrayInput
|
<ArrayInput
|
||||||
label={t('builder.common.form.keywords.label')}
|
label={t<string>('builder.common.form.keywords.label')}
|
||||||
value={field.value as string[]}
|
value={field.value as string[]}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
errors={fieldState.error}
|
errors={fieldState.error}
|
||||||
@ -282,6 +283,7 @@ const CustomModal: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<input type="submit" style={{ display: 'none' }} />
|
||||||
</form>
|
</form>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { joiResolver } from '@hookform/resolvers/joi';
|
import { joiResolver } from '@hookform/resolvers/joi';
|
||||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||||
import DatePicker from '@mui/lab/DatePicker';
|
|
||||||
import { Button, TextField } from '@mui/material';
|
import { Button, TextField } from '@mui/material';
|
||||||
|
import { DatePicker } from '@mui/x-date-pickers';
|
||||||
import { Education, SectionPath } from '@reactive-resume/schema';
|
import { Education, SectionPath } from '@reactive-resume/schema';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import Joi from 'joi';
|
import Joi from 'joi';
|
||||||
@ -57,14 +57,14 @@ const EducationModal: React.FC = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
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 { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||||
|
|
||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -106,7 +106,7 @@ const EducationModal: React.FC = () => {
|
|||||||
heading={isEditMode ? editText : addText}
|
heading={isEditMode ? editText : addText}
|
||||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
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
|
<Controller
|
||||||
name="institution"
|
name="institution"
|
||||||
control={control}
|
control={control}
|
||||||
@ -114,7 +114,7 @@ const EducationModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.leftSidebar.sections.education.form.institution.label')}
|
label={t<string>('builder.leftSidebar.sections.education.form.institution.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -128,7 +128,7 @@ const EducationModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.leftSidebar.sections.education.form.degree.label')}
|
label={t<string>('builder.leftSidebar.sections.education.form.degree.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -141,7 +141,7 @@ const EducationModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.leftSidebar.sections.education.form.area-study.label')}
|
label={t<string>('builder.leftSidebar.sections.education.form.area-study.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -154,7 +154,7 @@ const EducationModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.leftSidebar.sections.education.form.grade.label')}
|
label={t<string>('builder.leftSidebar.sections.education.form.grade.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -169,11 +169,11 @@ const EducationModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.start-date.label')}
|
label={t<string>('builder.common.form.start-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
@ -193,17 +193,17 @@ const EducationModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.end-date.label')}
|
label={t<string>('builder.common.form.end-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || t('builder.common.form.end-date.help-text')}
|
helperText={fieldState.error?.message || t<string>('builder.common.form.end-date.help-text')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -215,7 +215,7 @@ const EducationModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t<string>('builder.common.form.url.label')}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
@ -233,7 +233,7 @@ const EducationModal: React.FC = () => {
|
|||||||
multiline
|
multiline
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||||
@ -247,7 +247,7 @@ const EducationModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<ArrayInput
|
<ArrayInput
|
||||||
label={t('builder.leftSidebar.sections.education.form.courses.label')}
|
label={t<string>('builder.leftSidebar.sections.education.form.courses.label')}
|
||||||
value={field.value as string[]}
|
value={field.value as string[]}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
errors={fieldState.error}
|
errors={fieldState.error}
|
||||||
@ -255,6 +255,7 @@ const EducationModal: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<input type="submit" style={{ display: 'none' }} />
|
||||||
</form>
|
</form>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -35,14 +35,14 @@ const InterestModal: React.FC = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
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 { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||||
|
|
||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -84,7 +84,7 @@ const InterestModal: React.FC = () => {
|
|||||||
heading={isEditMode ? editText : addText}
|
heading={isEditMode ? editText : addText}
|
||||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
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
|
<Controller
|
||||||
name="name"
|
name="name"
|
||||||
control={control}
|
control={control}
|
||||||
@ -92,7 +92,7 @@ const InterestModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.name.label')}
|
label={t<string>('builder.common.form.name.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
@ -106,7 +106,7 @@ const InterestModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<ArrayInput
|
<ArrayInput
|
||||||
label={t('builder.common.form.keywords.label')}
|
label={t<string>('builder.common.form.keywords.label')}
|
||||||
value={field.value as string[]}
|
value={field.value as string[]}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
errors={fieldState.error}
|
errors={fieldState.error}
|
||||||
@ -114,6 +114,7 @@ const InterestModal: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<input type="submit" style={{ display: 'none' }} />
|
||||||
</form>
|
</form>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -36,14 +36,14 @@ const LanguageModal: React.FC = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
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 { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||||
|
|
||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -85,7 +85,7 @@ const LanguageModal: React.FC = () => {
|
|||||||
heading={isEditMode ? editText : addText}
|
heading={isEditMode ? editText : addText}
|
||||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
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
|
<Controller
|
||||||
name="name"
|
name="name"
|
||||||
control={control}
|
control={control}
|
||||||
@ -93,7 +93,7 @@ const LanguageModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.name.label')}
|
label={t<string>('builder.common.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -107,7 +107,7 @@ const LanguageModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.common.form.level.label')}
|
label={t<string>('builder.common.form.level.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -120,7 +120,7 @@ const LanguageModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<h4 className="mb-3 font-semibold">{t('builder.common.form.levelNum.label')}</h4>
|
<h4 className="mb-3 font-semibold">{t<string>('builder.common.form.levelNum.label')}</h4>
|
||||||
|
|
||||||
<div className="px-10">
|
<div className="px-10">
|
||||||
<Slider
|
<Slider
|
||||||
@ -144,12 +144,13 @@ const LanguageModal: React.FC = () => {
|
|||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
valueLabelDisplay="auto"
|
valueLabelDisplay="auto"
|
||||||
aria-label={t('builder.common.form.levelNum.label')}
|
aria-label={t<string>('builder.common.form.levelNum.label')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<input type="submit" style={{ display: 'none' }} />
|
||||||
</form>
|
</form>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -42,11 +42,11 @@ const ProfileModal: React.FC = () => {
|
|||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = t('builder.common.actions.add', {
|
const addText = t<string>('builder.common.actions.add', {
|
||||||
token: t('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
token: t<string>('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
||||||
});
|
});
|
||||||
const editText = t('builder.common.actions.edit', {
|
const editText = t<string>('builder.common.actions.edit', {
|
||||||
token: t('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
token: t<string>('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
@ -89,7 +89,7 @@ const ProfileModal: React.FC = () => {
|
|||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
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
|
<Controller
|
||||||
name="network"
|
name="network"
|
||||||
control={control}
|
control={control}
|
||||||
@ -97,7 +97,7 @@ const ProfileModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.leftSidebar.sections.profiles.form.network.label')}
|
label={t<string>('builder.leftSidebar.sections.profiles.form.network.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -111,7 +111,7 @@ const ProfileModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.leftSidebar.sections.profiles.form.username.label')}
|
label={t<string>('builder.leftSidebar.sections.profiles.form.username.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@ -127,7 +127,7 @@ const ProfileModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t<string>('builder.common.form.url.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
@ -136,6 +136,7 @@ const ProfileModal: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<input type="submit" style={{ display: 'none' }} />
|
||||||
</form>
|
</form>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { joiResolver } from '@hookform/resolvers/joi';
|
import { joiResolver } from '@hookform/resolvers/joi';
|
||||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||||
import DatePicker from '@mui/lab/DatePicker';
|
|
||||||
import { Button, TextField } from '@mui/material';
|
import { Button, TextField } from '@mui/material';
|
||||||
|
import { DatePicker } from '@mui/x-date-pickers';
|
||||||
import { Project, SectionPath } from '@reactive-resume/schema';
|
import { Project, SectionPath } from '@reactive-resume/schema';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import Joi from 'joi';
|
import Joi from 'joi';
|
||||||
@ -53,14 +53,14 @@ const ProjectModal: React.FC = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
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 { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||||
|
|
||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -102,7 +102,7 @@ const ProjectModal: React.FC = () => {
|
|||||||
heading={isEditMode ? editText : addText}
|
heading={isEditMode ? editText : addText}
|
||||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
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
|
<Controller
|
||||||
name="name"
|
name="name"
|
||||||
control={control}
|
control={control}
|
||||||
@ -110,7 +110,7 @@ const ProjectModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.name.label')}
|
label={t<string>('builder.common.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -124,7 +124,7 @@ const ProjectModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.common.form.description.label')}
|
label={t<string>('builder.common.form.description.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -139,11 +139,11 @@ const ProjectModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.start-date.label')}
|
label={t<string>('builder.common.form.start-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
@ -163,11 +163,11 @@ const ProjectModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.end-date.label')}
|
label={t<string>('builder.common.form.end-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
@ -185,7 +185,7 @@ const ProjectModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t<string>('builder.common.form.url.label')}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
@ -203,7 +203,7 @@ const ProjectModal: React.FC = () => {
|
|||||||
multiline
|
multiline
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||||
@ -217,7 +217,7 @@ const ProjectModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<ArrayInput
|
<ArrayInput
|
||||||
label={t('builder.common.form.keywords.label')}
|
label={t<string>('builder.common.form.keywords.label')}
|
||||||
value={field.value as string[]}
|
value={field.value as string[]}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
errors={fieldState.error}
|
errors={fieldState.error}
|
||||||
@ -225,6 +225,7 @@ const ProjectModal: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<input type="submit" style={{ display: 'none' }} />
|
||||||
</form>
|
</form>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { joiResolver } from '@hookform/resolvers/joi';
|
import { joiResolver } from '@hookform/resolvers/joi';
|
||||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||||
import DatePicker from '@mui/lab/DatePicker';
|
|
||||||
import { Button, TextField } from '@mui/material';
|
import { Button, TextField } from '@mui/material';
|
||||||
|
import { DatePicker } from '@mui/x-date-pickers';
|
||||||
import { Publication, SectionPath } from '@reactive-resume/schema';
|
import { Publication, SectionPath } from '@reactive-resume/schema';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import Joi from 'joi';
|
import Joi from 'joi';
|
||||||
@ -44,14 +44,14 @@ const PublicationModal: React.FC = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
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 { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||||
|
|
||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -93,7 +93,7 @@ const PublicationModal: React.FC = () => {
|
|||||||
heading={isEditMode ? editText : addText}
|
heading={isEditMode ? editText : addText}
|
||||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
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
|
<Controller
|
||||||
name="name"
|
name="name"
|
||||||
control={control}
|
control={control}
|
||||||
@ -101,7 +101,7 @@ const PublicationModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.name.label')}
|
label={t<string>('builder.common.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -115,7 +115,7 @@ const PublicationModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label="{t('builder.leftSidebar.sections.publications.form.publisher.label')}"
|
label={t<string>('builder.leftSidebar.sections.publications.form.publisher.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -130,11 +130,11 @@ const PublicationModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.date.label')}
|
label={t<string>('builder.common.form.date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
@ -152,7 +152,7 @@ const PublicationModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t<string>('builder.common.form.url.label')}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
@ -169,7 +169,7 @@ const PublicationModal: React.FC = () => {
|
|||||||
multiline
|
multiline
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||||
@ -177,6 +177,7 @@ const PublicationModal: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<input type="submit" style={{ display: 'none' }} />
|
||||||
</form>
|
</form>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -41,14 +41,14 @@ const ReferenceModal: React.FC = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
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 { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||||
|
|
||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -90,7 +90,7 @@ const ReferenceModal: React.FC = () => {
|
|||||||
heading={isEditMode ? editText : addText}
|
heading={isEditMode ? editText : addText}
|
||||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
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
|
<Controller
|
||||||
name="name"
|
name="name"
|
||||||
control={control}
|
control={control}
|
||||||
@ -98,7 +98,7 @@ const ReferenceModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.name.label')}
|
label={t<string>('builder.common.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -112,7 +112,7 @@ const ReferenceModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.leftSidebar.sections.references.form.relationship.label')}
|
label={t<string>('builder.leftSidebar.sections.references.form.relationship.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -125,7 +125,7 @@ const ReferenceModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.phone.label')}
|
label={t<string>('builder.common.form.phone.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -138,7 +138,7 @@ const ReferenceModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.email.label')}
|
label={t<string>('builder.common.form.email.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -154,7 +154,7 @@ const ReferenceModal: React.FC = () => {
|
|||||||
multiline
|
multiline
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||||
@ -162,6 +162,7 @@ const ReferenceModal: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<input type="submit" style={{ display: 'none' }} />
|
||||||
</form>
|
</form>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -39,14 +39,14 @@ const SkillModal: React.FC = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
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 { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||||
|
|
||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -88,7 +88,7 @@ const SkillModal: React.FC = () => {
|
|||||||
heading={isEditMode ? editText : addText}
|
heading={isEditMode ? editText : addText}
|
||||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
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
|
<Controller
|
||||||
name="name"
|
name="name"
|
||||||
control={control}
|
control={control}
|
||||||
@ -96,7 +96,7 @@ const SkillModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.name.label')}
|
label={t<string>('builder.common.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -109,7 +109,7 @@ const SkillModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.level.label')}
|
label={t<string>('builder.common.form.level.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -122,7 +122,7 @@ const SkillModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<h4 className="mb-3 font-semibold">{t('builder.common.form.levelNum.label')}</h4>
|
<h4 className="mb-3 font-semibold">{t<string>('builder.common.form.levelNum.label')}</h4>
|
||||||
|
|
||||||
<div className="px-3">
|
<div className="px-3">
|
||||||
<Slider
|
<Slider
|
||||||
@ -146,7 +146,7 @@ const SkillModal: React.FC = () => {
|
|||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
valueLabelDisplay="auto"
|
valueLabelDisplay="auto"
|
||||||
aria-label={t('builder.common.form.levelNum.label')}
|
aria-label={t<string>('builder.common.form.levelNum.label')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -158,14 +158,16 @@ const SkillModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<ArrayInput
|
<ArrayInput
|
||||||
label={t('builder.common.form.keywords.label')}
|
label={t<string>('builder.common.form.keywords.label')}
|
||||||
value={field.value as string[]}
|
value={field.value}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
errors={fieldState.error}
|
errors={fieldState.error}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<input type="submit" style={{ display: 'none' }} />
|
||||||
</form>
|
</form>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { joiResolver } from '@hookform/resolvers/joi';
|
import { joiResolver } from '@hookform/resolvers/joi';
|
||||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||||
import DatePicker from '@mui/lab/DatePicker';
|
|
||||||
import { Button, TextField } from '@mui/material';
|
import { Button, TextField } from '@mui/material';
|
||||||
|
import { DatePicker } from '@mui/x-date-pickers';
|
||||||
import { SectionPath, Volunteer } from '@reactive-resume/schema';
|
import { SectionPath, Volunteer } from '@reactive-resume/schema';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import Joi from 'joi';
|
import Joi from 'joi';
|
||||||
@ -50,14 +50,14 @@ const VolunteerModal: React.FC = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
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 { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
||||||
|
|
||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -99,7 +99,7 @@ const VolunteerModal: React.FC = () => {
|
|||||||
heading={isEditMode ? editText : addText}
|
heading={isEditMode ? editText : addText}
|
||||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
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
|
<Controller
|
||||||
name="organization"
|
name="organization"
|
||||||
control={control}
|
control={control}
|
||||||
@ -107,7 +107,7 @@ const VolunteerModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.leftSidebar.sections.volunteer.form.organization.label')}
|
label={t<string>('builder.leftSidebar.sections.volunteer.form.organization.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -121,7 +121,7 @@ const VolunteerModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.common.form.position.label')}
|
label={t<string>('builder.common.form.position.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -136,11 +136,11 @@ const VolunteerModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.start-date.label')}
|
label={t<string>('builder.common.form.start-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
@ -160,11 +160,11 @@ const VolunteerModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.end-date.label')}
|
label={t<string>('builder.common.form.end-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
@ -182,7 +182,7 @@ const VolunteerModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t<string>('builder.common.form.url.label')}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
@ -200,7 +200,7 @@ const VolunteerModal: React.FC = () => {
|
|||||||
multiline
|
multiline
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||||
@ -208,6 +208,7 @@ const VolunteerModal: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<input type="submit" style={{ display: 'none' }} />
|
||||||
</form>
|
</form>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { joiResolver } from '@hookform/resolvers/joi';
|
import { joiResolver } from '@hookform/resolvers/joi';
|
||||||
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
import { Add, DriveFileRenameOutline } from '@mui/icons-material';
|
||||||
import DatePicker from '@mui/lab/DatePicker';
|
|
||||||
import { Button, TextField } from '@mui/material';
|
import { Button, TextField } from '@mui/material';
|
||||||
import { SectionPath, WorkExperience } from '@reactive-resume/schema';
|
import { DatePicker } from '@mui/x-date-pickers';
|
||||||
|
import { WorkExperience } from '@reactive-resume/schema';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import Joi from 'joi';
|
import Joi from 'joi';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
@ -20,8 +20,6 @@ import { addItem, editItem } from '@/store/resume/resumeSlice';
|
|||||||
|
|
||||||
type FormData = WorkExperience;
|
type FormData = WorkExperience;
|
||||||
|
|
||||||
const path: SectionPath = 'sections.work';
|
|
||||||
|
|
||||||
const defaultState: FormData = {
|
const defaultState: FormData = {
|
||||||
name: '',
|
name: '',
|
||||||
position: '',
|
position: '',
|
||||||
@ -50,14 +48,16 @@ const WorkModal: React.FC = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const heading = useAppSelector((state) => get(state.resume, `${path}.name`));
|
const { open: isOpen, payload } = useAppSelector((state) => state.modal['builder.sections.work']);
|
||||||
const { open: isOpen, payload } = useAppSelector((state) => state.modal[`builder.${path}`]);
|
const path: string = get(payload, 'path', 'sections.work');
|
||||||
|
|
||||||
const item: FormData = get(payload, 'item', null);
|
const item: FormData = get(payload, 'item', null);
|
||||||
|
|
||||||
|
const heading = useAppSelector((state) => get(state.resume.present, `${path}.name`));
|
||||||
|
|
||||||
const isEditMode = useMemo(() => !!item, [item]);
|
const isEditMode = useMemo(() => !!item, [item]);
|
||||||
|
|
||||||
const addText = useMemo(() => t('builder.common.actions.add', { token: heading }), [t, heading]);
|
const addText = useMemo(() => t<string>('builder.common.actions.add', { token: heading }), [t, heading]);
|
||||||
const editText = useMemo(() => t('builder.common.actions.edit', { token: heading }), [t, heading]);
|
const editText = useMemo(() => t<string>('builder.common.actions.edit', { token: heading }), [t, heading]);
|
||||||
|
|
||||||
const { reset, control, handleSubmit } = useForm<FormData>({
|
const { reset, control, handleSubmit } = useForm<FormData>({
|
||||||
defaultValues: defaultState,
|
defaultValues: defaultState,
|
||||||
@ -77,7 +77,7 @@ const WorkModal: React.FC = () => {
|
|||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
dispatch(
|
dispatch(
|
||||||
setModalState({
|
setModalState({
|
||||||
modal: `builder.${path}`,
|
modal: 'builder.sections.work',
|
||||||
state: { open: false },
|
state: { open: false },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -99,7 +99,7 @@ const WorkModal: React.FC = () => {
|
|||||||
heading={isEditMode ? editText : addText}
|
heading={isEditMode ? editText : addText}
|
||||||
footerChildren={<Button onClick={handleSubmit(onSubmit)}>{isEditMode ? editText : addText}</Button>}
|
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
|
<Controller
|
||||||
name="name"
|
name="name"
|
||||||
control={control}
|
control={control}
|
||||||
@ -107,7 +107,7 @@ const WorkModal: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('builder.common.form.name.label')}
|
label={t<string>('builder.common.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -121,7 +121,7 @@ const WorkModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t('builder.common.form.position.label')}
|
label={t<string>('builder.common.form.position.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -136,11 +136,11 @@ const WorkModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.start-date.label')}
|
label={t<string>('builder.common.form.start-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
@ -160,17 +160,17 @@ const WorkModal: React.FC = () => {
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
{...field}
|
{...field}
|
||||||
openTo="year"
|
openTo="year"
|
||||||
label={t('builder.common.form.end-date.label')}
|
label={t<string>('builder.common.form.end-date.label')}
|
||||||
views={['year', 'month', 'day']}
|
views={['year', 'month', 'day']}
|
||||||
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
onChange={(date: Date | null, keyboardInputValue: string | undefined) => {
|
||||||
isEmpty(keyboardInputValue) && field.onChange('');
|
isEmpty(keyboardInputValue) && field.onChange('');
|
||||||
date && dayjs(date).isValid() && field.onChange(date.toISOString());
|
date && dayjs(date).utc().isValid() && field.onChange(dayjs(date).utc().toISOString());
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || t('builder.common.form.end-date.help-text')}
|
helperText={fieldState.error?.message || t<string>('builder.common.form.end-date.help-text')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -182,7 +182,7 @@ const WorkModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('builder.common.form.url.label')}
|
label={t<string>('builder.common.form.url.label')}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
@ -200,7 +200,7 @@ const WorkModal: React.FC = () => {
|
|||||||
multiline
|
multiline
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
label={t('builder.common.form.summary.label')}
|
label={t<string>('builder.common.form.summary.label')}
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message || <MarkdownSupported />}
|
helperText={fieldState.error?.message || <MarkdownSupported />}
|
||||||
@ -208,6 +208,7 @@ const WorkModal: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<input type="submit" style={{ display: 'none' }} />
|
||||||
</form>
|
</form>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -58,7 +58,7 @@ const CreateResumeModal: React.FC = () => {
|
|||||||
const slug = name
|
const slug = name
|
||||||
? name
|
? name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[`~!@#$%^&*()_|+=?;:'",.<>{}[]\\\/]/gi, '')
|
.replace(/[^\w\s]/gi, '')
|
||||||
.replace(/[ ]/gi, '-')
|
.replace(/[ ]/gi, '-')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
@ -69,7 +69,8 @@ const CreateResumeModal: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
await mutateAsync({ name, slug, public: isPublic });
|
await mutateAsync({ name, slug, public: isPublic });
|
||||||
|
|
||||||
queryClient.invalidateQueries(RESUMES_QUERY);
|
await queryClient.invalidateQueries(RESUMES_QUERY);
|
||||||
|
|
||||||
handleClose();
|
handleClose();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
@ -85,15 +86,15 @@ const CreateResumeModal: React.FC = () => {
|
|||||||
<BaseModal
|
<BaseModal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
icon={<Add />}
|
icon={<Add />}
|
||||||
heading={t('modals.dashboard.create-resume.heading')}
|
heading={t<string>('modals.dashboard.create-resume.heading')}
|
||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
footerChildren={
|
footerChildren={
|
||||||
<Button type="submit" disabled={isLoading} onClick={handleSubmit(onSubmit)}>
|
<Button type="submit" disabled={isLoading} onClick={handleSubmit(onSubmit)}>
|
||||||
{t('modals.dashboard.create-resume.actions.create-resume')}
|
{t<string>('modals.dashboard.create-resume.actions.create-resume')}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<p>{t('modals.dashboard.create-resume.body')}</p>
|
<p>{t<string>('modals.dashboard.create-resume.body')}</p>
|
||||||
|
|
||||||
<form className="grid gap-4">
|
<form className="grid gap-4">
|
||||||
<Controller
|
<Controller
|
||||||
@ -102,7 +103,7 @@ const CreateResumeModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('modals.dashboard.create-resume.form.name.label')}
|
label={t<string>('modals.dashboard.create-resume.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -115,7 +116,7 @@ const CreateResumeModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('modals.dashboard.create-resume.form.slug.label')}
|
label={t<string>('modals.dashboard.create-resume.form.slug.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -125,7 +126,7 @@ const CreateResumeModal: React.FC = () => {
|
|||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label={t('modals.dashboard.create-resume.form.public.label') as string}
|
label={t<string>('modals.dashboard.create-resume.form.public.label')}
|
||||||
control={
|
control={
|
||||||
<Controller
|
<Controller
|
||||||
name="isPublic"
|
name="isPublic"
|
||||||
|
|||||||
@ -63,7 +63,7 @@ const ImportExternalModal: React.FC = () => {
|
|||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
|
|
||||||
if (file.size > FILE_UPLOAD_MAX_SIZE) {
|
if (file.size > FILE_UPLOAD_MAX_SIZE) {
|
||||||
toast.error(t('common.toast.error.upload-file-size'));
|
toast.error(t<string>('common.toast.error.upload-file-size'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,13 +78,13 @@ const ImportExternalModal: React.FC = () => {
|
|||||||
<BaseModal
|
<BaseModal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
icon={<ImportExport />}
|
icon={<ImportExport />}
|
||||||
heading={t('modals.dashboard.import-external.heading')}
|
heading={t<string>('modals.dashboard.import-external.heading')}
|
||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
>
|
>
|
||||||
<div className="grid gap-5">
|
<div className="grid gap-5">
|
||||||
<h2 className="inline-flex items-center gap-2 text-lg font-medium">
|
<h2 className="inline-flex items-center gap-2 text-lg font-medium">
|
||||||
<LinkedIn />
|
<LinkedIn />
|
||||||
{t('modals.dashboard.import-external.linkedin.heading')}
|
{t<string>('modals.dashboard.import-external.linkedin.heading')}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className="mb-2">
|
<p className="mb-2">
|
||||||
@ -110,7 +110,7 @@ const ImportExternalModal: React.FC = () => {
|
|||||||
startIcon={<UploadFile />}
|
startIcon={<UploadFile />}
|
||||||
onClick={() => handleClick('linkedin')}
|
onClick={() => handleClick('linkedin')}
|
||||||
>
|
>
|
||||||
{t('modals.dashboard.import-external.linkedin.actions.upload-archive')}
|
{t<string>('modals.dashboard.import-external.linkedin.actions.upload-archive')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@ -128,7 +128,7 @@ const ImportExternalModal: React.FC = () => {
|
|||||||
<div className="grid gap-5">
|
<div className="grid gap-5">
|
||||||
<h2 className="inline-flex items-center gap-2 text-lg font-medium">
|
<h2 className="inline-flex items-center gap-2 text-lg font-medium">
|
||||||
<Code />
|
<Code />
|
||||||
{t('modals.dashboard.import-external.json-resume.heading')}
|
{t<string>('modals.dashboard.import-external.json-resume.heading')}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className="mb-2">
|
<p className="mb-2">
|
||||||
@ -154,7 +154,7 @@ const ImportExternalModal: React.FC = () => {
|
|||||||
startIcon={<UploadFile />}
|
startIcon={<UploadFile />}
|
||||||
onClick={() => handleClick('json-resume')}
|
onClick={() => handleClick('json-resume')}
|
||||||
>
|
>
|
||||||
{t('modals.dashboard.import-external.json-resume.actions.upload-json')}
|
{t<string>('modals.dashboard.import-external.json-resume.actions.upload-json')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@ -172,10 +172,10 @@ const ImportExternalModal: React.FC = () => {
|
|||||||
<div className="grid gap-5">
|
<div className="grid gap-5">
|
||||||
<h2 className="inline-flex items-center gap-2 text-lg font-medium">
|
<h2 className="inline-flex items-center gap-2 text-lg font-medium">
|
||||||
<TrackChanges />
|
<TrackChanges />
|
||||||
{t('modals.dashboard.import-external.reactive-resume.heading')}
|
{t<string>('modals.dashboard.import-external.reactive-resume.heading')}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className="mb-2">{t('modals.dashboard.import-external.reactive-resume.body')}</p>
|
<p className="mb-2">{t<string>('modals.dashboard.import-external.reactive-resume.body')}</p>
|
||||||
|
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<Button
|
<Button
|
||||||
@ -184,7 +184,7 @@ const ImportExternalModal: React.FC = () => {
|
|||||||
startIcon={<UploadFile />}
|
startIcon={<UploadFile />}
|
||||||
onClick={() => handleClick('reactive-resume')}
|
onClick={() => handleClick('reactive-resume')}
|
||||||
>
|
>
|
||||||
{t('modals.dashboard.import-external.reactive-resume.actions.upload-json')}
|
{t<string>('modals.dashboard.import-external.reactive-resume.actions.upload-json')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -193,7 +193,7 @@ const ImportExternalModal: React.FC = () => {
|
|||||||
startIcon={<UploadFile />}
|
startIcon={<UploadFile />}
|
||||||
onClick={() => handleClick('reactive-resume-v2')}
|
onClick={() => handleClick('reactive-resume-v2')}
|
||||||
>
|
>
|
||||||
{t('modals.dashboard.import-external.reactive-resume.actions.upload-json-v2')}
|
{t<string>('modals.dashboard.import-external.reactive-resume.actions.upload-json-v2')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
|||||||
@ -56,7 +56,7 @@ const RenameResumeModal: React.FC = () => {
|
|||||||
const slug = name
|
const slug = name
|
||||||
? name
|
? name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[`~!@#$%^&*()_|+=?;:'",.<>{}[]\\\/]/gi, '')
|
.replace(/[^\w\s]/gi, '')
|
||||||
.replace(/[ ]/gi, '-')
|
.replace(/[ ]/gi, '-')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
@ -92,11 +92,11 @@ const RenameResumeModal: React.FC = () => {
|
|||||||
<BaseModal
|
<BaseModal
|
||||||
icon={<DriveFileRenameOutline />}
|
icon={<DriveFileRenameOutline />}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
heading={t('modals.dashboard.rename-resume.heading')}
|
heading={t<string>('modals.dashboard.rename-resume.heading')}
|
||||||
handleClose={handleClose}
|
handleClose={handleClose}
|
||||||
footerChildren={
|
footerChildren={
|
||||||
<Button type="submit" disabled={isLoading} onClick={handleSubmit(onSubmit)}>
|
<Button type="submit" disabled={isLoading} onClick={handleSubmit(onSubmit)}>
|
||||||
{t('modals.dashboard.rename-resume.actions.rename-resume')}
|
{t<string>('modals.dashboard.rename-resume.actions.rename-resume')}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -107,7 +107,7 @@ const RenameResumeModal: React.FC = () => {
|
|||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
label={t('modals.dashboard.rename-resume.form.name.label')}
|
label={t<string>('modals.dashboard.rename-resume.form.name.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
@ -120,7 +120,7 @@ const RenameResumeModal: React.FC = () => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label={t('modals.dashboard.rename-resume.form.slug.label')}
|
label={t<string>('modals.dashboard.rename-resume.form.slug.label')}
|
||||||
error={!!fieldState.error}
|
error={!!fieldState.error}
|
||||||
helperText={fieldState.error?.message}
|
helperText={fieldState.error?.message}
|
||||||
{...field}
|
{...field}
|
||||||
|
|||||||
@ -3,7 +3,48 @@ const path = require('path');
|
|||||||
const i18nConfig = {
|
const i18nConfig = {
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: 'en',
|
defaultLocale: 'en',
|
||||||
locales: ['bn', 'da', 'de', 'en', 'es', 'fr', 'hi', 'it', 'kn', 'pl', 'ta', 'tr', 'zh'],
|
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',
|
||||||
|
'ro',
|
||||||
|
'ru',
|
||||||
|
'sr',
|
||||||
|
'sv',
|
||||||
|
'ta',
|
||||||
|
'tr',
|
||||||
|
'uk',
|
||||||
|
'vi',
|
||||||
|
'zh',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
nsSeparator: '.',
|
nsSeparator: '.',
|
||||||
localePath: path.resolve('./public/locales'),
|
localePath: path.resolve('./public/locales'),
|
||||||
|
|||||||
@ -12,20 +12,7 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
images: {
|
images: {
|
||||||
domains: ['www.gravatar.com'],
|
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
|
// Hack to make Tailwind darkMode 'class' strategy with CSS Modules
|
||||||
|
|||||||
@ -2,77 +2,82 @@
|
|||||||
"name": "@reactive-resume/client",
|
"name": "@reactive-resume/client",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "react-env --prefix PUBLIC -- next dev",
|
"dev": "react-env --prefix PUBLIC -- next dev",
|
||||||
|
"lint": "next lint --fix",
|
||||||
"build": "next build && npm run sitemap",
|
"build": "next build && npm run sitemap",
|
||||||
"start": "react-env --prefix PUBLIC -- next start",
|
"start": "react-env --prefix PUBLIC -- next start",
|
||||||
"lint": "next lint --fix",
|
|
||||||
"sitemap": "next-sitemap --config next-sitemap.config.js"
|
"sitemap": "next-sitemap --config next-sitemap.config.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@beam-australia/react-env": "^3.1.1",
|
"@beam-australia/react-env": "^3.1.1",
|
||||||
"@emotion/css": "^11.7.1",
|
"@date-io/dayjs": "^2.16.0",
|
||||||
"@emotion/react": "^11.8.2",
|
"@emotion/css": "^11.10.5",
|
||||||
"@emotion/styled": "^11.8.1",
|
"@emotion/react": "^11.10.5",
|
||||||
"@hookform/resolvers": "2.8.8",
|
"@emotion/styled": "^11.10.5",
|
||||||
"@monaco-editor/react": "^4.3.1",
|
"@hello-pangea/dnd": "^16.2.0",
|
||||||
"@mui/icons-material": "^5.5.1",
|
"@hookform/resolvers": "2.9.10",
|
||||||
"@mui/lab": "^5.0.0-alpha.73",
|
"@monaco-editor/react": "^4.4.6",
|
||||||
"@mui/material": "^5.5.1",
|
"@mui/icons-material": "^5.11.0",
|
||||||
"@reduxjs/toolkit": "^1.8.0",
|
"@mui/lab": "^5.0.0-alpha.112",
|
||||||
"axios": "^0.26.1",
|
"@mui/material": "^5.11.0",
|
||||||
"clsx": "^1.1.1",
|
"@mui/system": "^5.11.0",
|
||||||
"dayjs": "^1.11.0",
|
"@mui/x-date-pickers": "5.0.11",
|
||||||
|
"@next/env": "^13.0.7",
|
||||||
|
"@react-oauth/google": "^0.5.1",
|
||||||
|
"@reduxjs/toolkit": "^1.9.1",
|
||||||
|
"axios": "^1.2.1",
|
||||||
|
"clsx": "^1.2.1",
|
||||||
|
"dayjs": "^1.11.7",
|
||||||
"downloadjs": "^1.4.7",
|
"downloadjs": "^1.4.7",
|
||||||
"joi": "^17.6.0",
|
"joi": "^17.7.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"md5-hex": "^4.0.0",
|
"md5-hex": "^4.0.0",
|
||||||
"monaco-editor": "^0.33.0",
|
"monaco-editor": "^0.34.1",
|
||||||
"nanoid": "^3.3.1",
|
"nanoid": "^3.3.4",
|
||||||
"next": "12.1.0",
|
"next": "13.0.7",
|
||||||
"next-i18next": "^10.5.0",
|
"next-i18next": "^13.0.2",
|
||||||
"react": ">=17",
|
"react": "^18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-colorful": "^5.6.1",
|
||||||
"react-colorful": "^5.5.1",
|
"react-dnd": "16.0.1",
|
||||||
"react-dnd": "^15.1.1",
|
"react-dnd-html5-backend": "16.0.1",
|
||||||
"react-dnd-html5-backend": "^15.1.2",
|
"react-dom": "^18.2.0",
|
||||||
"react-dom": ">=17",
|
"react-hook-form": "^7.40.0",
|
||||||
"react-google-login": "^5.2.2",
|
"react-hot-toast": "2.4.0",
|
||||||
"react-hook-form": "^7.28.0",
|
"react-icons": "^4.7.1",
|
||||||
"react-hot-toast": "2.2.0",
|
"react-markdown": "^8.0.4",
|
||||||
"react-hotkeys-hook": "^3.4.4",
|
"react-query": "^3.39.2",
|
||||||
"react-icons": "^4.3.1",
|
"react-redux": "^8.0.5",
|
||||||
"react-markdown": "^8.0.1",
|
|
||||||
"react-query": "^3.34.16",
|
|
||||||
"react-redux": "^7.2.6",
|
|
||||||
"react-zoom-pan-pinch": "^2.1.3",
|
"react-zoom-pan-pinch": "^2.1.3",
|
||||||
"redux": "^4.1.2",
|
"redux": "^4.2.0",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"redux-saga": "^1.1.3",
|
"redux-saga": "^1.2.2",
|
||||||
|
"redux-undo": "^1.0.1",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"sharp": "^0.30.3",
|
"sharp": "^0.31.2",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^9.0.0",
|
||||||
"webfontloader": "^1.6.28"
|
"webfontloader": "^1.6.28"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.17.7",
|
"@babel/core": "^7.20.5",
|
||||||
"@reactive-resume/schema": "workspace:*",
|
"@reactive-resume/schema": "workspace:*",
|
||||||
"@tailwindcss/typography": "^0.5.2",
|
"@tailwindcss/typography": "^0.5.8",
|
||||||
"@types/downloadjs": "^1.4.3",
|
"@types/downloadjs": "^1.4.3",
|
||||||
"@types/lodash": "^4.14.180",
|
"@types/lodash": "^4.14.191",
|
||||||
"@types/node": "17.0.21",
|
"@types/node": "^18.11.15",
|
||||||
"@types/react": "17.0.40",
|
"@types/react": "^18.0.26",
|
||||||
"@types/react-beautiful-dnd": "^13.1.2",
|
"@types/react-dom": "^18.0.9",
|
||||||
"@types/react-redux": "^7.1.23",
|
"@types/react-redux": "^7.1.24",
|
||||||
"@types/tailwindcss": "^3.0.9",
|
"@types/tailwindcss": "^3.0.11",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^9.0.0",
|
||||||
"@types/webfontloader": "^1.6.34",
|
"@types/webfontloader": "^1.6.35",
|
||||||
"autoprefixer": "^10.4.3",
|
"autoprefixer": "^10.4.13",
|
||||||
"eslint": "^8.11.0",
|
"csstype": "^3.1.1",
|
||||||
"eslint-config-next": "12.1.0",
|
"eslint-config-next": "^13.0.7",
|
||||||
"next-sitemap": "^2.5.10",
|
"eslint-plugin-tailwindcss": "^3.7.1",
|
||||||
"postcss": "^8.4.11",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"prettier": "^2.6.0",
|
"next-sitemap": "^3.1.42",
|
||||||
"sass": "^1.49.9",
|
"postcss": "^8.4.20",
|
||||||
"tailwindcss": "^3.0.23",
|
"sass": "^1.56.2",
|
||||||
"typescript": "<4.6.0"
|
"tailwindcss": "^3.2.4",
|
||||||
|
"typescript": "^4.9.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,7 +62,7 @@ const Build: NextPage<Props> = ({ username, slug }) => {
|
|||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
{resume.name} | {t('common.title')}
|
{resume.name} | {t<string>('common.title')}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Download, Downloading } from '@mui/icons-material';
|
|||||||
import { ButtonBase } from '@mui/material';
|
import { ButtonBase } from '@mui/material';
|
||||||
import { Resume } from '@reactive-resume/schema';
|
import { Resume } from '@reactive-resume/schema';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import download from 'downloadjs';
|
import download from 'downloadjs';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
@ -51,7 +52,7 @@ const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const resume = useAppSelector((state) => state.resume);
|
const resume = useAppSelector((state) => state.resume.present);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialData && !isEmpty(initialData)) {
|
if (initialData && !isEmpty(initialData)) {
|
||||||
@ -59,6 +60,16 @@ const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
|
|||||||
}
|
}
|
||||||
}, [dispatch, 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 }), {
|
useQuery<Resume>(`resume/${username}/${slug}`, () => fetchResumeByIdentifier({ username, slug }), {
|
||||||
initialData,
|
initialData,
|
||||||
retry: false,
|
retry: false,
|
||||||
@ -88,9 +99,11 @@ const Preview: NextPage<Props> = ({ username, slug, resume: initialData }) => {
|
|||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
try {
|
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 {
|
} catch {
|
||||||
toast.error('Something went wrong, please try again later.');
|
toast.error('Something went wrong, please try again later.');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import clsx from 'clsx';
|
|||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
import { GetServerSideProps, NextPage } from 'next';
|
import { GetServerSideProps, NextPage } from 'next';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
@ -20,12 +21,13 @@ type QueryParams = {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
resume?: Resume;
|
resume?: Resume;
|
||||||
|
locale: string;
|
||||||
redirect?: any;
|
redirect?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps<Props | Promise<Props>, QueryParams> = async ({
|
export const getServerSideProps: GetServerSideProps<Props | Promise<Props>, QueryParams> = async ({
|
||||||
query,
|
query,
|
||||||
locale,
|
locale = 'en',
|
||||||
}) => {
|
}) => {
|
||||||
const { username, slug, secretKey } = query as QueryParams;
|
const { username, slug, secretKey } = query as QueryParams;
|
||||||
|
|
||||||
@ -33,9 +35,15 @@ export const getServerSideProps: GetServerSideProps<Props | Promise<Props>, Quer
|
|||||||
if (isEmpty(secretKey)) throw new Error('There is no secret key!');
|
if (isEmpty(secretKey)) throw new Error('There is no secret key!');
|
||||||
|
|
||||||
const resume = await fetchResumeByIdentifier({ username, slug, options: { secretKey } });
|
const resume = await fetchResumeByIdentifier({ username, slug, options: { secretKey } });
|
||||||
const displayLocale = resume.metadata.locale || locale || 'en';
|
const displayLocale = get(resume, 'metadata.locale') ?? locale;
|
||||||
|
|
||||||
return { props: { resume, ...(await serverSideTranslations(displayLocale, ['common'])) } };
|
return {
|
||||||
|
props: {
|
||||||
|
resume,
|
||||||
|
locale: displayLocale,
|
||||||
|
...(await serverSideTranslations(displayLocale, ['common'])),
|
||||||
|
},
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@ -46,10 +54,20 @@ export const getServerSideProps: GetServerSideProps<Props | Promise<Props>, Quer
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Printer: NextPage<Props> = ({ resume: initialData }) => {
|
const Printer: NextPage<Props> = ({ resume: initialData, locale }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const resume = useAppSelector((state) => state.resume);
|
const resume = useAppSelector((state) => state.resume.present);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (router.locale !== locale) {
|
||||||
|
const { pathname, asPath, query } = router;
|
||||||
|
|
||||||
|
router.push({ pathname, query }, asPath, { locale });
|
||||||
|
}
|
||||||
|
}, [router, locale]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialData) dispatch(setResume(initialData));
|
if (initialData) dispatch(setResume(initialData));
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user