Compare commits
879 Commits
v3.0.0-bet
...
v3.6.8
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 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 | |||
| 7d188622a8 | |||
| 97e9432d6b | |||
| c46b8fc162 | |||
| b2f1fb3a55 | |||
| 4743828e6b | |||
| 519fbbd1b2 | |||
| ebc084ad52 | |||
| 26fdd72610 | |||
| ea704c6d99 | |||
| ea88044d25 | |||
| a461cc147b | |||
| 5aefcae2ac | |||
| ba1e968510 | |||
| ba12abe506 | |||
| 29fb1dcca3 | |||
| 4be6c48aab | |||
| ee1017aa25 | |||
| bf806c5ecf | |||
| bf9709ed8e | |||
| 6c74ecfef7 | |||
| fb8c925037 | |||
| c8c154c2f4 | |||
| 92b2c4b757 | |||
| 260a354c22 | |||
| c67a969353 | |||
| 8d61703250 | |||
| 28df783bba | |||
| 51575a340b | |||
| 8068d34bf3 | |||
| b154fae0fa | |||
| c5ba1730c3 | |||
| a7d90da30e | |||
| 0bbc54a97f | |||
| 2081f1344f | |||
| d029607e16 | |||
| 5fe0c02cec | |||
| 70b45b3686 | |||
| ff098d5df1 | |||
| 95d7d70caa | |||
| 107ba6e525 | |||
| f72e0556e5 | |||
| 0ef975a177 | |||
| eb9f5450df | |||
| c7fffff495 | |||
| 42408ce8c5 | |||
| 7c49b50979 | |||
| 59b2fc9fd6 | |||
| f93ac987ac | |||
| fb32f9b523 | |||
| ed78f8fc4e | |||
| 318145f007 | |||
| c2a35a1066 | |||
| 541cfa784d | |||
| de53d8dfe7 | |||
| c28afbc75d | |||
| 40e6227aa9 | |||
| 02e396bfdb | |||
| 4dc83c1d7f | |||
| 143a123212 | |||
| c64b96619f | |||
| ff35a2a95c | |||
| 549363bbe5 | |||
| e6bda688ac | |||
| 64b0c5e7cf | |||
| 57f7edc134 | |||
| c62a3c2dfd | |||
| b7f024913c | |||
| 488631e6b0 | |||
| ca5a866249 | |||
| 3a0cd4e150 | |||
| e82e714e41 | |||
| 21931bc324 | |||
| ed75a85827 | |||
| fbb0285d0d | |||
| b056b002b7 | |||
| 8b32bfb9f4 | |||
| cf3696c976 | |||
| aa0dc1d7fb | |||
| f5bf77cfd0 | |||
| 9ddbc7cab2 | |||
| f7d11c5fd2 | |||
| bede07656b | |||
| 49b56f7a76 | |||
| 1421fc5183 | |||
| b3da226d24 | |||
| 3d7a5b9313 | |||
| 86ca4602fd | |||
| 3dde7e5772 | |||
| 0782c616ea | |||
| d1d3f240b4 | |||
| b18120b3f7 | |||
| b5809ea449 | |||
| 01acec4a51 | |||
| 9d076d384c | |||
| e7a8596456 | |||
| ab4df6193c | |||
| e4a9f269d2 | |||
| 189cc702c2 | |||
| c348b6449b | |||
| 708920df44 | |||
| 81733e5855 | |||
| 794c7df374 | |||
| 267f593ec2 | |||
| 048927a163 | |||
| f4a65122c6 | |||
| 6587c76397 | |||
| 80223a240c | |||
| 50faa5dff3 | |||
| 6a4521b057 | |||
| 81a4d7291a | |||
| 381cfcc220 | |||
| 0f555e4f88 | |||
| ebd9253038 | |||
| cf670af403 | |||
| dfccb3130f | |||
| ef06240935 | |||
| 55e57353a4 | |||
| f0144cc6e7 | |||
| e5150ab128 | |||
| d61905db10 | |||
| 6d55f917ea | |||
| 4371f3b693 | |||
| c8c5916d02 | |||
| 3ca27f2326 | |||
| f78f24c972 | |||
| 11cb066573 | |||
| 528ac84d3b | |||
| b515fc36e7 | |||
| d7268423df | |||
| bf167f81a3 | |||
| 9e2f22d878 | |||
| 084b909152 | |||
| 3955afee8d | |||
| f2dd2b5fcf | |||
| 305561955a | |||
| cadbd3dfe8 | |||
| b5cd6c412b | |||
| 799f20823e | |||
| dda42b4c6b | |||
| f1c260736a | |||
| b5a9b26f34 | |||
| 918bd555c1 | |||
| 9ea2775790 | |||
| 9d83b997f5 | |||
| 228fb42ba5 | |||
| 01da1a06b8 | |||
| 82bf44daa2 | |||
| 2cbc582a12 | |||
| 2b9f016b95 | |||
| 358c97eb71 | |||
| 76ef513b46 | |||
| 497c6e01f1 | |||
| e78c4a9adb | |||
| 30d0151bdb | |||
| 4dd94c3363 | |||
| f711b089bc | |||
| 01c1125153 | |||
| fa42d82416 | |||
| 6322d4d105 | |||
| 77467929c7 | |||
| 3a524f9c9c | |||
| 63f900870b | |||
| 32f78e85f7 | |||
| 1cc2232730 | |||
| 2ff6761630 | |||
| 5836e55a36 | |||
| ec98c14fbd | |||
| 78c1f5a380 | |||
| 808fa45124 | |||
| 2625ed4f3d | |||
| 40085f8d78 | |||
| f4e3be178c | |||
| 601f61c59a | |||
| 59049e8f77 | |||
| 798e77f693 | |||
| 78565079e7 | |||
| 0bec4cff05 | |||
| 26dc0069f9 | |||
| 90bb80b1e2 | |||
| 61ed3ff018 | |||
| 36a12e82a2 | |||
| a3cf1752cc | |||
| 5b79e23564 | |||
| 300e4a790d | |||
| ba4666b767 | |||
| b283c6ee8f | |||
| 316eca35ef | |||
| 16c18de964 | |||
| 0c23af4be8 | |||
| fe1b325fdf | |||
| 9395a4d578 | |||
| c11f92841b | |||
| 2654cba039 | |||
| 7d8828a358 | |||
| 8bc7d2599e | |||
| 036adbfc96 | |||
| 4b7e43424c | |||
| 0f1c3a8142 | |||
| 8dc27ecf07 | |||
| a05917b00d | |||
| d5f2eea34c | |||
| 29bc3f33a6 | |||
| b332b77eff | |||
| 8e09db276e | |||
| 2f7cfd2add | |||
| 6a4464b239 | |||
| 972e8b1bcf | |||
| ad916c5b07 | |||
| eca80a1663 | |||
| 8f48f5fcd6 | |||
| 40f5111eba | |||
| 55a09c0c05 | |||
| 1e72efa7ac | |||
| fd752bfd70 | |||
| ee328186c8 | |||
| ecab1e0bfa | |||
| cbbdc92c66 | |||
| 5d54f8101b | |||
| 4fe5788b23 | |||
| 612335696c | |||
| ef3b2c5638 | |||
| 6c671f2dba | |||
| dc4aa0b496 |
@ -1,13 +1,22 @@
|
|||||||
|
# Android App
|
||||||
|
/app
|
||||||
|
|
||||||
# Build Artifacts
|
# Build Artifacts
|
||||||
dist
|
dist
|
||||||
.next
|
.next
|
||||||
|
.turbo
|
||||||
|
|
||||||
# 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
|
||||||
@ -15,4 +24,4 @@ node_modules
|
|||||||
# Docker
|
# Docker
|
||||||
Dockerfile
|
Dockerfile
|
||||||
.dockerignore
|
.dockerignore
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
|||||||
53
.env.example
@ -1,30 +1,39 @@
|
|||||||
# App
|
# Turbo Cache (Optional)
|
||||||
TZ=UTC
|
TURBO_TEAM=
|
||||||
SECRET_KEY=change-me
|
TURBO_TOKEN=
|
||||||
|
|
||||||
# URLs
|
# Server + Client
|
||||||
|
TZ=UTC
|
||||||
PUBLIC_URL=http://localhost:3000
|
PUBLIC_URL=http://localhost:3000
|
||||||
PUBLIC_SERVER_URL=http://localhost:3100
|
PUBLIC_SERVER_URL=http://localhost:3100
|
||||||
|
PUBLIC_GOOGLE_CLIENT_ID=
|
||||||
|
|
||||||
# Database
|
# Server + Database
|
||||||
POSTGRES_HOST=localhost
|
POSTGRES_DB=postgres
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_USER=postgres
|
||||||
POSTGRES_USERNAME=postgres
|
|
||||||
POSTGRES_PASSWORD=postgres
|
POSTGRES_PASSWORD=postgres
|
||||||
POSTGRES_DATABASE=reactive_resume
|
|
||||||
|
# Server
|
||||||
|
SECRET_KEY=
|
||||||
|
POSTGRES_HOST=postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
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=
|
||||||
|
|
||||||
# Google
|
# Flags (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,30 +1,22 @@
|
|||||||
{
|
{
|
||||||
"root": true,
|
"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": {
|
||||||
|
// ESLint
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
|
||||||
|
// Simple Import Sort
|
||||||
|
"simple-import-sort/imports": "error",
|
||||||
|
"simple-import-sort/exports": "error",
|
||||||
|
|
||||||
// TypeScript ESLint
|
// TypeScript ESLint
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"@typescript-eslint/interface-name-prefix": "off",
|
"@typescript-eslint/interface-name-prefix": "off",
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
"@typescript-eslint/explicit-module-boundary-types": "off"
|
||||||
// Simple Import Sort
|
|
||||||
"simple-import-sort/imports": "error",
|
|
||||||
"simple-import-sort/exports": "error",
|
|
||||||
// Unused Imports
|
|
||||||
"no-unused-vars": "off",
|
|
||||||
"unused-imports/no-unused-imports": "error",
|
|
||||||
"unused-imports/no-unused-vars": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
"vars": "all",
|
|
||||||
"varsIgnorePattern": "^_",
|
|
||||||
"args": "none",
|
|
||||||
"argsIgnorePattern": "^_"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
|
|||||||
2
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
github: AmruthPillai
|
||||||
|
custom: https://paypal.me/RajaRajanA
|
||||||
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. -->
|
||||||
|
|||||||
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help improve
|
|
||||||
title: "[BUG] "
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
|
||||||
- OS: [e.g. iOS]
|
|
||||||
- Browser [e.g. chrome, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Smartphone (please complete the following information):**
|
|
||||||
- Device: [e.g. iPhone6]
|
|
||||||
- OS: [e.g. iOS8.1]
|
|
||||||
- Browser [e.g. stock browser, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
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. -->
|
||||||
|
|||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: "[FEATURE] "
|
|
||||||
labels: enhancement
|
|
||||||
assignees: AmruthPillai
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
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'
|
||||||
2
.github/workflows/digitalocean-deploy.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install DigitalOcean CLI
|
- name: Install DigitalOcean CLI
|
||||||
uses: digitalocean/action-doctl@v2.1.0
|
uses: digitalocean/action-doctl@v2.1.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.DIGITALOCEAN_TOKEN }}
|
token: ${{ secrets.DIGITALOCEAN_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
146
.github/workflows/docker-build-push.yml
vendored
@ -5,116 +5,108 @@ on:
|
|||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker_client:
|
client:
|
||||||
name: Docker (Client)
|
name: Client
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v3.0.0
|
uses: actions/checkout@v3.1.0
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
- id: slug
|
- id: version
|
||||||
name: Get Short Commit SHA
|
name: Get Version
|
||||||
run: echo "::set-output name=sha8::$(echo ${GITHUB_SHA} | cut -c1-8)"
|
run: echo "version=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Login to Docker
|
- name: Set up QEMU
|
||||||
uses: docker/login-action@v1.14.1
|
uses: docker/setup-qemu-action@v2.0.0
|
||||||
|
|
||||||
|
- id: buildx
|
||||||
|
name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
|
with:
|
||||||
|
install: true
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v2.0.0
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: $GITHUB_REPOSITORY_OWNER
|
||||||
|
password: ${{ secrets.GH_TOKEN }}
|
||||||
|
|
||||||
- name: Build and Push Client Image
|
- name: Build and Push Client Image
|
||||||
uses: docker/build-push-action@v2.9.0
|
uses: docker/build-push-action@v3.1.1
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
file: client/Dockerfile
|
file: client/Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: |
|
tags: |
|
||||||
amruthpillai/reactive-resume:client-latest
|
amruthpillai/reactive-resume:client-latest
|
||||||
amruthpillai/reactive-resume:client-${{ steps.slug.outputs.sha8 }}
|
amruthpillai/reactive-resume:client-${{ env.version }}
|
||||||
|
ghcr.io/amruthpillai/reactive-resume:client-latest
|
||||||
|
ghcr.io/amruthpillai/reactive-resume:client-${{ env.version }}
|
||||||
|
|
||||||
docker_server:
|
server:
|
||||||
name: Docker (Server)
|
name: Server
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v3.0.0
|
uses: actions/checkout@v3.1.0
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
- id: slug
|
- id: version
|
||||||
name: Get Short Commit SHA
|
name: Get Version
|
||||||
run: echo "::set-output name=sha8::$(echo ${GITHUB_SHA} | cut -c1-8)"
|
run: echo "version=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Login to Docker
|
- name: Set up QEMU
|
||||||
uses: docker/login-action@v1.14.1
|
uses: docker/setup-qemu-action@v2.0.0
|
||||||
|
|
||||||
|
- id: buildx
|
||||||
|
name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
|
with:
|
||||||
|
install: true
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v2.0.0
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: $GITHUB_REPOSITORY_OWNER
|
||||||
|
password: ${{ secrets.GH_TOKEN }}
|
||||||
|
|
||||||
- name: Build and Push Server Image
|
- name: Build and Push Server Image
|
||||||
uses: docker/build-push-action@v2.9.0
|
uses: docker/build-push-action@v3.1.1
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
file: server/Dockerfile
|
file: server/Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: |
|
tags: |
|
||||||
amruthpillai/reactive-resume:server-latest
|
amruthpillai/reactive-resume:server-latest
|
||||||
amruthpillai/reactive-resume:server-${{ steps.slug.outputs.sha8 }}
|
amruthpillai/reactive-resume:server-${{ env.version }}
|
||||||
|
|
||||||
github_client:
|
|
||||||
name: GitHub (Client)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout the repository
|
|
||||||
uses: actions/checkout@v3.0.0
|
|
||||||
|
|
||||||
- id: slug
|
|
||||||
name: Get Short Commit SHA
|
|
||||||
run: echo "::set-output name=sha8::$(echo ${GITHUB_SHA} | cut -c1-8)"
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v1.14.1
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: $GITHUB_REPOSITORY_OWNER
|
|
||||||
password: ${{ secrets.GH_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and Push Client Image
|
|
||||||
uses: docker/build-push-action@v2.9.0
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
file: client/Dockerfile
|
|
||||||
tags: |
|
|
||||||
ghcr.io/amruthpillai/reactive-resume:client-latest
|
|
||||||
ghcr.io/amruthpillai/reactive-resume:client-${{ steps.slug.outputs.sha8 }}
|
|
||||||
|
|
||||||
github_server:
|
|
||||||
name: GitHub (Server)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout the repository
|
|
||||||
uses: actions/checkout@v3.0.0
|
|
||||||
|
|
||||||
- id: slug
|
|
||||||
name: Get Short Commit SHA
|
|
||||||
run: echo "::set-output name=sha8::$(echo ${GITHUB_SHA} | cut -c1-8)"
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v1.14.1
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: $GITHUB_REPOSITORY_OWNER
|
|
||||||
password: ${{ secrets.GH_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and Push Server Image
|
|
||||||
uses: docker/build-push-action@v2.9.0
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
file: server/Dockerfile
|
|
||||||
tags: |
|
|
||||||
ghcr.io/amruthpillai/reactive-resume:server-latest
|
ghcr.io/amruthpillai/reactive-resume:server-latest
|
||||||
ghcr.io/amruthpillai/reactive-resume:server-${{ steps.slug.outputs.sha8 }}
|
ghcr.io/amruthpillai/reactive-resume:server-${{ env.version }}
|
||||||
|
|||||||
6
.gitignore
vendored
@ -5,3 +5,9 @@
|
|||||||
|
|
||||||
# Project Dependencies
|
# Project Dependencies
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Turbo
|
||||||
|
.turbo
|
||||||
@ -1,6 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
pnpm install
|
|
||||||
pnpm run lint
|
|
||||||
pnpm run format
|
|
||||||
2
.npmrc
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
auto-install-peers=true
|
||||||
|
strict-peer-dependencies=false
|
||||||
@ -1,4 +1,29 @@
|
|||||||
dist
|
# Android App
|
||||||
.next
|
/app
|
||||||
__ENV.js
|
|
||||||
node_modules
|
# Schema
|
||||||
|
schema/dist
|
||||||
|
|
||||||
|
# Server
|
||||||
|
server/dist
|
||||||
|
|
||||||
|
# Client
|
||||||
|
client/.next
|
||||||
|
client/public/__ENV.js
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Project Metadata
|
||||||
|
LICENSE
|
||||||
|
README.md
|
||||||
|
CHANGELOG.md
|
||||||
|
|
||||||
|
# Project Dependencies
|
||||||
|
node_modules
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
docker-compose.yml
|
||||||
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
@ -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
@ -1,15 +1,22 @@
|
|||||||
{
|
{
|
||||||
"css.validate": false,
|
"css.validate": false,
|
||||||
"scss.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",
|
"eslint.workingDirectories": [
|
||||||
"editor.formatOnSave": true,
|
"schema",
|
||||||
"editor.wordWrap": "on",
|
"client",
|
||||||
"eslint.workingDirectories": ["schema", "client", "server"],
|
"server"
|
||||||
"i18n-ally.enabledFrameworks": ["i18next"],
|
],
|
||||||
"i18n-ally.localesPaths": ["client/public/locales"],
|
"conventionalCommits.scopes": [
|
||||||
"i18n-ally.sourceLanguage": "en",
|
"client",
|
||||||
"i18n-ally.keystyle": "nested"
|
"server",
|
||||||
}
|
"docker",
|
||||||
|
"dependencies"
|
||||||
|
]
|
||||||
|
}
|
||||||
128
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
- Demonstrating empathy and kindness toward other people
|
||||||
|
- Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
- Giving and gracefully accepting constructive feedback
|
||||||
|
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
- Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
- The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
- Public or private harassment
|
||||||
|
- Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
- Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
im.amruth@gmail.com.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Amruth Pillai
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
151
README.md
@ -1,27 +1,49 @@
|
|||||||
<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://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://beta.rxresu.me/)
|
## [Go to App](https://rxresu.me) | [Docs](https://docs.rxresu.me)
|
||||||
|
|
||||||
Reactive Resume is a free and open source resume builder that’s built to make the mundane tasks of creating, updating and sharing your resume as easy as 1, 2, 3. With this app, you can create multiple resumes, share them with recruiters through a unique link and print as PDF, all for free, no advertisements, without losing the integrity and privacy of your data.
|
Reactive Resume is a free and open source resume builder that’s built to make the mundane tasks of creating, updating and sharing your resume as easy as 1, 2, 3. With this app, you can create multiple resumes, share them with recruiters through a unique link and print as PDF, all for free, no advertisements, without losing the integrity and privacy of your data.
|
||||||
|
|
||||||
You have complete control over what goes into your resume, how it looks, what colors, what templates, even the layout in which sections placed. Want a dark mode resume? It’s as easy as editing 3 values and you’re done. You don’t need to wait to see your changes either. Everything you type, everything you change, appears immediately on your resume and gets updated in real time.
|
You have complete control over what goes into your resume, how it looks, what colors, what templates, even the layout in which sections placed. Want a dark mode resume? It’s as easy as editing 3 values and you’re done. You don’t need to wait to see your changes either. Everything you type, everything you change, appears immediately on your resume and gets updated in real time.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Reactive Resume](#reactive-resume)
|
||||||
|
- [Go to App | [Docs](https://docs.rxresu.me)](#go-to-app--docs)
|
||||||
|
- [Table of Contents](#table-of-contents)
|
||||||
|
- [Features](#features)
|
||||||
|
- [Languages](#languages)
|
||||||
|
- [Tutorial](#tutorial)
|
||||||
|
- [Build from Source](#build-from-source)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
- [Report Bugs and Feature Requests](#report-bugs-and-feature-requests)
|
||||||
|
- [Donations](#donations)
|
||||||
|
- [GitHub Sponsor](#github-sponsor)
|
||||||
|
- [PayPal](#paypal)
|
||||||
|
- [Infrastructure](#infrastructure)
|
||||||
|
- [Contributors Wall](#contributors-wall)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Free, forever
|
- Free, forever
|
||||||
- No Advertising
|
- No Advertising
|
||||||
- No Tracking (no 🍪s too)
|
- No User Tracking
|
||||||
- Sync your data across devices
|
- Sync your data across devices
|
||||||
|
- Accessible in multiple languages
|
||||||
- Import data from [LinkedIn](https://www.linkedin.com/), [JSON Resume](https://jsonresume.org/)
|
- Import data from [LinkedIn](https://www.linkedin.com/), [JSON Resume](https://jsonresume.org/)
|
||||||
- Manage multiple resumes with one account
|
- Manage multiple resumes with one account
|
||||||
- Open Source (with large community support)
|
- Open Source (with large community support)
|
||||||
- Send your resume to others with a unique sharable link
|
- Send your resume to others with a unique sharable link
|
||||||
- Accessible in multiple languages, [help translate here](https://translate.rxresu.me/)
|
|
||||||
- Pick any font from [Google Fonts](https://fonts.google.com/) to use on your resume
|
- Pick any font from [Google Fonts](https://fonts.google.com/) to use on your resume
|
||||||
- Choose from 6 vibrant templates and more coming soon
|
- Choose from 6 vibrant templates and more coming soon
|
||||||
- Export your resume to JSON or PDF format with just one click
|
- Export your resume to JSON or PDF format with just one click
|
||||||
@ -31,64 +53,62 @@ You have complete control over what goes into your resume, how it looks, what co
|
|||||||
- Tailor-made Backend and Database, isolated from Google, Amazon etc.
|
- Tailor-made Backend and Database, isolated from Google, Amazon etc.
|
||||||
- **Oh, and did I mention that it's free?**
|
- **Oh, and did I mention that it's free?**
|
||||||
|
|
||||||
## Docker Setup
|
## Languages
|
||||||
|
|
||||||
You can pull the prebuilt docker images right off the shelf from either [Docker Hub](https://hub.docker.com/repository/docker/amruthpillai/reactive-resume) or [GitHub Container Registry](https://ghcr.io/amruthpillai/reactive-resume). Keep in mind, you would also need a database for this to work as intended.
|
- Amharic (አማርኛ)
|
||||||
|
- Arabic (اَلْعَرَبِيَّةُ)
|
||||||
|
- Bengali (বাংলা)
|
||||||
|
- Bulgarian (български)
|
||||||
|
- Catalan (Valencian)
|
||||||
|
- Chinese (中文)
|
||||||
|
- Czech (čeština)
|
||||||
|
- Danish (Dansk)
|
||||||
|
- Dutch (Nederlands)
|
||||||
|
- English
|
||||||
|
- Finnish (Suomi)
|
||||||
|
- French (Français)
|
||||||
|
- German (Deutsch)
|
||||||
|
- Greek (Ελληνικά)
|
||||||
|
- Hebrew (Ivrit)
|
||||||
|
- Hindi (हिन्दी)
|
||||||
|
- Hungarian (Magyar)
|
||||||
|
- Indonesian (Bahasa Indonesia)
|
||||||
|
- Italian (Italiano)
|
||||||
|
- Japanese (日本語)
|
||||||
|
- Kannada (ಕನ್ನಡ)
|
||||||
|
- Khmer (ភាសាខ្មែរ)
|
||||||
|
- Korean (한국어)
|
||||||
|
- Malayalam (മലയാളം)
|
||||||
|
- Marathi (मराठी)
|
||||||
|
- Nepali (नेपाली)
|
||||||
|
- Norwegian (Norsk)
|
||||||
|
- Odia (ଓଡ଼ିଆ)
|
||||||
|
- Persian (فارسی)
|
||||||
|
- Polish (Polski)
|
||||||
|
- Portuguese (Português)
|
||||||
|
- Romanian (limba română)
|
||||||
|
- Russian (русский)
|
||||||
|
- Serbian (српски језик)
|
||||||
|
- Spanish (Español)
|
||||||
|
- Swedish (Svenska)
|
||||||
|
- Tamil (தமிழ்)
|
||||||
|
- Turkish (Türkçe)
|
||||||
|
- Ukranian (Українська мова)
|
||||||
|
- Vietnamese (Tiếng Việt)
|
||||||
|
|
||||||
```sh
|
Help by [translating Reactive Resume](https://translate.rxresu.me) to your language!
|
||||||
# Server
|
|
||||||
docker run -p 3100:3100 --env-file .env amruthpillai/reactive-resume:server-latest
|
|
||||||
|
|
||||||
# Client
|
## Tutorial
|
||||||
docker run -p 3000:3000 --env-file .env amruthpillai/reactive-resume:client-latest
|
|
||||||
```
|
|
||||||
|
|
||||||
Or, to make your life easier there's a simple `docker-compose.yml` included to help you get set up for success.
|
The docs include an extensive [Tutorial](https://docs.rxresu.me/tutorial) section which outline the features of Reactive Resume and help you through building your first resume on the app.
|
||||||
|
|
||||||
```sh
|
|
||||||
docker compose up
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build from Source
|
## Build from Source
|
||||||
|
|
||||||
If you don't want to use Docker, I understand. There's an old-school way to build the app too. This project, and these instructions rely heavily on [pnpm](https://pnpm.io/) so you might want to have that installed on your system before you continue.
|
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.
|
||||||
|
|
||||||
1. Clone the repository locally, or use GitHub Codespaces or CodeSandbox
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://github.com/AmruthPillai/Reactive-Resume.git
|
|
||||||
cd Reactive-Resume
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Install dependencies using `pnpm`, but feel free to use any other package manager that supports npm workspaces.
|
|
||||||
|
|
||||||
```
|
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Copy the `.env.example` file to `.env` in the project root and fill it with values according to your setup. You can skip the SendGrid variables if you don't want to set up mail right away.
|
|
||||||
|
|
||||||
```
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Use Docker Compose to create a PostgreSQL instance and a `reactive_resume` database, or feel free to use your own and modify the variables used in `.env`
|
|
||||||
|
|
||||||
```
|
|
||||||
docker-compose up -d postgres
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Run the project and start building!
|
|
||||||
|
|
||||||
```
|
|
||||||
pnpm dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Please refer to the project's style and contribution guidelines for submitting pull requests.
|
This project makes use of [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) style and workflow for commit messages to ensure that the CHANGELOG is auto-generated. In general, this project follows the "fork-and-pull" Git workflow.
|
||||||
|
|
||||||
In general, this project follows the "fork-and-pull" Git workflow.
|
|
||||||
|
|
||||||
1. **Fork** the repo on GitHub
|
1. **Fork** the repo on GitHub
|
||||||
2. **Clone** the project to your own machine
|
2. **Clone** the project to your own machine
|
||||||
@ -98,28 +118,39 @@ In general, this project follows the "fork-and-pull" Git workflow.
|
|||||||
|
|
||||||
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.
|
||||||
|
|
||||||
## Donations
|
## Donations
|
||||||
|
|
||||||
Reactive Resume would be nothing without the folks who supported me and kept the project alive in the beginning, and your cotinued 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 your 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.
|
||||||
|
|
||||||
[☕️ Buy me a coffee](https://www.buymeacoffee.com/AmruthPillai)
|
### [GitHub Sponsor](https://github.com/sponsors/AmruthPillai)
|
||||||
|
### [PayPal](https://paypal.me/RajaRajanA)
|
||||||
|
|
||||||
## Infrastructure
|
## Infrastructure
|
||||||
|
|
||||||
- Next.js, frontend
|
- [Next.js](https://nextjs.org/), frontend
|
||||||
- NestJS, backend
|
- [NestJS](https://nestjs.com/), backend
|
||||||
- PostgreSQL, database
|
- [PostgreSQL](https://www.postgresql.org/), database
|
||||||
- DigitalOcean, infrastructure provider
|
- [DigitalOcean](https://www.digitalocean.com/), infrastructure provider
|
||||||
- Crowdin, translation management platform
|
- [Crowdin](https://translate.rxresu.me/), translation management platform
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a href="https://pillai.xyz/digitalocean">
|
<a href="https://pillai.xyz/digitalocean">
|
||||||
<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
@ -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/
|
||||||
33
app/.gitignore
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Gradle files
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Local configuration file (sdk path, etc)
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
# Log/OS Files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Android Studio generated files and folders
|
||||||
|
captures/
|
||||||
|
.externalNativeBuild/
|
||||||
|
.cxx/
|
||||||
|
*.apk
|
||||||
|
output.json
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
*.iml
|
||||||
|
.idea/
|
||||||
|
misc.xml
|
||||||
|
deploymentTargetDropDown.xml
|
||||||
|
render.experimental.xml
|
||||||
|
|
||||||
|
# Keystore files
|
||||||
|
*.jks
|
||||||
|
*.keystore
|
||||||
|
|
||||||
|
# Google Services (e.g. APIs or Firebase)
|
||||||
|
google-services.json
|
||||||
|
|
||||||
|
# Android Profiling
|
||||||
|
*.hprof
|
||||||
8
app/app/.editorconfig
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
2
app/app/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/build
|
||||||
|
/release
|
||||||
46
app/app/build.gradle
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
plugins {
|
||||||
|
id 'com.android.application'
|
||||||
|
id 'org.jetbrains.kotlin.android'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk 32
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "me.rxresu.app"
|
||||||
|
minSdk 21
|
||||||
|
targetSdk 32
|
||||||
|
versionCode 3
|
||||||
|
versionName "1.0"
|
||||||
|
|
||||||
|
resConfigs "en"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
|
zipAlignEnabled true
|
||||||
|
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '1.8'
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'androidx.core:core-ktx:1.7.0'
|
||||||
|
implementation 'com.google.android.material:material:1.5.0'
|
||||||
|
}
|
||||||
21
app/app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
27
app/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="me.rxresu.app">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppTheme">
|
||||||
|
<activity
|
||||||
|
android:configChanges="orientation|screenSize"
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
BIN
app/app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
21
app/app/src/main/java/me/rxresu/app/CustomWebViewClient.kt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package me.rxresu.app
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
|
||||||
|
internal class CustomWebViewClient : WebViewClient() {
|
||||||
|
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
||||||
|
val hostname = "rxresu.me"
|
||||||
|
val uri = Uri.parse(url)
|
||||||
|
|
||||||
|
if (uri.host != null && uri.host!!.endsWith(hostname)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
view.context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/app/src/main/java/me/rxresu/app/MainActivity.kt
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package me.rxresu.app
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.webkit.WebView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var webView: WebView
|
||||||
|
|
||||||
|
private var url = "https://rxresu.me"
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
webView = findViewById(R.id.webview)
|
||||||
|
|
||||||
|
webView.webViewClient = CustomWebViewClient()
|
||||||
|
webView.settings.javaScriptEnabled = true
|
||||||
|
webView.settings.userAgentString = "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Mobile Safari/537.36"
|
||||||
|
|
||||||
|
webView.loadUrl(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (webView.canGoBack()) {
|
||||||
|
webView.goBack()
|
||||||
|
} else {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
app/app/src/main/res/layout/activity_main.xml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/content_main"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="visible" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
17
app/app/src/main/res/layout/content_main.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<WebView
|
||||||
|
android:id="@+id/webview"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
5
app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
app/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
app/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
4
app/app/src/main/res/values/ic_launcher_background.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
3
app/app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">Reactive Resume</string>
|
||||||
|
</resources>
|
||||||
6
app/app/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<resources>
|
||||||
|
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
|
<item name="windowActionBar">false</item>
|
||||||
|
<item name="windowNoTitle">true</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
9
app/build.gradle
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
plugins {
|
||||||
|
id 'com.android.application' version '7.1.2' apply false
|
||||||
|
id 'com.android.library' version '7.1.2' apply false
|
||||||
|
id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
||||||
23
app/gradle.properties
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app"s APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
|
kotlin.code.style=official
|
||||||
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
|
# thereby reducing the size of the R class for that library
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
BIN
app/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
app/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#Wed Mar 09 21:34:49 CET 2022
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
185
app/gradlew
vendored
Executable file
@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=`expr $i + 1`
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
0) set -- ;;
|
||||||
|
1) set -- "$args0" ;;
|
||||||
|
2) set -- "$args0" "$args1" ;;
|
||||||
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
89
app/gradlew.bat
vendored
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
16
app/settings.gradle
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
gradlePluginPortal()
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootProject.name = "Reactive Resume"
|
||||||
|
include ':app'
|
||||||
@ -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
@ -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,20 @@ 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
|
ARG TURBO_TEAM
|
||||||
RUN pnpm run build:client
|
ARG TURBO_TOKEN
|
||||||
|
|
||||||
FROM node:16-alpine as production
|
ENV TURBO_TEAM $TURBO_TEAM
|
||||||
|
ENV TURBO_TOKEN $TURBO_TOKEN
|
||||||
|
|
||||||
WORKDIR /app
|
RUN pnpm run build --filter client
|
||||||
|
|
||||||
RUN apk add --no-cache curl \
|
FROM base as production
|
||||||
&& 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 /app/pnpm-*.yaml /app/turbo.json ./
|
||||||
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 +45,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 127.0.0.1:3000 || exit 1
|
||||||
|
|
||||||
|
CMD [ "pnpm", "run", "start", "--filter", "client" ]
|
||||||
@ -5,6 +5,8 @@ import {
|
|||||||
FilterCenterFocus,
|
FilterCenterFocus,
|
||||||
InsertPageBreak,
|
InsertPageBreak,
|
||||||
Link,
|
Link,
|
||||||
|
RedoOutlined,
|
||||||
|
UndoOutlined,
|
||||||
ViewSidebar,
|
ViewSidebar,
|
||||||
ZoomIn,
|
ZoomIn,
|
||||||
ZoomOut,
|
ZoomOut,
|
||||||
@ -16,6 +18,7 @@ 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,13 +34,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 { 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());
|
||||||
@ -51,7 +59,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 () => {
|
||||||
@ -62,7 +70,7 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
|||||||
|
|
||||||
const url = await mutateAsync({ username, slug });
|
const url = await mutateAsync({ username, slug });
|
||||||
|
|
||||||
download(`/api${url}`);
|
download(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -74,6 +82,20 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className={styles.controller}>
|
<div className={styles.controller}>
|
||||||
|
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.undo')}>
|
||||||
|
<ButtonBase onClick={handleUndo} className={clsx({ 'pointer-events-none opacity-50': past.length < 2 })}>
|
||||||
|
<UndoOutlined fontSize="medium" />
|
||||||
|
</ButtonBase>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.redo')}>
|
||||||
|
<ButtonBase onClick={handleRedo} className={clsx({ 'pointer-events-none opacity-50': future.length === 0 })}>
|
||||||
|
<RedoOutlined fontSize="medium" />
|
||||||
|
</ButtonBase>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.zoom-in')}>
|
<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" />
|
||||||
@ -97,7 +119,10 @@ const ArtboardController: React.FC<ReactZoomPanPinchRef> = ({ zoomIn, zoomOut, c
|
|||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
<>
|
<>
|
||||||
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-orientation')}>
|
<Tooltip arrow placement="top" title={t<string>('builder.controller.tooltip.toggle-orientation')}>
|
||||||
<ButtonBase onClick={handleTogglePageOrientation}>
|
<ButtonBase
|
||||||
|
onClick={handleTogglePageOrientation}
|
||||||
|
className={clsx({ 'pointer-events-none opacity-50': pages.length === 1 })}
|
||||||
|
>
|
||||||
{orientation === 'vertical' ? (
|
{orientation === 'vertical' ? (
|
||||||
<AlignHorizontalCenter fontSize="medium" />
|
<AlignHorizontalCenter fontSize="medium" />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -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,7 +181,7 @@ 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<string>('builder.header.menu.tooltips.share-link')}>
|
<Tooltip arrow placement="right" title={t<string>('builder.header.menu.tooltips.share-link')}>
|
||||||
@ -190,7 +190,7 @@ 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>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -201,7 +201,7 @@ const Header = () => {
|
|||||||
<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,22 @@
|
|||||||
@apply hidden;
|
@apply hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.format-letter {
|
||||||
|
width: 216mm;
|
||||||
|
min-height: 279mm;
|
||||||
|
|
||||||
|
&.break::after {
|
||||||
|
top: calc(279mm - 19px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown {
|
||||||
|
ul {
|
||||||
|
padding-left: 1.5em;
|
||||||
|
text-indent: -1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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,12 +20,13 @@ 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 pageConfig: PageConfig = get(resume, 'metadata.page');
|
||||||
const typography: Typography = get(resume, 'metadata.typography');
|
const typography: Typography = get(resume, 'metadata.typography');
|
||||||
|
|
||||||
const themeCSS = useMemo(() => !isEmpty(theme) && generateThemeStyles(theme), [theme]);
|
const themeCSS = useMemo(() => !isEmpty(theme) && generateThemeStyles(theme), [theme]);
|
||||||
@ -33,7 +34,7 @@ const Page: React.FC<Props> = ({ page, showPageNumbers = false }) => {
|
|||||||
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 data-page={page + 1} data-format={pageConfig.format || 'A4'} className={styles.container}>
|
||||||
<div
|
<div
|
||||||
className={clsx({
|
className={clsx({
|
||||||
reset: true,
|
reset: true,
|
||||||
@ -42,15 +43,14 @@ const Page: React.FC<Props> = ({ page, showPageNumbers = false }) => {
|
|||||||
[css(themeCSS)]: true,
|
[css(themeCSS)]: true,
|
||||||
[css(typographyCSS)]: true,
|
[css(typographyCSS)]: true,
|
||||||
[css(customCSS.value)]: customCSS.visible,
|
[css(customCSS.value)]: customCSS.visible,
|
||||||
|
[styles['format-letter']]: pageConfig?.format === 'Letter',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{TemplatePage && <TemplatePage page={page} />}
|
{TemplatePage && <TemplatePage page={page} />}
|
||||||
</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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -25,7 +25,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]);
|
||||||
@ -65,7 +65,7 @@ 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">
|
<a className="inline-flex">
|
||||||
@ -81,14 +81,14 @@ const LeftSidebar = () => {
|
|||||||
arrow
|
arrow
|
||||||
key={id}
|
key={id}
|
||||||
placement="right"
|
placement="right"
|
||||||
title={get(sections, `${id}.name`, t<string>(`builder.leftSidebar.sections.${id}.heading`))}
|
title={get(sections, `${id}.name`, t<string>(`builder.leftSidebar.sections.${id}.heading`)) as string}
|
||||||
>
|
>
|
||||||
<IconButton onClick={() => handleClick(id)}>{icon}</IconButton>
|
<IconButton onClick={() => handleClick(id)}>{icon}</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{customSections.map(({ id }) => (
|
{customSections.map(({ id }) => (
|
||||||
<Tooltip key={id} title={get(sections, `${id}.name`, '')} placement="right" arrow>
|
<Tooltip key={id} title={get(sections, `${id}.name`, '') as string} placement="right" arrow>
|
||||||
<IconButton onClick={() => handleClick(id)}>
|
<IconButton onClick={() => handleClick(id)}>
|
||||||
<Star />
|
<Star />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@ -114,7 +114,9 @@ const LeftSidebar = () => {
|
|||||||
|
|
||||||
<div className="py-6 text-right">
|
<div className="py-6 text-right">
|
||||||
<Button fullWidth variant="outlined" startIcon={<Add />} onClick={handleAddSection}>
|
<Button fullWidth variant="outlined" startIcon={<Add />} onClick={handleAddSection}>
|
||||||
{t('builder.common.actions.add', { token: t('builder.leftSidebar.sections.section.heading') })}
|
{t<string>('builder.common.actions.add', {
|
||||||
|
token: t<string>('builder.leftSidebar.sections.section.heading'),
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -24,17 +24,19 @@ 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="flex flex-col items-center gap-4 sm:col-span-2 sm:flex-row">
|
<div className="grid items-center gap-4 sm:col-span-2 sm:grid-cols-3">
|
||||||
<PhotoUpload />
|
<div className="mx-auto">
|
||||||
|
<PhotoUpload />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full flex-col-reverse gap-4 sm:flex-col sm:gap-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
|
||||||
@ -55,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,7 +54,9 @@ 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
|
||||||
@ -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<string>('builder.leftSidebar.sections.basics.photo-upload.tooltip.upload')
|
? (t<string>('builder.leftSidebar.sections.basics.photo-upload.tooltip.upload') as string)
|
||||||
: t<string>('builder.leftSidebar.sections.basics.photo-upload.tooltip.remove')
|
: (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', {
|
||||||
section: t('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
token: t<string>('builder.leftSidebar.sections.profiles.heading', { count: 1 }),
|
||||||
})}
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -37,18 +37,18 @@ const Section: React.FC<Props> = ({
|
|||||||
|
|
||||||
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('.').at(-1) as string;
|
const id = path.split('.')[1];
|
||||||
const modal: ModalName = validate(id) ? 'builder.sections.custom' : `builder.${path}`;
|
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('.').at(-1) as string;
|
const id = path.split('.')[1];
|
||||||
const modal: ModalName = validate(id) ? 'builder.sections.custom' : `builder.${path}`;
|
const modal: ModalName = validate(id) ? 'builder.sections.custom' : `builder.${path}`;
|
||||||
const payload = validate(id) ? { path, item } : { item };
|
const payload = validate(id) ? { path, item } : { item };
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ const Section: React.FC<Props> = ({
|
|||||||
<SectionSettings path={path} />
|
<SectionSettings path={path} />
|
||||||
|
|
||||||
<Button variant="outlined" startIcon={<Add />} onClick={handleAdd}>
|
<Button variant="outlined" startIcon={<Add />} onClick={handleAdd}>
|
||||||
{t('builder.common.actions.add', { token: heading })}
|
{t<string>('builder.common.actions.add', { token: heading })}
|
||||||
</Button>
|
</Button>
|
||||||
</footer>
|
</footer>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -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);
|
||||||
@ -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,7 @@ 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', {}));
|
||||||
|
|
||||||
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 +25,7 @@ const CustomCSS = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading path="metadata.css" name={t('builder.rightSidebar.sections.css.heading')} isHideable />
|
<Heading path="metadata.css" name={t<string>('builder.rightSidebar.sections.css.heading')} isHideable />
|
||||||
|
|
||||||
<Editor
|
<Editor
|
||||||
height="200px"
|
height="200px"
|
||||||
|
|||||||
@ -13,18 +13,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 +34,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 () => {
|
||||||
@ -47,12 +48,12 @@ const Export = () => {
|
|||||||
|
|
||||||
const url = await mutateAsync({ username, slug });
|
const url = await mutateAsync({ username, slug });
|
||||||
|
|
||||||
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 +61,8 @@ const Export = () => {
|
|||||||
<Schema />
|
<Schema />
|
||||||
|
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('builder.rightSidebar.sections.export.json.primary')}
|
primary={t<string>('builder.rightSidebar.sections.export.json.primary')}
|
||||||
secondary={t('builder.rightSidebar.sections.export.json.secondary')}
|
secondary={t<string>('builder.rightSidebar.sections.export.json.secondary')}
|
||||||
/>
|
/>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|||||||
@ -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,7 +60,7 @@ 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<string>('builder.rightSidebar.sections.layout.tooltip.reset-layout')}>
|
<Tooltip title={t<string>('builder.rightSidebar.sections.layout.tooltip.reset-layout')}>
|
||||||
<IconButton onClick={handleResetLayout}>
|
<IconButton onClick={handleResetLayout}>
|
||||||
@ -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<string>('builder.common.actions.delete', { token: t('builder.common.glossary.page') })}
|
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,20 +10,21 @@ 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 { 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';
|
||||||
import ThemeSwitch from '@/components/shared/ThemeSwitch';
|
import ThemeSwitch from '@/components/shared/ThemeSwitch';
|
||||||
import { Language, languages } from '@/config/languages';
|
import { Language, languageMap, languages } from '@/config/languages';
|
||||||
import { ServerError } from '@/services/axios';
|
import { ServerError } from '@/services/axios';
|
||||||
import queryClient from '@/services/react-query';
|
import queryClient from '@/services/react-query';
|
||||||
import { loadSampleData, LoadSampleDataParams, resetResume, ResetResumeParams } from '@/services/resume';
|
import { loadSampleData, LoadSampleDataParams, resetResume, ResetResumeParams } from '@/services/resume';
|
||||||
import { setLanguage, setTheme, togglePageBreakLine, togglePageOrientation } from '@/store/build/buildSlice';
|
import { setTheme, togglePageBreakLine, togglePageOrientation } from '@/store/build/buildSlice';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
import { setResumeState } from '@/store/resume/resumeSlice';
|
import { setResumeState } from '@/store/resume/resumeSlice';
|
||||||
import { dateFormatOptions } from '@/utils/date';
|
import { dateFormatOptions } from '@/utils/date';
|
||||||
@ -33,19 +34,24 @@ const Settings = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const resume = useAppSelector((state) => state.resume);
|
const { locale, ...router } = useRouter();
|
||||||
|
|
||||||
|
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 language = useAppSelector((state) => state.build.language);
|
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);
|
||||||
|
|
||||||
const id: number = useMemo(() => get(resume, 'id'), [resume]);
|
const id: number = useMemo(() => get(resume, 'id'), [resume]);
|
||||||
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 pageConfig: PageConfig = useMemo(() => get(resume, 'metadata.page'), [resume]);
|
||||||
const dateConfig: DateConfig = useMemo(() => get(resume, 'metadata.date'), [resume]);
|
const dateConfig: DateConfig = useMemo(() => get(resume, 'metadata.date'), [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>(
|
||||||
@ -55,11 +61,20 @@ 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 }));
|
||||||
|
|
||||||
const handleChangeLanguage = (value: Language | null) => {
|
const handleChangeLanguage = (value: Language | null) => {
|
||||||
dispatch(setLanguage({ language: value?.code || 'en' }));
|
const { pathname, asPath, query, push } = router;
|
||||||
|
const code = value?.code || 'en';
|
||||||
|
|
||||||
|
document.cookie = `NEXT_LOCALE=${code}; path=/; expires=2147483647`;
|
||||||
|
dispatch(setResumeState({ path: 'metadata.locale', value: code }));
|
||||||
|
|
||||||
|
push({ pathname, query }, asPath, { locale: code });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoadSampleData = async () => {
|
const handleLoadSampleData = async () => {
|
||||||
@ -69,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 sx={{ padding: 0 }}>
|
||||||
{/* 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>
|
||||||
@ -90,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)} />
|
||||||
@ -99,30 +119,30 @@ 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}
|
||||||
value={language}
|
value={languageMap[locale ?? 'en']}
|
||||||
isOptionEqualToValue={(a, b) => a.code === b.code}
|
isOptionEqualToValue={(a, b) => a.code === b.code}
|
||||||
onChange={(_, value) => handleChangeLanguage(value)}
|
onChange={(_, value) => handleChangeLanguage(value)}
|
||||||
renderInput={(params) => <TextField {...params} />}
|
renderInput={(params) => <TextField {...params} />}
|
||||||
@ -139,15 +159,39 @@ 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={t('builder.rightSidebar.sections.settings.page.orientation.secondary')}
|
secondary={
|
||||||
|
pages.length === 1
|
||||||
|
? t<string>('builder.rightSidebar.sections.settings.page.orientation.disabled')
|
||||||
|
: t<string>('builder.rightSidebar.sections.settings.page.orientation.secondary')
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
color="secondary"
|
color="secondary"
|
||||||
|
disabled={pages.length === 1}
|
||||||
checked={orientation === 'horizontal'}
|
checked={orientation === 'horizontal'}
|
||||||
onChange={() => dispatch(togglePageOrientation())}
|
onChange={() => dispatch(togglePageOrientation())}
|
||||||
/>
|
/>
|
||||||
@ -155,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>
|
||||||
@ -164,8 +208,8 @@ 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>
|
||||||
@ -174,8 +218,8 @@ const Settings = () => {
|
|||||||
<Anchor />
|
<Anchor />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={t('builder.rightSidebar.sections.settings.resume.sample.primary')}
|
primary={t<string>('builder.rightSidebar.sections.settings.resume.sample.primary')}
|
||||||
secondary={t('builder.rightSidebar.sections.settings.resume.sample.secondary')}
|
secondary={t<string>('builder.rightSidebar.sections.settings.resume.sample.secondary')}
|
||||||
/>
|
/>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@ -186,8 +230,12 @@ const Settings = () => {
|
|||||||
<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>
|
||||||
|
|||||||
@ -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,14 @@ 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 src={template.preview} alt={template.name} className="rounded-sm" layout="fill" priority />
|
||||||
</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 } 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<Theme>((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" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -94,7 +94,7 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
|||||||
const url = getResumeUrl(resume, { withHost: true });
|
const url = getResumeUrl(resume, { withHost: true });
|
||||||
await navigator.clipboard.writeText(url);
|
await navigator.clipboard.writeText(url);
|
||||||
|
|
||||||
toast.success(t('common.toast.success.resume-link-copied'));
|
toast.success(t<string>('common.toast.success.resume-link-copied'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
@ -124,7 +124,7 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
|||||||
<footer>
|
<footer>
|
||||||
<div className={styles.meta}>
|
<div className={styles.meta}>
|
||||||
<p>{resume.name}</p>
|
<p>{resume.name}</p>
|
||||||
<p>{t('dashboard.resume.timestamp', { timestamp: getRelativeTime(resume.updatedAt) })}</p>
|
<p>{t<string>('dashboard.resume.timestamp', { timestamp: getRelativeTime(resume.updatedAt) })}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ButtonBase className={styles.menu} onClick={handleOpenMenu}>
|
<ButtonBase className={styles.menu} onClick={handleOpenMenu}>
|
||||||
@ -136,21 +136,21 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<OpenInNew className="scale-90" />
|
<OpenInNew className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('dashboard.resume.menu.open')}</ListItemText>
|
<ListItemText>{t<string>('dashboard.resume.menu.open')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem onClick={handleRename}>
|
<MenuItem onClick={handleRename}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DriveFileRenameOutline className="scale-90" />
|
<DriveFileRenameOutline className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('dashboard.resume.menu.rename')}</ListItemText>
|
<ListItemText>{t<string>('dashboard.resume.menu.rename')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem onClick={handleDuplicate}>
|
<MenuItem onClick={handleDuplicate}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<ContentCopy className="scale-90" />
|
<ContentCopy className="scale-90" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{t('dashboard.resume.menu.duplicate')}</ListItemText>
|
<ListItemText>{t<string>('dashboard.resume.menu.duplicate')}</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
{resume.public ? (
|
{resume.public ? (
|
||||||
@ -158,7 +158,7 @@ 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<string>('dashboard.resume.menu.tooltips.share-link')}>
|
<Tooltip arrow placement="right" title={t<string>('dashboard.resume.menu.tooltips.share-link')}>
|
||||||
@ -167,7 +167,7 @@ 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>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -178,7 +178,7 @@ const ResumePreview: React.FC<Props> = ({ resume }) => {
|
|||||||
<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 {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { Testimony } from '@/data/testimonials';
|
import { Testimony as TestimonyType } from '@/data/testimonials';
|
||||||
|
|
||||||
import styles from './Testimony.module.scss';
|
import styles from './Testimony.module.scss';
|
||||||
|
|
||||||
type Props = Testimony;
|
type Props = TestimonyType;
|
||||||
|
|
||||||
const Testimony: React.FC<Props> = ({ name, message }) => {
|
const Testimony: React.FC<Props> = ({ name, message }) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -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, '']);
|
||||||
|
|
||||||
|
|||||||
@ -56,12 +56,12 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
|||||||
<Menu anchorEl={anchorEl} onClose={handleClose} open={Boolean(anchorEl)}>
|
<Menu anchorEl={anchorEl} onClose={handleClose} open={Boolean(anchorEl)}>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-xs opacity-50">{t('common.avatar.menu.greeting')}</span>
|
<span className="text-xs opacity-50">{t<string>('common.avatar.menu.greeting')}</span>
|
||||||
<p>{user?.name}</p>
|
<p>{user?.name}</p>
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
<MenuItem onClick={handleLogout}>{t('common.avatar.menu.logout')}</MenuItem>
|
<MenuItem onClick={handleLogout}>{t<string>('common.avatar.menu.logout')}</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
.content {
|
.content {
|
||||||
@apply rounded p-6 text-sm shadow lg:w-1/2 xl:w-2/5;
|
@apply rounded px-6 text-sm shadow lg:w-1/2 xl:w-2/5;
|
||||||
@apply absolute inset-4 sm:inset-x-4 sm:inset-y-auto lg:inset-auto;
|
@apply absolute inset-4 sm:inset-x-4 sm:inset-y-auto lg:inset-auto;
|
||||||
@apply overflow-scroll bg-neutral-50 dark:bg-neutral-900 lg:overflow-auto;
|
@apply overflow-scroll bg-neutral-50 dark:bg-neutral-900 lg:overflow-auto;
|
||||||
|
@apply max-h-[90vh] min-h-fit;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
@apply sticky top-0 left-0 right-0 z-50 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 }) => {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ const ColorAvatar: React.FC<Props> = ({ color, size = 20, onClick }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton onClick={handleClick}>
|
<IconButton onClick={handleClick}>
|
||||||
<Avatar sx={{ bgcolor: color, width: size, height: size }}> </Avatar>
|
<Avatar sx={{ bgcolor: color, width: size, height: size }}> </Avatar>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,8 +9,9 @@ const Footer: React.FC<Props> = ({ className }) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer 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">
|
||||||
A passion project by
|
A passion project by
|
||||||
@ -19,7 +20,7 @@ const Footer: React.FC<Props> = ({ className }) => {
|
|||||||
</a>
|
</a>
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -32,12 +32,12 @@ 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);
|
||||||
|
|
||||||
const id = useMemo(() => path && path.split('.').at(-1), [path]);
|
const id = useMemo(() => path.split('.')[1], [path]);
|
||||||
|
|
||||||
const icon = sections.find((x) => x.id === id)?.icon || <Grade />;
|
const icon = sections.find((x) => x.id === id)?.icon || <Grade />;
|
||||||
|
|
||||||
|
|||||||
13
client/components/shared/LanguageSwitcher.module.scss
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.popover {
|
||||||
|
width: 460px;
|
||||||
|
|
||||||
|
@apply px-4 py-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
@apply grid grid-cols-3 items-center justify-center gap-x-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language {
|
||||||
|
@apply cursor-pointer py-2 px-4 text-center hover:underline;
|
||||||
|
}
|
||||||
53
client/components/shared/LanguageSwitcher.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Language } from '@mui/icons-material';
|
||||||
|
import { IconButton, Menu, MenuItem } from '@mui/material';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { MouseEvent, useState } from 'react';
|
||||||
|
|
||||||
|
import { languages } from '@/config/languages';
|
||||||
|
import { TRANSLATE_URL } from '@/constants/index';
|
||||||
|
|
||||||
|
const LanguageSwitcher = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
|
const handleClick = (event: MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget);
|
||||||
|
|
||||||
|
const handleClose = () => setAnchorEl(null);
|
||||||
|
|
||||||
|
const handleChange = (locale: string) => {
|
||||||
|
const { pathname, asPath, query } = router;
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
|
||||||
|
document.cookie = `NEXT_LOCALE=${locale}; path=/; expires=2147483647`;
|
||||||
|
|
||||||
|
router.push({ pathname, query }, asPath, { locale });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddLanguage = () => window.open(TRANSLATE_URL, '_blank');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<IconButton onClick={handleClick}>
|
||||||
|
<Language />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleClose}>
|
||||||
|
{languages.map(({ code, name, localName }) => (
|
||||||
|
<MenuItem key={code} onClick={() => handleChange(code)}>
|
||||||
|
{name} {localName && `(${localName})`}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<MenuItem>
|
||||||
|
<span className="font-bold" onClick={handleAddLanguage}>
|
||||||
|
Add your language
|
||||||
|
</span>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LanguageSwitcher;
|
||||||