Compare commits
761 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0117e5735e | |||
| 7a3626df1c | |||
| ba11af887c | |||
| 41a9ce2b06 | |||
| becb4993b2 | |||
| bcc3ab44eb | |||
| 5d2961945c | |||
| bb1e68fcc5 | |||
| 4d9c41f52b | |||
| 13abbd21e2 | |||
| a9f4fb863f | |||
| 97f2b0c6f8 | |||
| 7cf57a999d | |||
| 1e927838d0 | |||
| cfad352903 | |||
| 2cbdf77c51 | |||
| 3b127a81fd | |||
| 99d7d3aad2 | |||
| 1640ffb177 | |||
| 9f31a890ef | |||
| 8b79a88809 | |||
| 3801b54ced | |||
| d6d8f240bd | |||
| ec7b33c008 | |||
| d11784161a | |||
| b869291589 | |||
| be4334f6a2 | |||
| ed950ddcdc | |||
| 5771f28018 | |||
| 5fbf48cc3b | |||
| 36a9b1302b | |||
| 6754cb1e45 | |||
| 4f40e8421f | |||
| d6470620eb | |||
| 795f14713d | |||
| 6b7b71dc7b | |||
| 75f76b267f | |||
| cdf8a133e3 | |||
| 91ffcba7da | |||
| 04e3cd4b8d | |||
| a256bb8ce7 | |||
| d33dc23ad1 | |||
| 4dde9fbcc1 | |||
| b9c5aac485 | |||
| 19906ea18b | |||
| eebb6890ab | |||
| 6ecf8dd404 | |||
| affbc26e22 | |||
| cfff2bf562 | |||
| 9db04bfaba | |||
| 21b4e02dee | |||
| 8804a3a18d | |||
| ff2131141c | |||
| 62b3144ef9 | |||
| 75dd08188e | |||
| ef59464bcd | |||
| 08325b3bdd | |||
| 56e0852d23 | |||
| c4ddba2933 | |||
| 2eaeb11c93 | |||
| 60b84b8e14 | |||
| 97d5bf73c4 | |||
| db9dc36e6d | |||
| 9dab038a9a | |||
| 08b5ac588c | |||
| cddb238994 | |||
| b2583724fd | |||
| c908f450f9 | |||
| c6986b5048 | |||
| 6f12251aef | |||
| 6539430980 | |||
| a13842d014 | |||
| 8d72ec2460 | |||
| 47f1beec48 | |||
| b3ca6be1c4 | |||
| 36c54a0af2 | |||
| da4c74917d | |||
| c4495d2912 | |||
| f9183384e9 | |||
| 7a4f50cb92 | |||
| 9084a2803e | |||
| 45042050ee | |||
| d4f9926e94 | |||
| 79c903f02d | |||
| d23040a374 | |||
| 882fde899b | |||
| fc96a6fc92 | |||
| 847c2c0f9b | |||
| 2a1a6498e2 | |||
| 7a721f40b6 | |||
| 23690d7bc1 | |||
| 4fb6763328 | |||
| 85939e59c6 | |||
| bb7a41cd62 | |||
| d0c18afed1 | |||
| 6bd545df55 | |||
| d71d378aa1 | |||
| 99f87ebb3e | |||
| fbe2c1f91c | |||
| 4af318dc61 | |||
| 2c22c13f3e | |||
| 6daf6e8b11 | |||
| fb18b5c94c | |||
| a4d474b8ec | |||
| 23ebb25a89 | |||
| ab95160b04 | |||
| 9e63d0b2c7 | |||
| e1186591f9 | |||
| 7cb469657d | |||
| 53d0e17865 | |||
| a8cf553217 | |||
| d11d414504 | |||
| ffb8ae45e0 | |||
| e794325787 | |||
| eb54a7f69d | |||
| b0bd8c94f3 | |||
| 9f38ebd044 | |||
| 14ea8de709 | |||
| 1ac828b532 | |||
| 13508d3c16 | |||
| 8c849e4018 | |||
| 4b007749cb | |||
| 8aaee4cd9e | |||
| aa8d9228b9 | |||
| 34627e2acb | |||
| b5d4d54ad3 | |||
| f53e34c37d | |||
| 54ddfe30d7 | |||
| d4e7914a27 | |||
| 43c8729c5b | |||
| 8bfed6a342 | |||
| 71471020b6 | |||
| 1e02f5b27a | |||
| abd18236ed | |||
| 6ffcf2a1c4 | |||
| 8628ca5475 | |||
| cf95dd6a28 | |||
| b392bada3b | |||
| 978207a700 | |||
| a7fa5f5fe9 | |||
| f73d9ad7d0 | |||
| ee93c47403 | |||
| a0dd63edb2 | |||
| 0f1778a11c | |||
| bf03e40895 | |||
| 9ed97c8b6c | |||
| b06e9b3ef0 | |||
| b81c167a10 | |||
| 33ac260683 | |||
| e28bd17d3b | |||
| 9134375ad8 | |||
| e6c8a73e72 | |||
| 1e87b1a7f4 | |||
| 70be890e37 | |||
| d6db251a8f | |||
| fa5fa3e891 | |||
| a92a56449c | |||
| 7b422bfccd | |||
| d1feab6701 | |||
| b2a511736e | |||
| e405b3b641 | |||
| 4609fb583e | |||
| a12a8a2f83 | |||
| 34d07bc863 | |||
| e4c9833c8d | |||
| 5070a11854 | |||
| 1365972676 | |||
| b0e0a53794 | |||
| cf47447102 | |||
| 7af837165d | |||
| d98e591fee | |||
| a219b1d979 | |||
| 797e30022e | |||
| d6ae8309b4 | |||
| f1a911560d | |||
| 5e5ee5fffa | |||
| 0639cef426 | |||
| d1a392c2f0 | |||
| b781731e5c | |||
| 90b968f152 | |||
| b6d5f0ce12 | |||
| eb01954b9b | |||
| 39278b7803 | |||
| bd1bd2fc97 | |||
| 8ce73a38ea | |||
| 2fa4ff8d9d | |||
| db460c9b55 | |||
| 23bf495a5a | |||
| 3ff4b5c90f | |||
| 341f36870b | |||
| 89936b651a | |||
| 04f8fe2db7 | |||
| 91329969be | |||
| 142f8e0b31 | |||
| 811e158fbc | |||
| f2bc5e36b3 | |||
| a2b637fa71 | |||
| fd79891a1a | |||
| 23386839a6 | |||
| a589b3530a | |||
| 26022ae73e | |||
| 4634b4f0a0 | |||
| f72d2639e5 | |||
| e39026c8b4 | |||
| 5ee077dac5 | |||
| c748e6a0b4 | |||
| 97f065f51c | |||
| 5b0124bedc | |||
| 51673a9e6d | |||
| 9ddba69ff0 | |||
| 2a2ceea448 | |||
| bff0bec759 | |||
| 7bdddb8dc7 | |||
| da8bf8bc36 | |||
| 5d9527e46e | |||
| a20fe10c06 | |||
| 8018d7f527 | |||
| e27bedcf88 | |||
| b196ca1bf5 | |||
| 87fee23480 | |||
| 9d486c11cc | |||
| ef3f2a802f | |||
| 8b39dcc239 | |||
| b73e3b7c4d | |||
| 4820a8bea3 | |||
| 22a171c25c | |||
| 30109ecb1d | |||
| b76fa1dcc5 | |||
| 6c31d3dff3 | |||
| c01be98cfc | |||
| 74335a1771 | |||
| 5317f5e525 | |||
| 9a7bdb188b | |||
| dc18e84bc2 | |||
| cb2aeec70a | |||
| 16ed433705 | |||
| 0bab0d01d1 | |||
| 306db64ff1 | |||
| af966bdf7b | |||
| 562a07619c | |||
| 4c63f68215 | |||
| 1215071ab9 | |||
| 3d39576a90 | |||
| 155cd499fb | |||
| aca1d13116 | |||
| fbc5f0de72 | |||
| 536bdb7bae | |||
| d507b9d54d | |||
| efe1762b33 | |||
| 5ba978dae5 | |||
| aac1e12cfc | |||
| 978aafae75 | |||
| 72176e2500 | |||
| 6ac0f37ada | |||
| 46781bba60 | |||
| 6d36c27889 | |||
| 36036cc411 | |||
| b6a0527fbe | |||
| 253f778a63 | |||
| 6c1fc543ad | |||
| dce5d2c591 | |||
| cb45629032 | |||
| 9c5b8398a3 | |||
| e78d3dbaa4 | |||
| 70cd92f29e | |||
| 4a0995eeb6 | |||
| ba31e18ffd | |||
| 3c6e45abf4 | |||
| c2ac9594ef | |||
| 69f0338b19 | |||
| da8849c854 | |||
| 294dbf195f | |||
| 98aef7eb77 | |||
| 176ba0de7a | |||
| c667c31737 | |||
| 7733d827e2 | |||
| 35c663ebe8 | |||
| 854020481c | |||
| e99c73a5b1 | |||
| 5197878b79 | |||
| 57d234ae02 | |||
| 8202ca5461 | |||
| 2e346c93eb | |||
| 5470fb8d6f | |||
| 4cd4af6ee1 | |||
| c065c844ee | |||
| 4768135963 | |||
| a63496ca86 | |||
| c40d89e98e | |||
| eb4023e3b2 | |||
| e2218030cf | |||
| fdd35492a0 | |||
| 297a258e4a | |||
| c0808af7f4 | |||
| 5887babd15 | |||
| abd24e7eb7 | |||
| 988a903acc | |||
| f76609c2fc | |||
| 53333bb6fb | |||
| d951a3f52d | |||
| 458aab4e8d | |||
| 445d1e37d9 | |||
| 85a489cefa | |||
| 53b36a1aad | |||
| 25d3661599 | |||
| 2158c8b245 | |||
| 18e3e47ee4 | |||
| c6a400a2f1 | |||
| 30d94fd2a8 | |||
| fb8dd6d986 | |||
| 356ae08643 | |||
| f73359f83a | |||
| 45bf7146e5 | |||
| 5bec64d723 | |||
| a9c6ed4cdb | |||
| b9014eb6cb | |||
| 9dc1f1b89b | |||
| 7450b1c683 | |||
| 9d713d7449 | |||
| c1cc9d0a69 | |||
| 45edbded87 | |||
| d6ca39817d | |||
| 534736a3a4 | |||
| 8bac6c79a8 | |||
| 7fb8d0d59d | |||
| c9c62b1f3c | |||
| 53676ad997 | |||
| fa1d8d50a0 | |||
| a13404aaf2 | |||
| a558a97cba | |||
| 72b68e060c | |||
| 7a0403f9d6 | |||
| 916579c874 | |||
| b3be9e5f50 | |||
| 918100c949 | |||
| 7c1076e08d | |||
| a79de37ac9 | |||
| a16f19a26f | |||
| 0459a77e4d | |||
| 3a68c4b5af | |||
| d33f707cea | |||
| 942d8a0120 | |||
| d51201d6f2 | |||
| d3d51eabf5 | |||
| 287336e166 | |||
| 96ab340b1c | |||
| b105b08c8f | |||
| d7123e511e | |||
| 6e666b1435 | |||
| 8ffe61a94a | |||
| c9ce3f6716 | |||
| a84cdf33cd | |||
| d51363663b | |||
| b992bc4b73 | |||
| e3d1f0305a | |||
| 9736b2c6d4 | |||
| 435400b1e3 | |||
| 422066dd62 | |||
| 5cd95b54f5 | |||
| df71b86028 | |||
| 88a3fe5148 | |||
| d56ce24064 | |||
| 5f0bab8dd0 | |||
| 4be55a4133 | |||
| 5b4f495fc9 | |||
| 7af9b1469d | |||
| c1c0f5a43d | |||
| bce80a9101 | |||
| cd0c847606 | |||
| cc8729e889 | |||
| faf67dd7a8 | |||
| 990336017e | |||
| fe2e56bda0 | |||
| fcf578a6d9 | |||
| 4198136ce4 | |||
| 354b517200 | |||
| 902a6eb0c5 | |||
| 285cc80592 | |||
| 8b8541227d | |||
| b8701f9fc6 | |||
| a5550d5e64 | |||
| d9af8286b9 | |||
| 71914d8edb | |||
| ab1b52fb77 | |||
| 97affd54fc | |||
| aea3950d73 | |||
| 06f84cd9cf | |||
| 0f390ab493 | |||
| e2b242d40d | |||
| 81ad16ae8a | |||
| df4f4e2ccd | |||
| 21e499502f | |||
| b365826f0a | |||
| d4816291b4 | |||
| b7c242a32f | |||
| 74b16824fa | |||
| f779ca4cbf | |||
| f0660b4266 | |||
| 2e6e4ad2c7 | |||
| ea9931a147 | |||
| 59f0ff9228 | |||
| 5bf9d5ae9e | |||
| eed71286f2 | |||
| 36aad98485 | |||
| 6bc0451ed9 | |||
| 52e43638e7 | |||
| 8f17f18b2b | |||
| c704f8029b | |||
| 32a6cb5d30 | |||
| 54b3b649b8 | |||
| 3a48d6a7c2 | |||
| 1ab07a2f4b | |||
| 1a275d4585 | |||
| 8297551e7c | |||
| 93d04f3300 | |||
| f2b74caa7c | |||
| b451276d68 | |||
| aec5238456 | |||
| 5de45ff155 | |||
| 4ba401cd31 | |||
| 2c452ebd6a | |||
| b43733c811 | |||
| 7118de4198 | |||
| 7e0222402d | |||
| 1071caa062 | |||
| fa82978ead | |||
| aa5be1e0a0 | |||
| 2020ecfacb | |||
| ab1e2a9678 | |||
| 6af4ec193e | |||
| 2630891cd1 | |||
| 054a97055b | |||
| ac7f16f829 | |||
| f7a2bb5af8 | |||
| 49d0a5607e | |||
| a11db1db10 | |||
| 469f53bf6e | |||
| 2aaed1a575 | |||
| 2410ce024a | |||
| 77f3837fc8 | |||
| c4d63da884 | |||
| c7b03bde79 | |||
| 0f6c4b3b45 | |||
| 52912aff0a | |||
| 0104e888e7 | |||
| 370de9ec47 | |||
| d712dbf5e2 | |||
| 054dec5b68 | |||
| 838b63ca42 | |||
| 50da90af89 | |||
| 656ff69af3 | |||
| 3d8b9f8ba3 | |||
| f622ccda1d | |||
| 1c25ffe037 | |||
| 895f18181a | |||
| e2ef7874a6 | |||
| 4fecbaf8d8 | |||
| 0d65e9d0c0 | |||
| 3695a25428 | |||
| 9f30f0dafb | |||
| 1dce5f248a | |||
| e0e08f7071 | |||
| 73a6ab096f | |||
| 64c1ec4cc2 | |||
| 5d1f378397 | |||
| d00cb00a7c | |||
| 1c123e922d | |||
| 9fcc3c1ae6 | |||
| dc6c5b848b | |||
| 929f7c7147 | |||
| 33ce66eab1 | |||
| 3aa968248d | |||
| 2d0a524f86 | |||
| 6d4bfc196b | |||
| 906f9434f4 | |||
| 033b6df89b | |||
| 5c8530f53e | |||
| 74c6fd7b4e | |||
| f45c9d3ef8 | |||
| f7245936c7 | |||
| 91dc642dbd | |||
| 5955567b5e | |||
| 1e5dc00da8 | |||
| 0374f572e6 | |||
| 28855bcc48 | |||
| 2b0a127904 | |||
| c148a677e9 | |||
| 94abd5ce4f | |||
| c77448a932 | |||
| dc95fb4f60 | |||
| b52118d1b2 | |||
| 2107a1af45 | |||
| 465ee689d3 | |||
| 87e3ebfaa8 | |||
| dc6f825f2e | |||
| bc622083c7 | |||
| 7db22e3b42 | |||
| ac9650e397 | |||
| 1f80fc481d | |||
| 9f81101084 | |||
| 3634e3cf35 | |||
| a991546e79 | |||
| 7a2f86a5c3 | |||
| 48e7149540 | |||
| 09ea3b95ab | |||
| 714d37deca | |||
| 0b6345ff16 | |||
| 4fbe758165 | |||
| 729570c8b7 | |||
| b8215a9081 | |||
| 63b3c4d1dd | |||
| 509a59b943 | |||
| 56616183f1 | |||
| 3eb69151a6 | |||
| a1d345dfb2 | |||
| ba0ee5fdf9 | |||
| e8b735e1cb | |||
| e4f0373661 | |||
| 78753dff08 | |||
| be8e506747 | |||
| 9d65b02554 | |||
| 79f4d68383 | |||
| 0b71193526 | |||
| 4c39d7fd74 | |||
| f1fe45e9c9 | |||
| f2a0b612d0 | |||
| 586f2b1eca | |||
| 59fcfbf78d | |||
| 7aab7c74f6 | |||
| e4f40f04ff | |||
| 068b955f6e | |||
| cad10b73de | |||
| 316d4c8a2a | |||
| 8f3ccdeabc | |||
| 4071d7e0a3 | |||
| f13984cbf3 | |||
| b696c71152 | |||
| cee9c7f35b | |||
| f54c7ec51f | |||
| 5aecfb47bd | |||
| 0068b2f085 | |||
| fe645e7d4c | |||
| 5e4dc8c4bd | |||
| 9930cee309 | |||
| 6a8775e31b | |||
| 0265eea7bf | |||
| 84dfdbca8a | |||
| d7fbbc3217 | |||
| 1d51353849 | |||
| 6d4f5b2e3f | |||
| aa1ac369cc | |||
| 68bd420211 | |||
| 8137ac1c9b | |||
| c20787f93e | |||
| cc730cf82a | |||
| 0a46c1ed52 | |||
| 7b850027fc | |||
| 6c87d3c1c6 | |||
| 1a81940085 | |||
| 44bf41e60a | |||
| c0ae8f3e4f | |||
| 1aab5f70a9 | |||
| 84440f09ed | |||
| 51b579ed3a | |||
| 50badbf7fe | |||
| 4c91d3f341 | |||
| dce6e3aed2 | |||
| 7ec18693b4 | |||
| 4a53e4b1bb | |||
| f9f84378d6 | |||
| e56876b56f | |||
| 3fa849e3bc | |||
| 3c0251ca1f | |||
| 13c5b92e53 | |||
| 8e8c5f4e04 | |||
| d8f35f30df | |||
| 626009c2b8 | |||
| d15b1b4067 | |||
| e26a0df1bd | |||
| dbe6469e9d | |||
| a33437abea | |||
| 688a77ddb5 | |||
| b811d73f26 | |||
| ec6ddd1fff | |||
| 5bb4bc5941 | |||
| f972d64eea | |||
| 2f44f90364 | |||
| 89261f27d8 | |||
| eaf2f38940 | |||
| ef9fc9c071 | |||
| f051eb2286 | |||
| 3a2f9c1542 | |||
| d6ac14eb93 | |||
| 60acdce555 | |||
| 0eab454d5d | |||
| b52f3dee07 | |||
| da5158f757 | |||
| 9a9590299d | |||
| ab029a759f | |||
| cc291b1fb5 | |||
| 38ace117ea | |||
| aacd516cf3 | |||
| ceddfa6cdc | |||
| 0dc6166b59 | |||
| 568e4e72d8 | |||
| a92941e568 | |||
| 78e97abb3b | |||
| 0b8145d5ff | |||
| 3eac9ef6ac | |||
| 7bcac45214 | |||
| a3d6a50489 | |||
| 3bab21d1e8 | |||
| 5fc46bf634 | |||
| 4b2ce15fc9 | |||
| bdc987e878 | |||
| 2f9dd15c0d | |||
| 5becf826d2 | |||
| 33fef62417 | |||
| c7aae10231 | |||
| 5dad0c7995 | |||
| a46550204b | |||
| de21432d07 | |||
| fc4cee3767 | |||
| 6635e6556d | |||
| 705fd4bbc6 | |||
| 6f24317c94 | |||
| 2e698a54e8 | |||
| 2c0f58bb07 | |||
| 6ba81683ff | |||
| 31506af8fc | |||
| 32dcc7ee84 | |||
| 11e5e392c3 | |||
| abee586e5a | |||
| 0a50fc3488 | |||
| 3413150177 | |||
| c06e3a9081 | |||
| 9127bda6b3 | |||
| 7aef4156c4 | |||
| 98501762a9 | |||
| 1e6471910b | |||
| 61d258d939 | |||
| b85e5680cf | |||
| ef8ca86f30 | |||
| a00297131b | |||
| b6d853759c | |||
| 100a7880d4 | |||
| 552287ddf8 | |||
| b9fa284644 | |||
| 82d512b602 | |||
| 9e6ff789a7 | |||
| 6a6e20ad57 | |||
| d047e1e5ea | |||
| 7713d92610 | |||
| dfba7103ff | |||
| ce8f0dd497 | |||
| fa354bf041 | |||
| 4a688a284d | |||
| 6dda5fb226 | |||
| c5eaf43a3d | |||
| 4a0241eded | |||
| 678d022bb6 | |||
| 42ee915aaf | |||
| 0a1ad4d091 | |||
| 2d76396040 | |||
| 9d0d7bd96e | |||
| 689d35a4af | |||
| 210d5c7f4d | |||
| ea26241a15 | |||
| 5627a2e941 | |||
| 96259c6c5d | |||
| 816e400b31 | |||
| c57eb2e9aa | |||
| f8d679993a | |||
| 0b70d7ebec | |||
| 0019fee34e | |||
| 406a3dba56 | |||
| f41018c60b | |||
| 8e9bd99e91 | |||
| be40644497 | |||
| a7657b4a5c | |||
| b7c565de79 | |||
| af955bbf55 | |||
| 851d6ef020 | |||
| e9bc40afb5 | |||
| f17471b94d | |||
| 48b11de774 | |||
| cf5d0b9571 | |||
| 591c9a6ccf | |||
| c7b39c9ed3 | |||
| 55fd1d4bdc | |||
| 7b2094a543 | |||
| 41e708e302 | |||
| 5ccc360345 | |||
| f468ca73c3 | |||
| 8972a96afd | |||
| 5ec1f21bd3 | |||
| 0b5653fab5 | |||
| a8c5d29858 | |||
| 89fa8236e8 | |||
| da197be2f5 | |||
| 3eaab3b427 | |||
| 3b252476c4 | |||
| c00d7a9eef | |||
| 370b0c4020 | |||
| 3aaef5f730 | |||
| 9e98da038c | |||
| 9045e2983d | |||
| a1931f5e36 | |||
| 70866420e5 | |||
| c38788aa3b | |||
| 922db70107 | |||
| bee6a40e9f | |||
| d7e86ddf29 | |||
| 4e064dba96 | |||
| 862ff7cdc1 | |||
| d02fad1164 | |||
| dbb005d26c | |||
| 6d3e5823fc | |||
| 65fc779d58 | |||
| 49a867dd37 | |||
| 03e1de1d14 | |||
| 202c7f5ad4 | |||
| 6f66181c17 | |||
| f806b3f96d | |||
| b42deb737c | |||
| e247cb102c | |||
| e1f1d84201 | |||
| dd5e594dc9 | |||
| c53cd84722 | |||
| 84ceae05c3 | |||
| 6dc2747a9b | |||
| 24fed8ff3f | |||
| 3a1d42025f | |||
| 5f01287685 | |||
| 954aeef366 | |||
| e379533e81 | |||
| 4caab8f7ea | |||
| 37a31f0ecb | |||
| cd2c284ca7 | |||
| 205c365ee2 | |||
| ec44bc93f2 | |||
| f0aa9a3bc9 | |||
| c45886dc18 | |||
| 336fd22150 | |||
| 52d9f92b62 | |||
| 0fa9cb5d47 | |||
| f145064b14 | |||
| cb06f2d731 | |||
| cbdd332de0 | |||
| ffbc8e560d | |||
| 57cdfcf7bd | |||
| 7bcdcf5987 | |||
| 48729306f1 | |||
| 48e324b768 | |||
| 7d6221327d | |||
| 17fd577a22 | |||
| 273c81242c | |||
| 97e05e01e4 | |||
| 908d933ab8 | |||
| 70ef926b70 | |||
| d2e3227d01 |
@ -1,8 +1,4 @@
|
||||
node_modules
|
||||
git
|
||||
.gitignore
|
||||
.dockerignore
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
README.md
|
||||
LICENSE
|
||||
.cache/
|
||||
node_modules/
|
||||
functions/
|
||||
public/
|
||||
8
.env.example
Normal file
@ -0,0 +1,8 @@
|
||||
FIREBASE_APIKEY=""
|
||||
FIREBASE_APPID=""
|
||||
FIREBASE_AUTHDOMAIN=""
|
||||
FIREBASE_DATABASEURL=""
|
||||
FIREBASE_MEASUREMENTID=""
|
||||
FIREBASE_MESSAGINGSENDERID=""
|
||||
FIREBASE_PROJECTID=""
|
||||
FIREBASE_STORAGEBUCKET=""
|
||||
4
.eslintignore
Normal file
@ -0,0 +1,4 @@
|
||||
.cache
|
||||
package.json
|
||||
package-lock.json
|
||||
public
|
||||
39
.eslintrc
@ -1,29 +1,36 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"allowImportExportEverywhere": false,
|
||||
"codeFrame": false
|
||||
"globals": {
|
||||
"atob": true,
|
||||
"Blob": true,
|
||||
"fetch": true,
|
||||
"window": true,
|
||||
"document": true,
|
||||
"FileReader": true,
|
||||
"localStorage": true
|
||||
},
|
||||
"extends": [
|
||||
"airbnb",
|
||||
"plugin:react/recommended",
|
||||
"prettier",
|
||||
"prettier/react"
|
||||
"plugin:jest/recommended",
|
||||
"plugin:jest/style",
|
||||
"prettier"
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"jest": true
|
||||
},
|
||||
"plugins": ["jest", "prettier", "sort-imports-es6-autofix"],
|
||||
"rules": {
|
||||
"jsx-a11y/no-static-element-interactions": 0,
|
||||
"jsx-a11y/click-events-have-key-events": 0,
|
||||
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
|
||||
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
|
||||
"jsx-a11y/label-has-associated-control": 0,
|
||||
"react/jsx-filename-extension": 0,
|
||||
"react/jsx-one-expression-per-line": 0,
|
||||
"react/jsx-props-no-spreading": 0,
|
||||
"prettier/prettier": ["error"],
|
||||
"react/no-array-index-key": 0,
|
||||
"jsx-a11y/anchor-is-valid": 0,
|
||||
"react/button-has-type": 0,
|
||||
"no-unused-expressions": 0,
|
||||
"no-restricted-syntax": 0,
|
||||
"no-param-reassign": 0,
|
||||
"consistent-return": 0,
|
||||
"no-nested-ternary": 0,
|
||||
"react/prop-types": 0,
|
||||
"no-plusplus": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,11 +5,8 @@
|
||||
"targets": {
|
||||
"rx-resume": {
|
||||
"hosting": {
|
||||
"app": [
|
||||
"rx-resume"
|
||||
],
|
||||
"docs": [
|
||||
"docs-rx-resume"
|
||||
"rxresume": [
|
||||
"public"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
4
.github/FUNDING.yml
vendored
@ -1 +1,3 @@
|
||||
open_collective: reactive-resume
|
||||
# These are supported funding model platforms
|
||||
|
||||
custom: ['buymeacoffee.com/AmruthPillai']
|
||||
|
||||
31
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@ -1,31 +0,0 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Create a report to help us improve
|
||||
title: "[Bug] "
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the Bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Reproduction**
|
||||
Steps to reproduce the behaviour:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected Behaviour**
|
||||
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/macOS/Windows 10]
|
||||
- Browser [e.g. Chrome/Safari/Firefox]
|
||||
|
||||
**Additional Context**
|
||||
Add any other context about the problem here.
|
||||
21
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@ -1,21 +0,0 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea for this project
|
||||
title: "[Feature] "
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is.
|
||||
Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
34
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
name: CodeQL Analysis
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
pull_request:
|
||||
branches: [ develop ]
|
||||
schedule:
|
||||
- cron: '37 16 * * 0'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
25
.github/workflows/main.yml
vendored
@ -1,25 +0,0 @@
|
||||
name: Build & Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build & Deploy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout GitHub Repository
|
||||
uses: actions/checkout@v2.0.0
|
||||
- name: Install Project Dependencies
|
||||
run: npm install
|
||||
- name: Build App
|
||||
run: npm run build
|
||||
- name: Build Documentation
|
||||
run: npm run docs:build
|
||||
- name: Deploy to Firebase Hosting
|
||||
uses: w9jds/firebase-action@v1.3.0
|
||||
with:
|
||||
args: deploy --only hosting
|
||||
env:
|
||||
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
|
||||
27
.github/workflows/run-tests.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
name: Run Unit Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
pull_request:
|
||||
branches: [ develop ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
100
.gitignore
vendored
@ -1,30 +1,76 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# firebase
|
||||
.firebase
|
||||
|
||||
# tailwind
|
||||
tailwind.css
|
||||
|
||||
# vuepress
|
||||
docs/.vuepress/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Coverage directory used by Jest
|
||||
test-coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
|
||||
# gatsby files
|
||||
.cache/
|
||||
public
|
||||
|
||||
# Firebase Files
|
||||
.firebase
|
||||
|
||||
# Mac files
|
||||
.DS_Store
|
||||
|
||||
# Yarn
|
||||
yarn-error.log
|
||||
.pnp/
|
||||
.pnp.js
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
1
.husky/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
_
|
||||
4
.husky/pre-commit
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
5
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
12
.idea/Reactive-Resume.iml
generated
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
58
.idea/codeStyles/Project.xml
generated
Normal file
@ -0,0 +1,58 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<HTMLCodeStyleSettings>
|
||||
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
<option name="HTML_ENFORCE_QUOTES" value="true" />
|
||||
</HTMLCodeStyleSettings>
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<TypeScriptCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<VueCodeStyleSettings>
|
||||
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||
</VueCodeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Vue">
|
||||
<option name="SOFT_MARGINS" value="80" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
7
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/Reactive-Resume.iml" filepath="$PROJECT_DIR$/.idea/Reactive-Resume.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
||||
.cache
|
||||
package.json
|
||||
package-lock.json
|
||||
public
|
||||
@ -1,7 +1,5 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true
|
||||
}
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
|
||||
13
.vscode/settings.json
vendored
@ -1,4 +1,11 @@
|
||||
{
|
||||
"i18n-ally.localesPaths": "src/i18n",
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
||||
"files.associations": {
|
||||
"*.js": "javascriptreact"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
},
|
||||
"i18n-ally.localesPaths": ["src/i18n/locales"],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"css.validate": false
|
||||
}
|
||||
|
||||
65
DEPLOYMENT.md
Normal file
@ -0,0 +1,65 @@
|
||||
# Deployment
|
||||
|
||||
This is a guide on how to build the source from scratch, along with setting up Firebase and related cloud functions to be able to export PDFs just like the original deployment of [rxresu.me](http://rxresu.me/).
|
||||
|
||||
### Requirements
|
||||
|
||||
- A Firebase project
|
||||
- Works on both Linux, macOS and Windows
|
||||
- Requires Node.js & NPM installed on the machine
|
||||
|
||||
### Setting up Firebase
|
||||
|
||||
1. Create a new Firebase project by visiting [Firebase Console](https://console.firebase.google.com/) and clicking on `Add Project`
|
||||
|
||||

|
||||
|
||||
2. Disable Google Analytics, or keep it enabled as per your requirements. Most people wouldn't need it.
|
||||
|
||||

|
||||
|
||||
3. Wait until Project is created, then click on Continue
|
||||
|
||||

|
||||
|
||||
4. Navigate to Realtime Database, and click on `Create Database`
|
||||
|
||||

|
||||
|
||||
5. Select any location that's nearby to you, and most importantly, create the database in `Test Mode` and click on Enable
|
||||
|
||||

|
||||
|
||||
6. Go back to Project Overview and click on `Web` and skip through every other step by clicking `Next`.
|
||||
|
||||

|
||||
|
||||
7. Copy configuration variables of your project, or keep this page open as you will need it later
|
||||
|
||||

|
||||
|
||||
### Cloning the Repository
|
||||
|
||||
1. Run this command on your machine's terminal or Command Prompt
|
||||
|
||||
```
|
||||
git clone git@github.com:AmruthPillai/Reactive-Resume.git
|
||||
```
|
||||
|
||||
<img width="550" alt="Screenshot 2021-03-13 at 10 38 16 AM" src="https://user-images.githubusercontent.com/1134738/111019919-3df43d00-83e8-11eb-8d6b-d9fe0cc74a3a.png">
|
||||
|
||||
2. Copy the file `.env.example` to `.env` and start editing the file
|
||||
|
||||
```
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
<img width="317" alt="Screenshot 2021-03-13 at 10 50 21 AM" src="https://user-images.githubusercontent.com/1134738/111020166-ed7ddf00-83e9-11eb-9cbb-a8732243bbd5.png">
|
||||
|
||||
3. Copy configuration variables from last step to the .env file, it's fine to have `FIREBASE_MEASUREMENTID` empty if you had Google Analytics disabled.
|
||||
|
||||
<img width="696" alt="Screenshot 2021-03-13 at 10 51 53 AM" src="https://user-images.githubusercontent.com/1134738/111020217-3c2b7900-83ea-11eb-801d-d8719cf23608.png">
|
||||
|
||||
4. Run `npm install` on the terminal/command prompt
|
||||
|
||||
5. After that's done, run `npm run build` and allow some time for the process to build
|
||||
63
Dockerfile
@ -1,39 +1,38 @@
|
||||
## build image
|
||||
FROM node:13.12.0-alpine as build
|
||||
FROM node:alpine as builder
|
||||
WORKDIR /app
|
||||
|
||||
## set working directory
|
||||
WORKDIR /usr/src/app
|
||||
RUN apk add --update --no-cache \
|
||||
g++ \
|
||||
yasm \
|
||||
bash \
|
||||
make \
|
||||
automake \
|
||||
autoconf \
|
||||
libtool \
|
||||
zlib-dev \
|
||||
libpng-dev
|
||||
|
||||
## add `/usr/src/app/node_modules/.bin` to $PATH
|
||||
ENV PATH /usr/src/app/node_modules/.bin:$PATH
|
||||
RUN apk add --update --no-cache \
|
||||
--repository http://dl-3.alpinelinux.org/alpine/edge/community \
|
||||
--repository http://dl-3.alpinelinux.org/alpine/edge/main \
|
||||
vips-dev
|
||||
|
||||
## install and cache app dependencies
|
||||
COPY package.json /usr/src/app/package.json
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
## install git
|
||||
RUN apk add --no-cache git
|
||||
ARG FIREBASE_APIKEY
|
||||
ARG FIREBASE_APPID
|
||||
ARG FIREBASE_AUTHDOMAIN
|
||||
ARG FIREBASE_DATABASEURL
|
||||
ARG FIREBASE_MEASUREMENTID
|
||||
ARG FIREBASE_MESSAGINGSENDERID
|
||||
ARG FIREBASE_PROJECTID
|
||||
ARG FIREBASE_STORAGEBUCKET
|
||||
|
||||
## install app dependencies
|
||||
RUN npm install
|
||||
|
||||
## copy files
|
||||
COPY . /usr/src/app
|
||||
|
||||
## build production app
|
||||
COPY . ./
|
||||
RUN npm run build
|
||||
|
||||
## production environment
|
||||
FROM nginx:1.17.9-alpine
|
||||
|
||||
## copy build artifacts to nginx
|
||||
COPY --from=build /usr/src/app/build /usr/share/nginx/html
|
||||
|
||||
## copy custom nginx config
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
COPY nginx/nginx.conf /etc/nginx/conf.d
|
||||
|
||||
## export port 80
|
||||
EXPOSE 80
|
||||
|
||||
## run nginx server
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
FROM nginx:alpine
|
||||
RUN rm -rf /usr/share/nginx/html
|
||||
COPY --from=builder /app/public/ /usr/share/nginx/html
|
||||
COPY server.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
## base image
|
||||
FROM node:13.12.0-alpine
|
||||
|
||||
## set working directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
## add `/usr/src/app/node_modules/.bin` to $PATH
|
||||
ENV PATH /usr/src/app/node_modules/.bin:$PATH
|
||||
|
||||
## install and cache app dependencies
|
||||
COPY package.json /usr/src/app/package.json
|
||||
|
||||
## install git
|
||||
RUN apk add --no-cache git
|
||||
|
||||
## install app dependencies
|
||||
RUN npm install
|
||||
|
||||
## start app
|
||||
CMD ["npm", "start"]
|
||||
2
LICENSE
@ -18,4 +18,4 @@ 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.
|
||||
SOFTWARE.
|
||||
141
README.md
@ -1,15 +1,138 @@
|
||||
<img src="https://i.imgur.com/4eps4gP.png" alt="Reactive Resume" width="256px" height="256px"/>
|
||||
<img src="https://raw.githubusercontent.com/AmruthPillai/Reactive-Resume/develop/static/images/logo.png" width="256px" />
|
||||
|
||||
# Reactive Resume
|
||||
## A free and open source resume builder.
|
||||
|
||||
[](https://github.com/AmruthPillai/Reactive-Resume/actions)
|
||||

|
||||
[](https://hub.docker.com/r/amruthpillai/reactive-resume)
|
||||
[](https://crowdin.com/project/reactive-resume)
|
||||
[](https://crowdin.com/project/reactive-resume)
|
||||
[](https://github.com/AmruthPillai/Reactive-Resume/blob/develop/LICENSE)
|
||||
|
||||
#### A Free and Open-Source Resume Builder That Respects Your Privacy
|
||||
### [Go to App](https://rxresu.me/)
|
||||
|
||||
Welcome to the front page of **Reactive Resume**, a free and open-source Resume Builder web app that focuses on one thing, **Privacy**. And also few other important features such as minimalistic UI/UX, customizability, portability, regularly updated templates, etc. But the important thing is that, your personal data is yours alone.
|
||||
### What is this app all about?
|
||||
|
||||
### [Go to App](https://rxresu.me/) | [Documentation](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.
|
||||
|
||||
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.
|
||||
|
||||
### Features
|
||||
|
||||
- Manage multiple resumes with one account
|
||||
- Sync your data across devices
|
||||
- Sign in with Google, or sign in anonymously just to test the app
|
||||
- Send your resume to others with a unique sharable link
|
||||
- Choose from 6 vibrant templates and more coming soon
|
||||
- Structure sections and change layouts the way you want to
|
||||
- Rename sections according to your language/industry
|
||||
- Mix and match colors to any degree, even a dark mode resume?
|
||||
- Pick from a variety of crisp and clear fonts
|
||||
- Easy to translate to your own language
|
||||
- Import your existing [JSON Resume](https://jsonresume.org/) in one click
|
||||
- No advertisements, no data sharing, no marketing emails
|
||||
- **Everything is free, and there’s no catch!**
|
||||
|
||||
### Screenshots
|
||||
|
||||
<img src="https://raw.githubusercontent.com/AmruthPillai/Reactive-Resume/develop/static/images/screenshots/screen-1.png" width="400px" />
|
||||
|
||||
<img src="https://raw.githubusercontent.com/AmruthPillai/Reactive-Resume/develop/static/images/screenshots/screen-3.png" width="400px" />
|
||||
|
||||
<img src="https://raw.githubusercontent.com/AmruthPillai/Reactive-Resume/develop/static/images/screenshots/screen-5.png" width="400px" />
|
||||
|
||||
### Translation
|
||||
|
||||
To translate the app, just fork the repository, go to `src/i18n/locales` and duplicate the `en.json` file to a new file `your-lang-code.json` and translate all of the strings inside. It's a simple process that would take just a few minutes, and by contributing, your name could also be added down below as a contributor.
|
||||
|
||||
For those of you familiar with the Crowdin Platform, you could do that too and just head to http://crowdin.com/project/reactive-resume/ to translate the app over there. They have a great interface that helps you navigate through various strings and manage translations.
|
||||
|
||||
##### Languages Currently Supported
|
||||
|
||||
- Arabic (عربى)
|
||||
- Bengali (বাংলা)
|
||||
- Czech (čeština)
|
||||
- Chinese Simplified (简体中文)
|
||||
- Danish (Dansk)
|
||||
- Dutch (Nederlands)
|
||||
- English (US)
|
||||
- Finnish (Suomalainen)
|
||||
- French (Français)
|
||||
- German (Deutsch)
|
||||
- Greek (Ελληνικά)
|
||||
- Hebrew (עִברִית)
|
||||
- Hindi (हिंदी)
|
||||
- Indonesian (Bahasa Indonesia)
|
||||
- Italian (Italiano)
|
||||
- Japanese (日本人)
|
||||
- Kannada (ಕನ್ನಡ)
|
||||
- Lithuanian (Lietuvių)
|
||||
- Norwegian (Norsk)
|
||||
- Persian (Farsi)
|
||||
- Polish (Polskie)
|
||||
- Portuguese (Brazilian)
|
||||
- Portuguese (Portugal)
|
||||
- Romanian (Română)
|
||||
- Russian (русский)
|
||||
- Slovak (Slovenčina)
|
||||
- Spanish (Español)
|
||||
- Swedish (Svenska)
|
||||
- Turkish (Türkçe)
|
||||
- Ukrainian (Українська)
|
||||
|
||||
Thank you to all the amazing people who have contributed to Reactive Resume by translating it into their native language.
|
||||
|
||||
### Building from Source
|
||||
|
||||
Want to run your own instance of Reactive Resume? You are very much free to do so. The requirements to build from source are:
|
||||
|
||||
- NodeJS/NPM
|
||||
- A Firebase Project
|
||||
|
||||
1. First, clone this project repository
|
||||
|
||||
```
|
||||
git clone https://github.com/AmruthPillai/Reactive-Resume.git
|
||||
cd Reactive-Resume
|
||||
```
|
||||
|
||||
2. Run npm install to install dependencies for the project
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Create a `.env` file and fill it with your Firebase credentials
|
||||
|
||||
You can get these by setting up a firebase web app [here](https://console.firebase.google.com/u/0/).
|
||||
|
||||
Also note that you'll need to set up a Realtime Database, _not_ a Firestore Database, to get the correct value for `FIREBASE_DATABASEURL`. Be sure to set it to test mode so you can read/write data. Just remember to either revert these or remove the database after your testing is completed.
|
||||
|
||||
```
|
||||
FIREBASE_APIKEY=""
|
||||
FIREBASE_APPID=""
|
||||
FIREBASE_AUTHDOMAIN=""
|
||||
FIREBASE_DATABASEURL=""
|
||||
FIREBASE_MEASUREMENTID=""
|
||||
FIREBASE_MESSAGINGSENDERID=""
|
||||
FIREBASE_PROJECTID=""
|
||||
FIREBASE_STORAGEBUCKET=""
|
||||
```
|
||||
|
||||
4. Run `npm run start` to run the development server or `npm run build` to build the production app.
|
||||
|
||||
And that's it! 🎉
|
||||
|
||||
### Donation
|
||||
|
||||
I try to do what I can, but if you found the app helpful, or you're in a better position than the others who depend on this project for their first job, please consider donating as little as \$5 to help keep the project alive :)
|
||||
|
||||
#### https://www.buymeacoffee.com/AmruthPillai
|
||||
|
||||

|
||||
|
||||
### Appreciation
|
||||
|
||||
Thank you to everyone who made this project possible, including the many users who voiced their opinions, created issues and PRs to the original Reactive Resume project, and helped me along the way to make this a reality.
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
###### Made with Love by [Amruth Pillai](https://amruthpillai.com/)
|
||||
|
||||
12
SECURITY.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 2.0.0 | :white_check_mark: |
|
||||
| 1.0.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Use the [GitHub Issues](https://github.com/AmruthPillai/Reactive-Resume/issues) tab to report a vulnerability
|
||||
17
__mocks__/@reach/router.js
Normal file
@ -0,0 +1,17 @@
|
||||
import { delay } from '../../src/utils/index';
|
||||
|
||||
const ReachRouter = jest.requireActual('@reach/router');
|
||||
|
||||
const defaultDelayInMilliseconds = 100;
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const navigate = async (to, options) => {
|
||||
await delay(defaultDelayInMilliseconds);
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
...ReachRouter,
|
||||
navigate: jest.fn(navigate),
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
import FirebaseStub, { AuthConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('sets current user to anonymous user 1', async () => {
|
||||
await FirebaseStub.auth().signInAnonymously();
|
||||
|
||||
const { currentUser } = FirebaseStub.auth();
|
||||
expect(currentUser).toBeTruthy();
|
||||
expect(currentUser.uid).toEqual(AuthConstants.anonymousUser1.uid);
|
||||
});
|
||||
|
||||
test('calls onAuthStateChanged observer with anonymous user 1', async () => {
|
||||
let user = null;
|
||||
let error = null;
|
||||
FirebaseStub.auth().onAuthStateChanged(
|
||||
(_user) => {
|
||||
user = _user;
|
||||
},
|
||||
(_error) => {
|
||||
error = _error;
|
||||
},
|
||||
);
|
||||
|
||||
await FirebaseStub.auth().signInAnonymously();
|
||||
|
||||
expect(user).toBeTruthy();
|
||||
expect(user.uid).toEqual(AuthConstants.anonymousUser1.uid);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
test('returns anonymous user 1', async () => {
|
||||
const user = await FirebaseStub.auth().signInAnonymously();
|
||||
|
||||
expect(user).toBeTruthy();
|
||||
expect(user.uid).toEqual(AuthConstants.anonymousUser1.uid);
|
||||
});
|
||||
@ -0,0 +1,54 @@
|
||||
import FirebaseStub, { AuthConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
describe('with Google auth provider', () => {
|
||||
test('sets current user to Google user 3', async () => {
|
||||
await FirebaseStub.auth().signInWithPopup(
|
||||
new FirebaseStub.auth.GoogleAuthProvider(),
|
||||
);
|
||||
|
||||
const { currentUser } = FirebaseStub.auth();
|
||||
expect(currentUser).toBeTruthy();
|
||||
expect(currentUser.uid).toEqual(AuthConstants.googleUser3.uid);
|
||||
});
|
||||
|
||||
test('sets current user provider data', async () => {
|
||||
const provider = new FirebaseStub.auth.GoogleAuthProvider();
|
||||
await FirebaseStub.auth().signInWithPopup(provider);
|
||||
|
||||
const { currentUser } = FirebaseStub.auth();
|
||||
expect(currentUser).toBeTruthy();
|
||||
expect(currentUser.providerData).toBeTruthy();
|
||||
expect(currentUser.providerData).toHaveLength(1);
|
||||
expect(currentUser.providerData[0].providerId).toEqual(provider.providerId);
|
||||
});
|
||||
|
||||
test('calls onAuthStateChanged observer with Google user 3', async () => {
|
||||
let user = null;
|
||||
let error = null;
|
||||
FirebaseStub.auth().onAuthStateChanged(
|
||||
(_user) => {
|
||||
user = _user;
|
||||
},
|
||||
(_error) => {
|
||||
error = _error;
|
||||
},
|
||||
);
|
||||
|
||||
await FirebaseStub.auth().signInWithPopup(
|
||||
new FirebaseStub.auth.GoogleAuthProvider(),
|
||||
);
|
||||
|
||||
expect(user).toBeTruthy();
|
||||
expect(user.uid).toEqual(AuthConstants.googleUser3.uid);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
test('returns Google user 3', async () => {
|
||||
const user = await FirebaseStub.auth().signInWithPopup(
|
||||
new FirebaseStub.auth.GoogleAuthProvider(),
|
||||
);
|
||||
|
||||
expect(user).toBeTruthy();
|
||||
expect(user.uid).toEqual(AuthConstants.googleUser3.uid);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,29 @@
|
||||
import FirebaseStub from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('sets current user to null', async () => {
|
||||
await FirebaseStub.auth().signInAnonymously();
|
||||
|
||||
await FirebaseStub.auth().signOut();
|
||||
|
||||
const { currentUser } = FirebaseStub.auth();
|
||||
expect(currentUser).toBeNull();
|
||||
});
|
||||
|
||||
test('calls onAuthStateChanged observer with null', async () => {
|
||||
let user = null;
|
||||
let error = null;
|
||||
FirebaseStub.auth().onAuthStateChanged(
|
||||
(_user) => {
|
||||
user = _user;
|
||||
},
|
||||
(_error) => {
|
||||
error = _error;
|
||||
},
|
||||
);
|
||||
await FirebaseStub.auth().signInAnonymously();
|
||||
|
||||
await FirebaseStub.auth().signOut();
|
||||
|
||||
expect(user).toBeNull();
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
35
__mocks__/__tests__/gatsby-plugin-firebase/auth.test.js
Normal file
@ -0,0 +1,35 @@
|
||||
import FirebaseStub from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('reuses existing Auth instance', () => {
|
||||
const auth1 = FirebaseStub.auth();
|
||||
const auth2 = FirebaseStub.auth();
|
||||
|
||||
expect(auth1.uuid).toBeTruthy();
|
||||
expect(auth2.uuid).toBeTruthy();
|
||||
expect(auth1.uuid).toEqual(auth2.uuid);
|
||||
});
|
||||
|
||||
test('onAuthStateChanged unsubscribe removes observer', () => {
|
||||
const observer = () => {};
|
||||
const unsubscribe = FirebaseStub.auth().onAuthStateChanged(observer);
|
||||
expect(unsubscribe).toBeTruthy();
|
||||
expect(
|
||||
FirebaseStub.auth().onAuthStateChangedObservers.indexOf(observer),
|
||||
).toBeGreaterThanOrEqual(0);
|
||||
|
||||
unsubscribe();
|
||||
|
||||
expect(
|
||||
FirebaseStub.auth().onAuthStateChangedObservers.indexOf(observer),
|
||||
).not.toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('current user delete calls signOut', async () => {
|
||||
const mockSignOut = jest.spyOn(FirebaseStub.auth(), 'signOut');
|
||||
await FirebaseStub.auth().signInAnonymously();
|
||||
const { currentUser } = FirebaseStub.auth();
|
||||
|
||||
await currentUser.delete();
|
||||
|
||||
expect(mockSignOut).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@ -0,0 +1,53 @@
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import FirebaseStub, { DatabaseConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('can filter resumes by user', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
let snapshotValue = null;
|
||||
|
||||
FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.orderByChild('user')
|
||||
.equalTo(DatabaseConstants.user1.uid)
|
||||
.on('value', (snapshot) => {
|
||||
snapshotValue = snapshot.val();
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
snapshotValue ? Promise.resolve(true) : Promise.reject(),
|
||||
);
|
||||
|
||||
expect(snapshotValue).not.toBeNull();
|
||||
expect(Object.keys(snapshotValue)).toHaveLength(2);
|
||||
Object.values(snapshotValue).forEach((resume) =>
|
||||
expect(resume.user).toEqual(DatabaseConstants.user1.uid),
|
||||
);
|
||||
});
|
||||
|
||||
test('previously set query parameters are not kept when retrieving reference again', () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
let reference = null;
|
||||
|
||||
reference = FirebaseStub.database().ref(DatabaseConstants.resumesPath);
|
||||
expect(reference).toBeTruthy();
|
||||
const { uuid } = reference;
|
||||
expect(reference.orderByChildPath).toHaveLength(0);
|
||||
expect(reference.equalToValue).toHaveLength(0);
|
||||
|
||||
reference = FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.orderByChild('user')
|
||||
.equalTo('testuser1');
|
||||
expect(reference).toBeTruthy();
|
||||
expect(reference.uuid).toBe(uuid);
|
||||
expect(reference.orderByChildPath).toBe('user');
|
||||
expect(reference.equalToValue).toBe('testuser1');
|
||||
|
||||
reference = FirebaseStub.database().ref(DatabaseConstants.resumesPath);
|
||||
expect(reference).toBeTruthy();
|
||||
expect(reference.uuid).toBe(uuid);
|
||||
expect(reference.orderByChildPath).toHaveLength(0);
|
||||
expect(reference.equalToValue).toHaveLength(0);
|
||||
});
|
||||
@ -0,0 +1,51 @@
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import FirebaseStub, { DatabaseConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('removes event callbacks', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const userUid = DatabaseConstants.user1.uid;
|
||||
|
||||
let valueCallbackSnapshotValue = null;
|
||||
const valueCallback = jest.fn((snapshot) => {
|
||||
valueCallbackSnapshotValue = snapshot.val();
|
||||
});
|
||||
FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.orderByChild('user')
|
||||
.equalTo(userUid)
|
||||
.on('value', valueCallback);
|
||||
await waitFor(() => valueCallback.mock.calls[0][0]);
|
||||
valueCallback.mockClear();
|
||||
valueCallbackSnapshotValue = null;
|
||||
|
||||
let childRemovedCallbackSnapshotValue = null;
|
||||
const childRemovedCallback = jest.fn((snapshot) => {
|
||||
childRemovedCallbackSnapshotValue = snapshot.val();
|
||||
});
|
||||
FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.orderByChild('user')
|
||||
.equalTo(userUid)
|
||||
.on('child_removed', childRemovedCallback);
|
||||
|
||||
const removedResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(
|
||||
`${DatabaseConstants.resumesPath}/${DatabaseConstants.demoStateResume1Id}`,
|
||||
)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(removedResume).toBeTruthy();
|
||||
|
||||
FirebaseStub.database().ref(DatabaseConstants.resumesPath).off();
|
||||
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${removedResume.id}`)
|
||||
.remove();
|
||||
|
||||
expect(childRemovedCallback.mock.calls).toHaveLength(0);
|
||||
expect(childRemovedCallbackSnapshotValue).toBeNull();
|
||||
expect(valueCallback.mock.calls).toHaveLength(0);
|
||||
expect(valueCallbackSnapshotValue).toBeNull();
|
||||
});
|
||||
@ -0,0 +1,45 @@
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import FirebaseStub, { DatabaseConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('triggers event with true if on the connected reference path', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
let snapshotValue = null;
|
||||
|
||||
FirebaseStub.database()
|
||||
.ref(DatabaseConstants.connectedPath)
|
||||
.on('value', (snapshot) => {
|
||||
snapshotValue = snapshot.val();
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
snapshotValue ? Promise.resolve(true) : Promise.reject(),
|
||||
);
|
||||
|
||||
expect(snapshotValue).not.toBeNull();
|
||||
expect(snapshotValue).toBe(true);
|
||||
});
|
||||
|
||||
test('triggers event with resumes if on the resumes reference path', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const resumesDataSnapshot = await FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.once('value');
|
||||
const resumes = resumesDataSnapshot.val();
|
||||
|
||||
let snapshotValue = null;
|
||||
|
||||
FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.on('value', (snapshot) => {
|
||||
snapshotValue = snapshot.val();
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
snapshotValue ? Promise.resolve(true) : Promise.reject(),
|
||||
);
|
||||
|
||||
expect(snapshotValue).not.toBeNull();
|
||||
expect(snapshotValue).toEqual(resumes);
|
||||
});
|
||||
@ -0,0 +1,59 @@
|
||||
import FirebaseStub, { DatabaseConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('retrieves resume if it exists', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const resumeId = DatabaseConstants.demoStateResume1Id;
|
||||
|
||||
const resume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
|
||||
.once('value')
|
||||
).val();
|
||||
|
||||
expect(resume).toBeTruthy();
|
||||
expect(resume.id).toEqual(resumeId);
|
||||
});
|
||||
|
||||
test('retrieves null if resume does not exist', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const resumeId = 'invalidResumeId';
|
||||
|
||||
const resume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
|
||||
.once('value')
|
||||
).val();
|
||||
|
||||
expect(resume).toBeNull();
|
||||
});
|
||||
|
||||
test('retrieves user if it exists', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const expectedUser = DatabaseConstants.user1;
|
||||
|
||||
const user = (
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.usersPath}/${expectedUser.uid}`)
|
||||
.once('value')
|
||||
).val();
|
||||
|
||||
expect(user).toBeTruthy();
|
||||
expect(user).toEqual(expectedUser);
|
||||
});
|
||||
|
||||
test('retrieves null if user does not exist', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const userId = 'invalidUserId';
|
||||
|
||||
const user = (
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.usersPath}/${userId}`)
|
||||
.once('value')
|
||||
).val();
|
||||
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
@ -0,0 +1,77 @@
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import FirebaseStub, { DatabaseConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('deletes data', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const resumeId = DatabaseConstants.demoStateResume1Id;
|
||||
const removedResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(removedResume).toBeTruthy();
|
||||
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
|
||||
.remove();
|
||||
|
||||
const actualResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(actualResume).toBeNull();
|
||||
});
|
||||
|
||||
test('triggers events', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const userUid = DatabaseConstants.user1.uid;
|
||||
|
||||
let valueCallbackSnapshotValue = null;
|
||||
const valueCallback = jest.fn((snapshot) => {
|
||||
valueCallbackSnapshotValue = snapshot.val();
|
||||
});
|
||||
FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.orderByChild('user')
|
||||
.equalTo(userUid)
|
||||
.on('value', valueCallback);
|
||||
await waitFor(() => valueCallback.mock.calls[0][0]);
|
||||
valueCallback.mockClear();
|
||||
valueCallbackSnapshotValue = null;
|
||||
|
||||
let childRemovedCallbackSnapshotValue = null;
|
||||
const childRemovedCallback = jest.fn((snapshot) => {
|
||||
childRemovedCallbackSnapshotValue = snapshot.val();
|
||||
});
|
||||
FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.orderByChild('user')
|
||||
.equalTo(userUid)
|
||||
.on('child_removed', childRemovedCallback);
|
||||
|
||||
const removedResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(
|
||||
`${DatabaseConstants.resumesPath}/${DatabaseConstants.demoStateResume1Id}`,
|
||||
)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(removedResume).toBeTruthy();
|
||||
expect(removedResume.user).toEqual(userUid);
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${removedResume.id}`)
|
||||
.remove();
|
||||
|
||||
await waitFor(() => childRemovedCallback.mock.calls[0][0]);
|
||||
expect(childRemovedCallback.mock.calls).toHaveLength(1);
|
||||
expect(childRemovedCallbackSnapshotValue).toBeTruthy();
|
||||
expect(childRemovedCallbackSnapshotValue).toEqual(removedResume);
|
||||
|
||||
await waitFor(() => valueCallback.mock.calls[0][0]);
|
||||
expect(valueCallback.mock.calls).toHaveLength(1);
|
||||
expect(valueCallbackSnapshotValue).toBeTruthy();
|
||||
expect(removedResume.id in valueCallbackSnapshotValue).toBe(false);
|
||||
});
|
||||
@ -0,0 +1,71 @@
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import FirebaseStub, { DatabaseConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('inserts data', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const existingResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(
|
||||
`${DatabaseConstants.resumesPath}/${DatabaseConstants.demoStateResume1Id}`,
|
||||
)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(existingResume).toBeTruthy();
|
||||
|
||||
const newResume = JSON.parse(JSON.stringify(existingResume));
|
||||
newResume.id = 'newre1';
|
||||
newResume.name = `Test Resume ${newResume.id}`;
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${newResume.id}`)
|
||||
.set(newResume);
|
||||
|
||||
const actualResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${newResume.id}`)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(actualResume).toBeTruthy();
|
||||
expect(actualResume).toEqual(newResume);
|
||||
});
|
||||
|
||||
test('triggers events', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
let snapshotValue = null;
|
||||
const callback = jest.fn((snapshot) => {
|
||||
snapshotValue = snapshot.val();
|
||||
});
|
||||
FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.orderByChild('user')
|
||||
.equalTo(DatabaseConstants.user1.uid)
|
||||
.on('value', callback);
|
||||
await waitFor(() => callback.mock.calls[0][0]);
|
||||
callback.mockClear();
|
||||
snapshotValue = null;
|
||||
|
||||
const existingResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(
|
||||
`${DatabaseConstants.resumesPath}/${DatabaseConstants.demoStateResume1Id}`,
|
||||
)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(existingResume).toBeTruthy();
|
||||
|
||||
const newResume = JSON.parse(JSON.stringify(existingResume));
|
||||
newResume.id = 'newre1';
|
||||
newResume.name = `Test Resume ${newResume.id}`;
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${newResume.id}`)
|
||||
.set(newResume);
|
||||
|
||||
await waitFor(() => callback.mock.calls[0][0]);
|
||||
|
||||
expect(callback.mock.calls).toHaveLength(1);
|
||||
expect(snapshotValue).not.toBeNull();
|
||||
expect(Object.keys(snapshotValue)).toHaveLength(3);
|
||||
expect(snapshotValue[newResume.id]).toBeTruthy();
|
||||
expect(snapshotValue[newResume.id]).toEqual(newResume);
|
||||
});
|
||||
@ -0,0 +1,26 @@
|
||||
import FirebaseStub, { DatabaseConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('reuses existing Reference instance', () => {
|
||||
const ref1 = FirebaseStub.database().ref(
|
||||
`${DatabaseConstants.resumesPath}/123`,
|
||||
);
|
||||
const ref2 = FirebaseStub.database().ref(
|
||||
`${DatabaseConstants.resumesPath}/123`,
|
||||
);
|
||||
|
||||
expect(ref1).toBeTruthy();
|
||||
expect(ref2).toBeTruthy();
|
||||
expect(ref1).toEqual(ref2);
|
||||
});
|
||||
|
||||
test('leading slash in reference path is ignored', () => {
|
||||
const path = `${DatabaseConstants.resumesPath}/123`;
|
||||
|
||||
const ref1 = FirebaseStub.database().ref(path);
|
||||
expect(ref1).toBeTruthy();
|
||||
expect(ref1.path).toEqual(path);
|
||||
|
||||
const ref2 = FirebaseStub.database().ref(`/${path}`);
|
||||
expect(ref2).toBeTruthy();
|
||||
expect(ref2).toEqual(ref1);
|
||||
});
|
||||
@ -0,0 +1,86 @@
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import FirebaseStub, { DatabaseConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('can spy on it', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const referencePath = `${DatabaseConstants.resumesPath}/123456`;
|
||||
const functionSpy = jest.spyOn(
|
||||
FirebaseStub.database().ref(referencePath),
|
||||
'update',
|
||||
);
|
||||
const updateArgument = 'test value 123';
|
||||
|
||||
await FirebaseStub.database().ref(referencePath).update(updateArgument);
|
||||
|
||||
expect(functionSpy).toHaveBeenCalledTimes(1);
|
||||
const functionCallArgument = functionSpy.mock.calls[0][0];
|
||||
expect(functionCallArgument).toBeTruthy();
|
||||
expect(functionCallArgument).toEqual(updateArgument);
|
||||
});
|
||||
|
||||
test('updates data', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const resumeId = DatabaseConstants.demoStateResume1Id;
|
||||
const existingResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(existingResume).toBeTruthy();
|
||||
|
||||
const resumeName = 'Test Resume renamed';
|
||||
existingResume.name = resumeName;
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
|
||||
.update(existingResume);
|
||||
|
||||
const actualResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(actualResume).toBeTruthy();
|
||||
expect(existingResume).toEqual(actualResume);
|
||||
expect(actualResume.name).toEqual(resumeName);
|
||||
});
|
||||
|
||||
test('triggers events', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
let snapshotValue = null;
|
||||
const callback = jest.fn((snapshot) => {
|
||||
snapshotValue = snapshot.val();
|
||||
});
|
||||
FirebaseStub.database()
|
||||
.ref(DatabaseConstants.resumesPath)
|
||||
.orderByChild('user')
|
||||
.equalTo(DatabaseConstants.user1.uid)
|
||||
.on('value', callback);
|
||||
await waitFor(() => callback.mock.calls[0][0]);
|
||||
callback.mockClear();
|
||||
snapshotValue = null;
|
||||
|
||||
const existingResume = (
|
||||
await FirebaseStub.database()
|
||||
.ref(
|
||||
`${DatabaseConstants.resumesPath}/${DatabaseConstants.demoStateResume1Id}`,
|
||||
)
|
||||
.once('value')
|
||||
).val();
|
||||
expect(existingResume).toBeTruthy();
|
||||
|
||||
existingResume.name = 'Test Resume renamed';
|
||||
await FirebaseStub.database()
|
||||
.ref(`${DatabaseConstants.resumesPath}/${existingResume.id}`)
|
||||
.update(existingResume);
|
||||
|
||||
await waitFor(() => callback.mock.calls[0][0]);
|
||||
|
||||
expect(callback.mock.calls).toHaveLength(1);
|
||||
expect(snapshotValue).not.toBeNull();
|
||||
expect(Object.keys(snapshotValue)).toHaveLength(2);
|
||||
expect(snapshotValue[existingResume.id]).toBeTruthy();
|
||||
expect(snapshotValue[existingResume.id]).toEqual(existingResume);
|
||||
});
|
||||
67
__mocks__/__tests__/gatsby-plugin-firebase/database.test.js
Normal file
@ -0,0 +1,67 @@
|
||||
import FirebaseStub, { DatabaseConstants } from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('reuses existing Database instance', () => {
|
||||
const database1 = FirebaseStub.database();
|
||||
const database2 = FirebaseStub.database();
|
||||
|
||||
expect(database1.uuid).toBeTruthy();
|
||||
expect(database2.uuid).toBeTruthy();
|
||||
expect(database1.uuid).toEqual(database2.uuid);
|
||||
});
|
||||
|
||||
test('ServerValue.TIMESTAMP returns current time in milliseconds', () => {
|
||||
const now = new Date().getTime();
|
||||
const timestamp = FirebaseStub.database.ServerValue.TIMESTAMP;
|
||||
|
||||
expect(timestamp).toBeTruthy();
|
||||
expect(timestamp).toBeGreaterThanOrEqual(now);
|
||||
});
|
||||
|
||||
test('initializing data sets up resumes and users', async () => {
|
||||
FirebaseStub.database().initializeData();
|
||||
|
||||
const resumesRef = FirebaseStub.database().ref(DatabaseConstants.resumesPath);
|
||||
const resumesDataSnapshot = await resumesRef.once('value');
|
||||
const resumes = resumesDataSnapshot.val();
|
||||
expect(resumes).toBeTruthy();
|
||||
expect(Object.keys(resumes)).toHaveLength(5);
|
||||
const demoStateResume1 = resumes[DatabaseConstants.demoStateResume1Id];
|
||||
expect(demoStateResume1).toBeTruthy();
|
||||
expect(demoStateResume1.id).toEqual(DatabaseConstants.demoStateResume1Id);
|
||||
expect(demoStateResume1.user).toEqual(DatabaseConstants.user1.uid);
|
||||
const demoStateResume2 = resumes[DatabaseConstants.demoStateResume2Id];
|
||||
expect(demoStateResume2).toBeTruthy();
|
||||
expect(demoStateResume2.id).toEqual(DatabaseConstants.demoStateResume2Id);
|
||||
expect(demoStateResume2.user).toEqual(DatabaseConstants.user2.uid);
|
||||
const initialStateResume1 = resumes[DatabaseConstants.initialStateResume1Id];
|
||||
expect(initialStateResume1).toBeTruthy();
|
||||
expect(initialStateResume1.id).toEqual(
|
||||
DatabaseConstants.initialStateResume1Id,
|
||||
);
|
||||
expect(initialStateResume1.user).toEqual(DatabaseConstants.user1.uid);
|
||||
const demoStateResume3 = resumes[DatabaseConstants.demoStateResume3Id];
|
||||
expect(demoStateResume3).toBeTruthy();
|
||||
expect(demoStateResume3.id).toEqual(DatabaseConstants.demoStateResume3Id);
|
||||
expect(demoStateResume3.user).toEqual(DatabaseConstants.user3.uid);
|
||||
const initialStateResume2 = resumes[DatabaseConstants.initialStateResume2Id];
|
||||
expect(initialStateResume2).toBeTruthy();
|
||||
expect(initialStateResume2.id).toEqual(
|
||||
DatabaseConstants.initialStateResume2Id,
|
||||
);
|
||||
expect(initialStateResume2.user).toEqual(DatabaseConstants.user3.uid);
|
||||
|
||||
const usersRef = FirebaseStub.database().ref(DatabaseConstants.usersPath);
|
||||
const usersDataSnapshot = await usersRef.once('value');
|
||||
const users = usersDataSnapshot.val();
|
||||
expect(users).toBeTruthy();
|
||||
expect(Object.keys(users)).toHaveLength(3);
|
||||
const anonymousUser1 = users[DatabaseConstants.user1.uid];
|
||||
expect(anonymousUser1).toBeTruthy();
|
||||
expect(anonymousUser1).toEqual(DatabaseConstants.user1);
|
||||
const anonymousUser2 = users[DatabaseConstants.user2.uid];
|
||||
expect(anonymousUser2).toBeTruthy();
|
||||
expect(anonymousUser2).toEqual(DatabaseConstants.user2);
|
||||
const googleUser3 = users[DatabaseConstants.user3.uid];
|
||||
expect(googleUser3).toBeTruthy();
|
||||
expect(googleUser3).toEqual(DatabaseConstants.user3);
|
||||
});
|
||||
19
__mocks__/__tests__/gatsby-plugin-firebase/functions.test.js
Normal file
@ -0,0 +1,19 @@
|
||||
import FirebaseStub from '../../gatsby-plugin-firebase';
|
||||
|
||||
test('reuses existing Functions instance', () => {
|
||||
const functions1 = FirebaseStub.functions();
|
||||
const functions2 = FirebaseStub.functions();
|
||||
|
||||
expect(functions1.uuid).toBeTruthy();
|
||||
expect(functions2.uuid).toBeTruthy();
|
||||
expect(functions1.uuid).toEqual(functions2.uuid);
|
||||
});
|
||||
|
||||
test('deleteUser function returns true', async () => {
|
||||
const deleteUser = FirebaseStub.functions().httpsCallable('deleteUser');
|
||||
|
||||
const result = await deleteUser();
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.data).toEqual(true);
|
||||
});
|
||||
2
__mocks__/file-mock.js
Normal file
@ -0,0 +1,2 @@
|
||||
const mockFile = 'test-file-stub';
|
||||
export default mockFile;
|
||||
33
__mocks__/gatsby-plugin-firebase.js
Normal file
@ -0,0 +1,33 @@
|
||||
import Auth from './gatsby-plugin-firebase/auth/auth';
|
||||
import AuthConstants from './gatsby-plugin-firebase/constants/auth';
|
||||
import Database from './gatsby-plugin-firebase/database/database';
|
||||
import DatabaseConstants from './gatsby-plugin-firebase/constants/database';
|
||||
import Functions from './gatsby-plugin-firebase/functions/functions';
|
||||
import FunctionsConstants from './gatsby-plugin-firebase/constants/functions';
|
||||
import GoogleAuthProvider from './gatsby-plugin-firebase/auth/googleAuthProvider';
|
||||
|
||||
class FirebaseStub {
|
||||
static auth() {
|
||||
return Auth.instance;
|
||||
}
|
||||
|
||||
static database() {
|
||||
return Database.instance;
|
||||
}
|
||||
|
||||
static functions() {
|
||||
return Functions.instance;
|
||||
}
|
||||
}
|
||||
|
||||
FirebaseStub.auth.GoogleAuthProvider = GoogleAuthProvider;
|
||||
|
||||
FirebaseStub.database.ServerValue = {};
|
||||
Object.defineProperty(FirebaseStub.database.ServerValue, 'TIMESTAMP', {
|
||||
get() {
|
||||
return new Date().getTime();
|
||||
},
|
||||
});
|
||||
|
||||
export default FirebaseStub;
|
||||
export { AuthConstants, DatabaseConstants, FunctionsConstants };
|
||||
125
__mocks__/gatsby-plugin-firebase/auth/auth.js
Normal file
@ -0,0 +1,125 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { delay } from '../../../src/utils/index';
|
||||
import AuthProvider from './authProvider';
|
||||
import Constants from '../constants/auth';
|
||||
import GoogleAuthProvider from './googleAuthProvider';
|
||||
import User from './user';
|
||||
|
||||
const singleton = Symbol('');
|
||||
const singletonEnforcer = Symbol('');
|
||||
|
||||
class Auth {
|
||||
constructor(enforcer) {
|
||||
if (enforcer !== singletonEnforcer) {
|
||||
throw new Error('Cannot construct singleton');
|
||||
}
|
||||
|
||||
this._uuid = uuidv4();
|
||||
this._currentUser = null;
|
||||
this._onAuthStateChangedObservers = [];
|
||||
}
|
||||
|
||||
static get instance() {
|
||||
if (!this[singleton]) {
|
||||
this[singleton] = new Auth(singletonEnforcer);
|
||||
}
|
||||
|
||||
return this[singleton];
|
||||
}
|
||||
|
||||
get currentUser() {
|
||||
return this._currentUser;
|
||||
}
|
||||
|
||||
get uuid() {
|
||||
return this._uuid;
|
||||
}
|
||||
|
||||
get onAuthStateChangedObservers() {
|
||||
return this._onAuthStateChangedObservers;
|
||||
}
|
||||
|
||||
onAuthStateChanged(observer) {
|
||||
this.onAuthStateChangedObservers.push(observer);
|
||||
|
||||
return () => {
|
||||
this._onAuthStateChangedObservers =
|
||||
this.onAuthStateChangedObservers.filter((obs) => obs !== observer);
|
||||
};
|
||||
}
|
||||
|
||||
async signInAnonymously() {
|
||||
const user = Constants.anonymousUser1;
|
||||
|
||||
this._currentUser = new User(
|
||||
user.displayName,
|
||||
user.email,
|
||||
user.providerId,
|
||||
user.uid,
|
||||
user.isAnonymous,
|
||||
this.signOut.bind(this),
|
||||
);
|
||||
|
||||
await delay(Constants.defaultDelayInMilliseconds);
|
||||
|
||||
this.onAuthStateChangedObservers.forEach((observer) =>
|
||||
observer(this._currentUser),
|
||||
);
|
||||
|
||||
return this._currentUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates with popup.
|
||||
*
|
||||
* @param {AuthProvider} provider The provider to authenticate.
|
||||
*/
|
||||
async signInWithPopup(provider) {
|
||||
if (!provider) {
|
||||
throw new Error('provider must be provided.');
|
||||
} else if (!(provider instanceof AuthProvider)) {
|
||||
throw new Error('provider should be an AuthProvider.');
|
||||
}
|
||||
|
||||
if (!(provider instanceof GoogleAuthProvider)) {
|
||||
throw new Error(
|
||||
`${provider.constructor.name} is currently not supported.`,
|
||||
);
|
||||
}
|
||||
|
||||
const user = Constants.googleUser3;
|
||||
|
||||
this._currentUser = new User(
|
||||
user.displayName,
|
||||
user.email,
|
||||
user.providerId,
|
||||
user.uid,
|
||||
user.isAnonymous,
|
||||
this.signOut.bind(this),
|
||||
);
|
||||
|
||||
await delay(Constants.defaultDelayInMilliseconds);
|
||||
|
||||
this.onAuthStateChangedObservers.forEach((observer) =>
|
||||
observer(this._currentUser),
|
||||
);
|
||||
|
||||
return this._currentUser;
|
||||
}
|
||||
|
||||
async signOut() {
|
||||
if (this._currentUser === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._currentUser = null;
|
||||
|
||||
await delay(Constants.defaultDelayInMilliseconds);
|
||||
|
||||
this.onAuthStateChangedObservers.forEach((observer) => observer(null));
|
||||
}
|
||||
}
|
||||
|
||||
export default Auth;
|
||||
23
__mocks__/gatsby-plugin-firebase/auth/authProvider.js
Normal file
@ -0,0 +1,23 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
class AuthProvider {
|
||||
/**
|
||||
* Creates a new auth provider.
|
||||
*
|
||||
* @param {string} providerId Provider ID.
|
||||
*/
|
||||
constructor(providerId) {
|
||||
if (!providerId) {
|
||||
throw new Error('providerId must be provided.');
|
||||
} else if (typeof providerId !== 'string') {
|
||||
throw new Error('providerId should be a string.');
|
||||
} else {
|
||||
this._providerId = providerId;
|
||||
}
|
||||
}
|
||||
|
||||
get providerId() {
|
||||
return this._providerId;
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthProvider;
|
||||
10
__mocks__/gatsby-plugin-firebase/auth/googleAuthProvider.js
Normal file
@ -0,0 +1,10 @@
|
||||
import AuthProvider from './authProvider';
|
||||
import Constants from '../constants/auth';
|
||||
|
||||
class GoogleAuthProvider extends AuthProvider {
|
||||
constructor() {
|
||||
super(Constants.googleAuthProviderId);
|
||||
}
|
||||
}
|
||||
|
||||
export default GoogleAuthProvider;
|
||||
67
__mocks__/gatsby-plugin-firebase/auth/user.js
Normal file
@ -0,0 +1,67 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import Constants from '../constants/auth';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import AuthProvider from './authProvider';
|
||||
import UserInfo from './userInfo';
|
||||
import { delay } from '../../../src/utils/index';
|
||||
|
||||
class User extends UserInfo {
|
||||
/**
|
||||
* Creates a new user.
|
||||
*
|
||||
* @param {string|null} displayName Display name.
|
||||
* @param {string|null} email Email.
|
||||
* @param {string} providerId Auth provider ID.
|
||||
* @param {string} uid The user's unique ID.
|
||||
* @param {boolean} isAnonymous Is anonymous.
|
||||
* @param {function():Promise<void>} deleteUser Delete user callback.
|
||||
*/
|
||||
constructor(displayName, email, providerId, uid, isAnonymous, deleteUser) {
|
||||
super(displayName, email, providerId, uid);
|
||||
|
||||
if (!deleteUser) {
|
||||
throw new Error('deleteUser must be provided.');
|
||||
} else if (typeof deleteUser !== 'function') {
|
||||
throw new Error('deleteUser should be a function.');
|
||||
} else {
|
||||
this._deleteUser = deleteUser;
|
||||
}
|
||||
|
||||
this._isAnonymous = isAnonymous;
|
||||
|
||||
this._providerData = [];
|
||||
if (!isAnonymous) {
|
||||
this._providerData.push(
|
||||
new UserInfo(displayName, email, providerId, uid),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get isAnonymous() {
|
||||
return this._isAnonymous;
|
||||
}
|
||||
|
||||
get providerData() {
|
||||
return this._providerData;
|
||||
}
|
||||
|
||||
async delete() {
|
||||
await delay(Constants.defaultDelayInMilliseconds);
|
||||
|
||||
await this._deleteUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reauthenticates the user with popup.
|
||||
*
|
||||
* @param {AuthProvider} provider The provider to authenticate.
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async reauthenticateWithPopup(provider) {
|
||||
await delay(Constants.defaultDelayInMilliseconds);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export default User;
|
||||
47
__mocks__/gatsby-plugin-firebase/auth/userInfo.js
Normal file
@ -0,0 +1,47 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
class UserInfo {
|
||||
/**
|
||||
* Creates a new user profile information.
|
||||
*
|
||||
* @param {string|null} displayName Display name.
|
||||
* @param {string|null} email Email.
|
||||
* @param {string} providerId Auth provider ID.
|
||||
* @param {string} uid The user's unique ID.
|
||||
*/
|
||||
constructor(displayName, email, providerId, uid) {
|
||||
if (!uid) {
|
||||
throw new Error('uid must be provided.');
|
||||
} else if (typeof uid !== 'string') {
|
||||
throw new Error('uid should be a string.');
|
||||
} else {
|
||||
this._uid = uid;
|
||||
}
|
||||
|
||||
if (typeof providerId !== 'string') {
|
||||
throw new Error('providerId should be a string.');
|
||||
} else {
|
||||
this._providerId = providerId;
|
||||
}
|
||||
|
||||
this._displayName = displayName;
|
||||
this._email = email;
|
||||
}
|
||||
|
||||
get displayName() {
|
||||
return this._displayName;
|
||||
}
|
||||
|
||||
get email() {
|
||||
return this._email;
|
||||
}
|
||||
|
||||
get providerId() {
|
||||
return this._providerId;
|
||||
}
|
||||
|
||||
get uid() {
|
||||
return this._uid;
|
||||
}
|
||||
}
|
||||
|
||||
export default UserInfo;
|
||||
51
__mocks__/gatsby-plugin-firebase/constants/auth.js
Normal file
@ -0,0 +1,51 @@
|
||||
const googleAuthProviderId = 'google.com';
|
||||
|
||||
const anonymousUser1 = {
|
||||
displayName: 'Anonymous User 1',
|
||||
email: 'anonymous1@noemail.com',
|
||||
isAnonymous: true,
|
||||
providerId: '',
|
||||
uid: 'anonym1',
|
||||
};
|
||||
|
||||
const anonymousUser2 = {
|
||||
displayName: 'Anonymous User 2',
|
||||
email: 'anonymous2@noemail.com',
|
||||
isAnonymous: true,
|
||||
providerId: '',
|
||||
uid: 'anonym2',
|
||||
};
|
||||
|
||||
const googleUser3 = {
|
||||
displayName: 'Google User 3',
|
||||
email: 'google3@noemail.com',
|
||||
isAnonymous: false,
|
||||
providerId: googleAuthProviderId,
|
||||
uid: 'google3',
|
||||
};
|
||||
|
||||
const defaultDelayInMilliseconds = 100;
|
||||
|
||||
class Auth {
|
||||
static get googleAuthProviderId() {
|
||||
return googleAuthProviderId;
|
||||
}
|
||||
|
||||
static get anonymousUser1() {
|
||||
return anonymousUser1;
|
||||
}
|
||||
|
||||
static get anonymousUser2() {
|
||||
return anonymousUser2;
|
||||
}
|
||||
|
||||
static get googleUser3() {
|
||||
return googleUser3;
|
||||
}
|
||||
|
||||
static get defaultDelayInMilliseconds() {
|
||||
return defaultDelayInMilliseconds;
|
||||
}
|
||||
}
|
||||
|
||||
export default Auth;
|
||||
89
__mocks__/gatsby-plugin-firebase/constants/database.js
Normal file
@ -0,0 +1,89 @@
|
||||
import AuthConstants from './auth';
|
||||
|
||||
const valueEventType = 'value';
|
||||
const childRemovedEventType = 'child_removed';
|
||||
|
||||
const resumesPath = 'resumes';
|
||||
const usersPath = 'users';
|
||||
const connectedPath = '.info/connected';
|
||||
|
||||
const demoStateResume1Id = 'demo_1';
|
||||
const demoStateResume2Id = 'demo_2';
|
||||
const demoStateResume3Id = 'demo_3';
|
||||
const initialStateResume1Id = 'init_1';
|
||||
const initialStateResume2Id = 'init_2';
|
||||
|
||||
const user1 = {
|
||||
uid: AuthConstants.anonymousUser1.uid,
|
||||
isAnonymous: AuthConstants.anonymousUser1.isAnonymous,
|
||||
};
|
||||
const user2 = {
|
||||
uid: AuthConstants.anonymousUser2.uid,
|
||||
isAnonymous: AuthConstants.anonymousUser2.isAnonymous,
|
||||
};
|
||||
const user3 = {
|
||||
uid: AuthConstants.googleUser3.uid,
|
||||
isAnonymous: AuthConstants.googleUser3.isAnonymous,
|
||||
};
|
||||
|
||||
const defaultDelayInMilliseconds = 100;
|
||||
|
||||
class Database {
|
||||
static get valueEventType() {
|
||||
return valueEventType;
|
||||
}
|
||||
|
||||
static get childRemovedEventType() {
|
||||
return childRemovedEventType;
|
||||
}
|
||||
|
||||
static get resumesPath() {
|
||||
return resumesPath;
|
||||
}
|
||||
|
||||
static get usersPath() {
|
||||
return usersPath;
|
||||
}
|
||||
|
||||
static get connectedPath() {
|
||||
return connectedPath;
|
||||
}
|
||||
|
||||
static get demoStateResume1Id() {
|
||||
return demoStateResume1Id;
|
||||
}
|
||||
|
||||
static get demoStateResume2Id() {
|
||||
return demoStateResume2Id;
|
||||
}
|
||||
|
||||
static get demoStateResume3Id() {
|
||||
return demoStateResume3Id;
|
||||
}
|
||||
|
||||
static get initialStateResume1Id() {
|
||||
return initialStateResume1Id;
|
||||
}
|
||||
|
||||
static get initialStateResume2Id() {
|
||||
return initialStateResume2Id;
|
||||
}
|
||||
|
||||
static get user1() {
|
||||
return user1;
|
||||
}
|
||||
|
||||
static get user2() {
|
||||
return user2;
|
||||
}
|
||||
|
||||
static get user3() {
|
||||
return user3;
|
||||
}
|
||||
|
||||
static get defaultDelayInMilliseconds() {
|
||||
return defaultDelayInMilliseconds;
|
||||
}
|
||||
}
|
||||
|
||||
export default Database;
|
||||
15
__mocks__/gatsby-plugin-firebase/constants/functions.js
Normal file
@ -0,0 +1,15 @@
|
||||
const deleteUserFunctionName = 'deleteUser';
|
||||
|
||||
const defaultDelayInMilliseconds = 100;
|
||||
|
||||
class Functions {
|
||||
static get deleteUserFunctionName() {
|
||||
return deleteUserFunctionName;
|
||||
}
|
||||
|
||||
static get defaultDelayInMilliseconds() {
|
||||
return defaultDelayInMilliseconds;
|
||||
}
|
||||
}
|
||||
|
||||
export default Functions;
|
||||
24
__mocks__/gatsby-plugin-firebase/database/dataSnapshot.js
Normal file
@ -0,0 +1,24 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
class DataSnapshot {
|
||||
constructor(getData, value = undefined) {
|
||||
if (!getData) {
|
||||
throw new Error('getData must be provided.');
|
||||
} else if (typeof getData !== 'function') {
|
||||
throw new Error('getData should be a function.');
|
||||
}
|
||||
|
||||
this._getData = getData;
|
||||
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
val() {
|
||||
return typeof this.value !== 'undefined' ? this.value : this._getData();
|
||||
}
|
||||
}
|
||||
|
||||
export default DataSnapshot;
|
||||
170
__mocks__/gatsby-plugin-firebase/database/database.js
Normal file
@ -0,0 +1,170 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import DatabaseConstants from '../constants/database';
|
||||
import Reference from './reference';
|
||||
|
||||
const singleton = Symbol('');
|
||||
const singletonEnforcer = Symbol('');
|
||||
|
||||
const readFile = (fileRelativePath) => {
|
||||
const fileAbsolutePath = path.resolve(__dirname, fileRelativePath);
|
||||
const fileBuffer = fs.readFileSync(fileAbsolutePath);
|
||||
const fileData = JSON.parse(fileBuffer);
|
||||
return fileData;
|
||||
};
|
||||
|
||||
class Database {
|
||||
constructor(enforcer) {
|
||||
if (enforcer !== singletonEnforcer) {
|
||||
throw new Error('Cannot construct singleton');
|
||||
}
|
||||
|
||||
this._uuid = uuidv4();
|
||||
this._data = {};
|
||||
this._references = {};
|
||||
}
|
||||
|
||||
static get instance() {
|
||||
if (!this[singleton]) {
|
||||
this[singleton] = new Database(singletonEnforcer);
|
||||
}
|
||||
|
||||
return this[singleton];
|
||||
}
|
||||
|
||||
get uuid() {
|
||||
return this._uuid;
|
||||
}
|
||||
|
||||
_getData(dataPath) {
|
||||
if (!dataPath) {
|
||||
throw new Error('dataPath must be provided.');
|
||||
}
|
||||
|
||||
const dataPathElements = dataPath.split('/');
|
||||
if (!(dataPathElements[0] in this._data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dataPathElements.length === 1) {
|
||||
return this._data[dataPathElements[0]];
|
||||
}
|
||||
|
||||
if (dataPathElements.length === 2) {
|
||||
if (!(dataPathElements[1] in this._data[dataPathElements[0]])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._data[dataPathElements[0]][dataPathElements[1]];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_getReference(referencePath) {
|
||||
return referencePath in this._references
|
||||
? this._references[referencePath]
|
||||
: null;
|
||||
}
|
||||
|
||||
_setData(dataPath, value) {
|
||||
if (!dataPath) {
|
||||
throw new Error('dataPath must be provided.');
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
throw new Error('value is undefined.');
|
||||
}
|
||||
|
||||
const dataPathElements = dataPath.split('/');
|
||||
if (dataPathElements.length !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(dataPathElements[0] in this._data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dataPathElements[1]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
delete this._data[dataPathElements[0]][dataPathElements[1]];
|
||||
} else {
|
||||
this._data[dataPathElements[0]][dataPathElements[1]] = value;
|
||||
}
|
||||
}
|
||||
|
||||
initializeData() {
|
||||
const resumes = {};
|
||||
const date = new Date('December 15, 2020 11:20:25');
|
||||
|
||||
const demoStateResume1 = readFile('../../../src/data/demoState.json');
|
||||
demoStateResume1.updatedAt = date.valueOf();
|
||||
date.setMonth(date.getMonth() - 2);
|
||||
demoStateResume1.createdAt = date.valueOf();
|
||||
demoStateResume1.user = DatabaseConstants.user1.uid;
|
||||
resumes[DatabaseConstants.demoStateResume1Id] = demoStateResume1;
|
||||
|
||||
const demoStateResume2 = JSON.parse(JSON.stringify(demoStateResume1));
|
||||
demoStateResume2.user = DatabaseConstants.user2.uid;
|
||||
resumes[DatabaseConstants.demoStateResume2Id] = demoStateResume2;
|
||||
|
||||
const initialStateResume1 = readFile('../../../src/data/initialState.json');
|
||||
initialStateResume1.updatedAt = date.valueOf();
|
||||
initialStateResume1.createdAt = date.valueOf();
|
||||
initialStateResume1.user = DatabaseConstants.user1.uid;
|
||||
resumes[DatabaseConstants.initialStateResume1Id] = initialStateResume1;
|
||||
|
||||
const demoStateResume3 = readFile('../../../src/data/demoState.json');
|
||||
demoStateResume3.updatedAt = date.valueOf();
|
||||
date.setMonth(date.getMonth() - 2);
|
||||
demoStateResume3.createdAt = date.valueOf();
|
||||
demoStateResume3.user = DatabaseConstants.user3.uid;
|
||||
resumes[DatabaseConstants.demoStateResume3Id] = demoStateResume3;
|
||||
|
||||
const initialStateResume2 = readFile('../../../src/data/initialState.json');
|
||||
initialStateResume2.updatedAt = date.valueOf();
|
||||
initialStateResume2.createdAt = date.valueOf();
|
||||
initialStateResume2.user = DatabaseConstants.user3.uid;
|
||||
resumes[DatabaseConstants.initialStateResume2Id] = initialStateResume2;
|
||||
|
||||
Object.keys(resumes).forEach((key) => {
|
||||
const resume = resumes[key];
|
||||
resume.id = key;
|
||||
resume.name = `Test Resume ${key}`;
|
||||
});
|
||||
|
||||
this._data[DatabaseConstants.resumesPath] = resumes;
|
||||
|
||||
const users = {};
|
||||
users[DatabaseConstants.user1.uid] = DatabaseConstants.user1;
|
||||
users[DatabaseConstants.user2.uid] = DatabaseConstants.user2;
|
||||
users[DatabaseConstants.user3.uid] = DatabaseConstants.user3;
|
||||
this._data[DatabaseConstants.usersPath] = users;
|
||||
}
|
||||
|
||||
ref(referencePath) {
|
||||
const newRef = new Reference(
|
||||
referencePath,
|
||||
(dataPath) => this._getData(dataPath),
|
||||
(dataPath, value) => this._setData(dataPath, value),
|
||||
(refPath) => this._getReference(refPath),
|
||||
);
|
||||
|
||||
const existingRef = this._getReference(newRef.path);
|
||||
if (existingRef) {
|
||||
existingRef.initializeQueryParameters();
|
||||
return existingRef;
|
||||
}
|
||||
|
||||
this._references[newRef.path] = newRef;
|
||||
return newRef;
|
||||
}
|
||||
}
|
||||
|
||||
export default Database;
|
||||
215
__mocks__/gatsby-plugin-firebase/database/reference.js
Normal file
@ -0,0 +1,215 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { delay } from '../../../src/utils/index';
|
||||
import DataSnapshot from './dataSnapshot';
|
||||
import DatabaseConstants from '../constants/database';
|
||||
|
||||
const parsePath = (path) => {
|
||||
if (!path) {
|
||||
throw new Error('path must be provided.');
|
||||
} else if (typeof path !== 'string') {
|
||||
throw new Error('path should be a string.');
|
||||
} else {
|
||||
let parsedPath = path.trim();
|
||||
|
||||
if (parsedPath[0] === '/') {
|
||||
parsedPath = parsedPath.substring(1);
|
||||
}
|
||||
|
||||
return parsedPath;
|
||||
}
|
||||
};
|
||||
|
||||
class Reference {
|
||||
constructor(path, getDatabaseData, setDatabaseData, getReference) {
|
||||
this._path = parsePath(path);
|
||||
|
||||
this._uuid = uuidv4();
|
||||
|
||||
if (this.path === DatabaseConstants.connectedPath) {
|
||||
this._dataSnapshot = new DataSnapshot(() => {}, true);
|
||||
} else {
|
||||
this._dataSnapshot = new DataSnapshot(() => this._getData());
|
||||
}
|
||||
|
||||
if (!getDatabaseData) {
|
||||
throw new Error('getDatabaseData must be provided.');
|
||||
} else if (typeof getDatabaseData !== 'function') {
|
||||
throw new Error('getDatabaseData should be a function.');
|
||||
}
|
||||
|
||||
this._getDatabaseData = getDatabaseData;
|
||||
|
||||
if (!setDatabaseData) {
|
||||
throw new Error('setDatabaseData must be provided.');
|
||||
} else if (typeof getDatabaseData !== 'function') {
|
||||
throw new Error('setDatabaseData should be a function.');
|
||||
}
|
||||
|
||||
this._setDatabaseData = setDatabaseData;
|
||||
|
||||
if (!getReference) {
|
||||
throw new Error('getReference must be provided.');
|
||||
} else if (typeof getDatabaseData !== 'function') {
|
||||
throw new Error('getReference should be a function.');
|
||||
}
|
||||
|
||||
this._getReference = getReference;
|
||||
|
||||
this._eventCallbacks = {};
|
||||
|
||||
this.initializeQueryParameters();
|
||||
}
|
||||
|
||||
get path() {
|
||||
return this._path;
|
||||
}
|
||||
|
||||
get uuid() {
|
||||
return this._uuid;
|
||||
}
|
||||
|
||||
get eventCallbacks() {
|
||||
return this._eventCallbacks;
|
||||
}
|
||||
|
||||
get orderByChildPath() {
|
||||
return this._orderByChildPath;
|
||||
}
|
||||
|
||||
get equalToValue() {
|
||||
return this._equalToValue;
|
||||
}
|
||||
|
||||
_getData() {
|
||||
const databaseData = this._getDatabaseData(this.path);
|
||||
|
||||
if (!databaseData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.orderByChildPath && this.equalToValue) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(databaseData).filter(
|
||||
([, value]) => value[this.orderByChildPath] === this.equalToValue,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return databaseData;
|
||||
}
|
||||
|
||||
_getParent() {
|
||||
const pathElements = this.path.split('/');
|
||||
|
||||
let parent = null;
|
||||
if (pathElements.length === 2) {
|
||||
parent = this._getReference(pathElements[0]);
|
||||
}
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
||||
_handleDataUpdate(value) {
|
||||
if (typeof value === 'undefined') {
|
||||
throw new Error('value must be provided.');
|
||||
}
|
||||
|
||||
const currentData = this._getData();
|
||||
const parentReference = this._getParent();
|
||||
|
||||
this._setDatabaseData(this.path, value);
|
||||
|
||||
if (value === null) {
|
||||
if (parentReference) {
|
||||
parentReference.triggerEventCallback(
|
||||
DatabaseConstants.childRemovedEventType,
|
||||
currentData,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.triggerEventCallback(DatabaseConstants.valueEventType);
|
||||
}
|
||||
|
||||
if (parentReference) {
|
||||
parentReference.triggerEventCallback(DatabaseConstants.valueEventType);
|
||||
}
|
||||
}
|
||||
|
||||
triggerEventCallback(eventType, snapshotValue = undefined) {
|
||||
if (!(eventType in this.eventCallbacks)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot =
|
||||
this.path === DatabaseConstants.connectedPath
|
||||
? this._dataSnapshot
|
||||
: new DataSnapshot(() => this._getData(), snapshotValue);
|
||||
|
||||
this.eventCallbacks[eventType](snapshot);
|
||||
}
|
||||
|
||||
equalTo(value) {
|
||||
this._equalToValue = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
initializeQueryParameters() {
|
||||
this._orderByChildPath = '';
|
||||
this._equalToValue = '';
|
||||
}
|
||||
|
||||
off() {
|
||||
this._eventCallbacks = {};
|
||||
}
|
||||
|
||||
on(eventType, callback) {
|
||||
this.eventCallbacks[eventType] = callback;
|
||||
|
||||
if (eventType === DatabaseConstants.valueEventType) {
|
||||
setTimeout(() => {
|
||||
this.triggerEventCallback(eventType);
|
||||
}, DatabaseConstants.defaultDelayInMilliseconds);
|
||||
}
|
||||
|
||||
return callback;
|
||||
}
|
||||
|
||||
async once(eventType) {
|
||||
if (!eventType) {
|
||||
throw new Error('eventType must be provided.');
|
||||
} else if (typeof eventType !== 'string') {
|
||||
throw new Error('eventType should be a string.');
|
||||
}
|
||||
|
||||
await delay(DatabaseConstants.defaultDelayInMilliseconds);
|
||||
|
||||
return this._dataSnapshot;
|
||||
}
|
||||
|
||||
orderByChild(path) {
|
||||
this._orderByChildPath = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
async update(value) {
|
||||
await delay(DatabaseConstants.defaultDelayInMilliseconds);
|
||||
|
||||
this._handleDataUpdate(value);
|
||||
}
|
||||
|
||||
async remove() {
|
||||
await delay(DatabaseConstants.defaultDelayInMilliseconds);
|
||||
|
||||
this._handleDataUpdate(null);
|
||||
}
|
||||
|
||||
async set(value) {
|
||||
await delay(DatabaseConstants.defaultDelayInMilliseconds);
|
||||
|
||||
this._handleDataUpdate(value);
|
||||
}
|
||||
}
|
||||
|
||||
export default Reference;
|
||||
53
__mocks__/gatsby-plugin-firebase/functions/functions.js
Normal file
@ -0,0 +1,53 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import FunctionsConstants from '../constants/functions';
|
||||
import HttpsCallableResult from './httpsCallableResult';
|
||||
import { delay } from '../../../src/utils/index';
|
||||
|
||||
const singleton = Symbol('');
|
||||
const singletonEnforcer = Symbol('');
|
||||
|
||||
const deleteUser = async () => {
|
||||
await delay(FunctionsConstants.defaultDelayInMilliseconds);
|
||||
|
||||
return new HttpsCallableResult(true);
|
||||
};
|
||||
|
||||
class Functions {
|
||||
constructor(enforcer) {
|
||||
if (enforcer !== singletonEnforcer) {
|
||||
throw new Error('Cannot construct singleton');
|
||||
}
|
||||
|
||||
this._uuid = uuidv4();
|
||||
|
||||
this._httpsCallables = {};
|
||||
this._httpsCallables[FunctionsConstants.deleteUserFunctionName] =
|
||||
deleteUser;
|
||||
}
|
||||
|
||||
static get instance() {
|
||||
if (!this[singleton]) {
|
||||
this[singleton] = new Functions(singletonEnforcer);
|
||||
}
|
||||
|
||||
return this[singleton];
|
||||
}
|
||||
|
||||
get uuid() {
|
||||
return this._uuid;
|
||||
}
|
||||
|
||||
httpsCallable(name) {
|
||||
if (!name) {
|
||||
throw new Error('name must be provided.');
|
||||
} else if (typeof name !== 'string') {
|
||||
throw new Error('name should be a string.');
|
||||
}
|
||||
|
||||
return this._httpsCallables[name];
|
||||
}
|
||||
}
|
||||
|
||||
export default Functions;
|
||||
@ -0,0 +1,19 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
class HttpsCallableResult {
|
||||
constructor(data) {
|
||||
this._uuid = uuidv4();
|
||||
this._data = data;
|
||||
}
|
||||
|
||||
get data() {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
get uuid() {
|
||||
return this._uuid;
|
||||
}
|
||||
}
|
||||
|
||||
export default HttpsCallableResult;
|
||||
71
__mocks__/gatsby.js
Normal file
@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
|
||||
import { delay } from '../src/utils/index';
|
||||
|
||||
const Gatsby = jest.requireActual('gatsby');
|
||||
|
||||
const imageData = {
|
||||
images: {
|
||||
fallback: {
|
||||
src: `image_src.jpg`,
|
||||
srcSet: `image_src_set.jpg 1x`,
|
||||
},
|
||||
},
|
||||
layout: `fixed`,
|
||||
width: 1,
|
||||
height: 2,
|
||||
};
|
||||
const childImageSharp = { gatsbyImageData: imageData };
|
||||
|
||||
const useStaticQuery = () => ({
|
||||
site: {
|
||||
siteMetadata: {
|
||||
title: 'Test title',
|
||||
description: 'Test description',
|
||||
author: 'Test author',
|
||||
siteUrl: 'https://testsiteurl/',
|
||||
},
|
||||
},
|
||||
file: {
|
||||
childImageSharp,
|
||||
},
|
||||
onyx: {
|
||||
childImageSharp,
|
||||
},
|
||||
pikachu: {
|
||||
childImageSharp,
|
||||
},
|
||||
gengar: {
|
||||
childImageSharp,
|
||||
},
|
||||
castform: {
|
||||
childImageSharp,
|
||||
},
|
||||
glalie: {
|
||||
childImageSharp,
|
||||
},
|
||||
celebi: {
|
||||
childImageSharp,
|
||||
},
|
||||
});
|
||||
|
||||
const defaultDelayInMilliseconds = 100;
|
||||
|
||||
const navigate = async () => {
|
||||
await delay(defaultDelayInMilliseconds);
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
...Gatsby,
|
||||
graphql: jest.fn(),
|
||||
Link: jest.fn().mockImplementation(({ to, ...rest }) =>
|
||||
React.createElement('a', {
|
||||
...rest,
|
||||
href: to,
|
||||
}),
|
||||
),
|
||||
navigate: jest.fn(navigate),
|
||||
useStaticQuery,
|
||||
};
|
||||
13
app.json
@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "Reactive Resume",
|
||||
"description": "A one-of-a-kind resume builder that's not out to get your data. Completely secure, customizable, portable, open-source and free forever.",
|
||||
"website": "https://rxresu.me/",
|
||||
"repository": "https://github.com/AmruthPillai/Reactive-Resume",
|
||||
"logo": "https://i.imgur.com/ugpElge.png",
|
||||
"buildpacks": [
|
||||
{
|
||||
"url": "mars/create-react-app"
|
||||
}
|
||||
],
|
||||
"keywords": ["react", "resume", "static"]
|
||||
}
|
||||
@ -1,3 +1,3 @@
|
||||
files:
|
||||
- source: /src/i18n/locales/en/**/*.json
|
||||
translation: /src/i18n/locales/%two_letters_code%/**/%original_file_name%
|
||||
- source: /src/i18n/locales/en.json
|
||||
translation: /src/i18n/locales/%two_letters_code%.json
|
||||
|
||||
19
database.rules.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"rules": {
|
||||
"resumes": {
|
||||
".indexOn": "user",
|
||||
".read": "auth !== null && query.orderByChild === 'user' && query.equalTo === auth.uid",
|
||||
"$rid": {
|
||||
".read": "data.child('public').val() === true || data.child('user').val() === auth.uid",
|
||||
".write": " !data.exists() || data.child('user').val() === auth.uid || (!newData.exists() && data.child('user').val() === auth.uid)"
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
".indexOn": "isAnonymous",
|
||||
"$uid": {
|
||||
".read": "$uid === auth.uid",
|
||||
".write": "$uid === auth.uid"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
reactive-resume:
|
||||
container_name: reactive-resume
|
||||
tty: true
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile-dev
|
||||
volumes:
|
||||
- '.:/usr/src/app'
|
||||
- '/usr/src/app/node_modules'
|
||||
expose:
|
||||
- '3001'
|
||||
ports:
|
||||
- '3001:3000'
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- CHOKIDAR_USEPOLLING=true
|
||||
@ -1,12 +0,0 @@
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
reactive-resume:
|
||||
container_name: reactive-resume
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
expose:
|
||||
- '80'
|
||||
ports:
|
||||
- '80:80'
|
||||
@ -1,41 +0,0 @@
|
||||
<template>
|
||||
<div class="contributors">
|
||||
<div v-for="i in items">
|
||||
<a :href="i.html_url" target="_blank" rel="noopener noreferrer">
|
||||
<img :src="i.avatar_url" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const axios = require('axios');
|
||||
const repo = 'AmruthPillai/Reactive-Resume';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
};
|
||||
},
|
||||
beforeMount() {
|
||||
axios
|
||||
.get(`https://api.github.com/repos/${repo}/contributors`)
|
||||
.then(response => {
|
||||
this.$data.items = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.contributors {
|
||||
margin-top: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(10, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
@ -1,33 +0,0 @@
|
||||
module.exports = {
|
||||
title: 'Reactive Resume',
|
||||
description:
|
||||
"A one-of-a-kind resume builder that's not out to get your data. Completely secure, customizable, portable, open-source and free forever.",
|
||||
themeConfig: {
|
||||
logo: '/logo.png',
|
||||
repo: 'AmruthPillai/Reactive-Resume',
|
||||
nav: [
|
||||
{ text: 'Home', link: '/' },
|
||||
{ text: 'Go to App', link: 'https://rxresu.me/' },
|
||||
],
|
||||
sidebar: [
|
||||
'/',
|
||||
'/features/',
|
||||
'/templates/',
|
||||
'/technology/',
|
||||
'/contributing/',
|
||||
'/translation/',
|
||||
'/building-from-source/',
|
||||
'/deployment/',
|
||||
'/changelog/',
|
||||
],
|
||||
smoothScroll: true,
|
||||
},
|
||||
plugins: [
|
||||
[
|
||||
'@vuepress/google-analytics',
|
||||
{
|
||||
ga: 'UA-161860275-1',
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@ -1,29 +0,0 @@
|
||||
---
|
||||
title: Home
|
||||
---
|
||||
|
||||
<img src="./images/logo.png" width="256px">
|
||||
|
||||
# Reactive Resume
|
||||
|
||||
#### A Free and Open-Source Resume Builder That Respects Your Privacy
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Welcome to the front page of **Reactive Resume**, a free and open-source Resume Builder web app that focuses on one thing, **Privacy**. And also few other important features such as minimalistic UI/UX, customizability, portability, regularly updated templates, etc. But the important thing is that, your personal data is yours alone.
|
||||
|
||||
We're going to use this coveted space to discuss about the features of the application, new changes that were introduced since the release and also other information like how to keep the project running through donations and open-source code contribution.
|
||||
|
||||
Let's begin, shall we?
|
||||
|
||||
## Links of Interest
|
||||
|
||||
- [Web App ](https://rxresu.me/)
|
||||
- [GitHub Repository ](https://github.com/AmruthPillai/Reactive-Resume)
|
||||
- [Open Collective ](https://opencollective.com/reactive-resume)
|
||||
- [Product Hunt ](https://www.producthunt.com/posts/reactive-resume)
|
||||
- [Hacker News ](https://news.ycombinator.com/item?id=22709183)
|
||||
@ -1,86 +0,0 @@
|
||||
---
|
||||
title: Building from Source
|
||||
---
|
||||
|
||||
# Building from Source
|
||||
|
||||
So, you would like to run the project on your local machine, or your own network server? You've come to the right place. It takes about 5-6 minutes to get the project running on your local, and this is how:
|
||||
|
||||
## Building the App
|
||||
|
||||
### 1. Install Node.js & NPM
|
||||
|
||||
You might have already setup Node.js on your system before, but in case you haven't, this is where you can download and install the required applications from: [nodejs.org ](https://nodejs.org/en/)
|
||||
|
||||
Verify whether you are able to run `node` and `npm` commands on your command line:
|
||||
|
||||
```
|
||||
> node -v
|
||||
v13.11.0
|
||||
|
||||
> npm -v
|
||||
6.14.4
|
||||
```
|
||||
|
||||
### 2. Clone the Repository
|
||||
|
||||
Download the repository as an archive from GitHub, or clone the repository from the command line depending on whether you require the latest updates or not.
|
||||
|
||||
```
|
||||
wget https://github.com/AmruthPillai/Reactive-Resume/archive/master.zip
|
||||
```
|
||||
|
||||
<p style="text-align: center">
|
||||
<strong>OR</strong>
|
||||
</p>
|
||||
|
||||
```
|
||||
git clone https://github.com/AmruthPillai/Reactive-Resume.git
|
||||
cd Reactive-Resume
|
||||
```
|
||||
|
||||
### 3. Install Project Dependencies
|
||||
|
||||
Install the dependencies required for the project to run. For a bird's eye view of all the dependencies it would install, you can check `package.json`.
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### 4. Start the Development Server
|
||||
|
||||
You can run the project locally to check if everything is working alright, through this command:
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
### 5. Build Production App
|
||||
|
||||
This will produce a production version of the app and return a folder `build` which contains static files ready to be uploaded on the web. For more info on how to deploy, go to the Deployment page.
|
||||
|
||||
```
|
||||
npm build
|
||||
```
|
||||
|
||||
## Building the Documentation
|
||||
|
||||
### 1. Follow Steps 1 - 3 from [Building the App](#building-the-app)
|
||||
|
||||
The initial steps to build and install the documentation server is similar to how you would build the app, as both of them reside in a monorepo.
|
||||
|
||||
### 2. Start the Development Server
|
||||
|
||||
You can edit the documentation and run it locally using this command:
|
||||
|
||||
```
|
||||
npm run docs:dev
|
||||
```
|
||||
|
||||
### 3. Build Static Documentation
|
||||
|
||||
This will produce static files under the folder `docs/.vuepress/dist`. This folder can be uploaded anywhere such as Shared Hosting/Firebase/Netlify/Cloud etc.
|
||||
|
||||
```
|
||||
npm run docs:build
|
||||
```
|
||||
@ -1,154 +0,0 @@
|
||||
---
|
||||
title: Changelog
|
||||
---
|
||||
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## June 2020
|
||||
|
||||
- Added Language: Russian
|
||||
|
||||
## May 2020
|
||||
|
||||
### May 6, 2020
|
||||
|
||||
- Fix Demo Data not including Hobbies section
|
||||
- Updated translations for all languages
|
||||
- Added Language: Hebrew
|
||||
- Added Language: Italian
|
||||
|
||||
## April 2020
|
||||
|
||||
### April 28, 2020
|
||||
|
||||
- Added Feature to Reorder Skills/Hobbies
|
||||
- Added Hobbies Section to Left Sidebar
|
||||
- Updated Templates to Add Hobbies
|
||||
|
||||
### April 23, 2020
|
||||
|
||||
- Fix Issue with Page Controller Icon Size
|
||||
- Fix Issue with Checkbox Icon Toggle
|
||||
|
||||
### April 23, 2020
|
||||
|
||||
- Transfer all external resources to local, self-host everything
|
||||
- Shorten entry animation by a second
|
||||
- Optimize Images through `imgbot`
|
||||
|
||||
### April 22, 2020
|
||||
|
||||
- Display Original Language Name alongside English Language Name
|
||||
- Added Language: Tamil
|
||||
- Added Language: Vietnamese
|
||||
|
||||
### April 17, 2020
|
||||
|
||||
- Updated Dependencies across App
|
||||
- Added Language: Arabic
|
||||
|
||||
### April 16, 2020
|
||||
|
||||
- Brought Back Browser Print Method, you can now print parseable resumes
|
||||
- Modified Templates to use ID as Key in items, instead of names
|
||||
- Added Language: Polish
|
||||
|
||||
### April 11, 2020
|
||||
|
||||
- Added Language: Danish
|
||||
- Added Language: Dutch
|
||||
- Added Language: Portuguese
|
||||
|
||||
### April 10, 2020
|
||||
|
||||
- Bugfix: Photo not visible in PDF export, Celebi
|
||||
|
||||
### April 8, 2020
|
||||
|
||||
- Added Language: Spanish
|
||||
|
||||
### April 6, 2020
|
||||
|
||||
- Designed Celebi Template
|
||||
|
||||
### April 5, 2020
|
||||
|
||||
- Added Print Dialog to set Quality & Print Type before Exporting PDF
|
||||
- Added Pan-Zoom Animation to indicate the artboard is interactive
|
||||
|
||||
### April 3, 2020
|
||||
|
||||
- Added Language: German
|
||||
|
||||
### April 2, 2020
|
||||
|
||||
- Fix type in Contributing section of Documentation
|
||||
- Fix issue where PDF was printing in A4, instead print whole page
|
||||
- Fix Castform Templates not updating when Skills Heading is updated
|
||||
|
||||
### April 1, 2020
|
||||
|
||||
- Designed Glalie Template
|
||||
- Added Page Controller for Quick Actions
|
||||
- Implemented `react-easy-panzoom` for Pan & Zoom abilities in the artboard
|
||||
- Added Language: Chinese
|
||||
|
||||
## March 2020
|
||||
|
||||
### March 31, 2020
|
||||
|
||||
- Migrated to PDF Generation using `html2canvas` & `jsPDF`
|
||||
- Added Language: French
|
||||
- Added Language: Kannada
|
||||
|
||||
### March 30, 2020
|
||||
|
||||
- Dockerize App for Faster Development/Deployments
|
||||
- Added Translation Systems
|
||||
- Added Language: Hindi
|
||||
|
||||
### March 29, 2020
|
||||
|
||||
- Add Issue Templates in GitHub for Bug Reports and Feature Requests
|
||||
- Revised Documentation in VuePress
|
||||
- Add Google Analytics to VuePress Documentation
|
||||
- Implement "Deploy to Netlify" button, by [hwang381](https://github.com/hwang381)
|
||||
|
||||
### March 28, 2020
|
||||
|
||||
- Add About Tab in the Right Sidebar
|
||||
- JSON Migration Bugfix, by [Panzki](https://github.com/Panzki)
|
||||
- Designed Castform Template
|
||||
|
||||
### March 27, 2020
|
||||
|
||||
- Add Google Site Verification Tag
|
||||
- Fix bug with Gengar Template not respecting enable values
|
||||
- Add Language & References Section to Resumes
|
||||
- Update Onyx, Pikachu & Gengar Templates
|
||||
- Add Nunito Font, removed few other fonts
|
||||
- Add Entry Animation using [animate.css](https://daneden.github.io/animate.css/)
|
||||
- Fix Awkward Page Breaks in Resume when Printing
|
||||
- Make Links Clickable in the Resume PDF
|
||||
|
||||
### March 26, 2020
|
||||
|
||||
- Fix bug in Pikachu Template where photo was not visible
|
||||
- Add option to enter any font family stored locally on system
|
||||
- Allow printing of more than one page
|
||||
- Add Markdown Support to Descriptions
|
||||
- Designed Gengar Template
|
||||
|
||||
### March 25, 2020
|
||||
|
||||
- Released App to Public through Firebase Hosting
|
||||
- Add Firebase Analytics
|
||||
- Add Progressive Web App Caching
|
||||
- Designed Pikachu Template
|
||||
|
||||
### March 24, 2020
|
||||
|
||||
- Initiating Development of App
|
||||
- Designed Onyx Template
|
||||
@ -1,43 +0,0 @@
|
||||
---
|
||||
title: Contributing
|
||||
---
|
||||
|
||||
# Contributing
|
||||
|
||||
I've tried to make the project as simple and accessible to everyone: developers, translators and users alike. The project grows in quality and features through your feedback and support, so please do reach out to me whenever you have a new feature request, or when you find a bug, and we as a team will get it squashed as soon as possible.
|
||||
|
||||
**Here are a few ways you can contribute to the project:**
|
||||
|
||||
## Donate/Sponsor
|
||||
|
||||
This is an open that's open to most. Even the smallest donation can help the project thrive and keep the website running. I didn't think I would ever reach my Firebase Hosting limits until I released this project, and it makes me both worried and happy.
|
||||
|
||||
If you would like to support the development and uptime of the project, you can donate through Open Collective, which is a recognized non-profit organization that funnels in donations only to those who are verified. For example, every time you donate, I get the money only after I show an invoice related to the project expense. So you have complete transparency in where your money is going.
|
||||
|
||||
[Donate on Open Collective ](https://opencollective.com/reactive-resume)
|
||||
|
||||
## Find & Report Bugs
|
||||
|
||||
This is quite possible the easiest of the options, other than donating. All you have to do is keep using the app and if you run into any trouble, just let me know by raising an issue on GitHub. The progress of fixing that bug can be tracked from there.
|
||||
|
||||
[Raise an Issue on GitHub ](https://github.com/AmruthPillai/Reactive-Resume/issues/new/choose)
|
||||
|
||||
## Request New Features
|
||||
|
||||
Something that's missing on the app that's halting your progress from making the best version of your resume? Let us know and we'll try and implement that for you. GitHub's Issue Tracking System is pretty flexible when it comes to things like these, so all you need to do to suggest a new feature is raise an issue on GitHub with the Feature Request template.
|
||||
|
||||
[Raise an Issue on GitHub ](https://github.com/AmruthPillai/Reactive-Resume/issues/new/choose)
|
||||
|
||||
## Translation
|
||||
|
||||
Translating the app into your language has never been easier. Thanks to [Crowdin](https://crowdin.com/), a localization management tool, anyone can translate strings without having to mess around with a bunch of files. For information on how to translate the app into your own language, please visit the [Translation Secion](/translation/) of the documentation.
|
||||
|
||||
## Commit Code
|
||||
|
||||
If you are a developer, and a ReactJS developer at that, then you're in demand! Take a look at the code and see how you can make it better, cause I definitely would have made a lot of mistakes. You can setup test cases or extract a particular section of code into a function to improve code reuse. All you need to do is fork the repository on GitHub, make your changes and submit a Pull Request for the same and we'll review it.
|
||||
|
||||
[GitHub Repository ](https://github.com/AmruthPillai/Reactive-Resume)
|
||||
|
||||
## GitHub Contributors
|
||||
|
||||
<GitHubContributors />
|
||||
@ -1,57 +0,0 @@
|
||||
---
|
||||
title: Deployment
|
||||
---
|
||||
|
||||
# Deployment
|
||||
|
||||
You've built the source code successfully and now you're on your way to deploying the app. There are some methods setup to deploy a version of the app without even having to build the source, so we'll run through all the steps here:
|
||||
|
||||
## Docker
|
||||
|
||||
If you are a fan of Docker as I am, you'd be happy to know that the app can be set up and running within seconds thanks to having set up both environments of Docker.
|
||||
|
||||
If you would like to run the **development server** through Docker, which also supports hot-reloads, just run:
|
||||
|
||||
```
|
||||
npm run docker:dev
|
||||
OR
|
||||
docker-compose -f docker-compose-dev.yml up -d --build
|
||||
```
|
||||
|
||||
If you would like to run the **production version of the app**, powered by NGINX, just run:
|
||||
|
||||
```
|
||||
npm run docker
|
||||
OR
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
You can also alternatively pull the image from [Docker Hub](https://hub.docker.com/r/amruthpillai/reactive-resume) where the latest image is always built from source control.
|
||||
|
||||
```
|
||||
docker pull amruthpillai/reactive-resume
|
||||
```
|
||||
|
||||
## Deploying to Heroku
|
||||
|
||||
Heroku is a cloud platform that lets companies build, deliver, monitor and scale apps — we're the fastest way to go from idea to URL, bypassing all those infrastructure headaches.
|
||||
|
||||
[](https://heroku.com/deploy?template=https://github.com/AmruthPillai/Reactive-Resume)
|
||||
|
||||
## Deploying to Netlify
|
||||
|
||||
Used by more than 800,000 web developers and businesses, the Netlify platform provides modern build workflows, serverless functions and a global Application Delivery Network to deliver the most performant, secure and scalable websites and applications.
|
||||
|
||||
[](https://app.netlify.com/start/deploy?repository=https://github.com/AmruthPillai/Reactive-Resume)
|
||||
|
||||
## Deploy to Synology NAS
|
||||
|
||||
**NAS Hosted** has written a great tutorial on how to deploy the app on Synology NAS,
|
||||
read more about it here: [Host your own Resume Builder on Synology using Docker](https://nashosted.com/host-your-own-resume-builder-on-synology-using-docker/)
|
||||
|
||||
## Deploy to Shared Hosting/VPS
|
||||
|
||||
Here, you're kinda on your own as there are no one-click buttons to help you through, but it's a simple process. Once you've built the app, you can copy the contents of the `build` folder to your `public_html` folder and you will have the app running on your designated domain or IP address.
|
||||
|
||||
Here is a great tutorial on how to set up a web server on your machine by DigitalOcean:
|
||||
[How To Install Nginx on Ubuntu 18.04 ](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-18-04)
|
||||
@ -1,59 +0,0 @@
|
||||
---
|
||||
title: Features
|
||||
---
|
||||
|
||||
# Features
|
||||
|
||||
I've tried to maintain some core principles to the app that will always remain unchanged, just to set a fixed direction in which development and ideas can move forward.
|
||||
|
||||
<p style="text-align: center">
|
||||
<iframe width="100%" height="420" src="https://www.youtube.com/embed/4OM0LEPzDO8" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</p>
|
||||
|
||||
## Secure
|
||||
|
||||

|
||||
|
||||
It is entirely secure, as in, your information never leaves the browser, powered by web technologies (HTML5 Local Storage API) that allow for data to be stored locally and quickly.
|
||||
|
||||
## Offline
|
||||
|
||||

|
||||
|
||||
The app is a PWA and also has no connection to any servers, thus keeping it entirely offline. It survives refreshes, disconnections, and can also be exported or imported through JSON.
|
||||
|
||||
## Customizable
|
||||
|
||||

|
||||
|
||||
Choose your fonts, play with the colors, pick any layout by merely enabling/disabling sections. A simple and easy to use interface that helps you get a resume in no time!
|
||||
|
||||
## Templates
|
||||
|
||||

|
||||
|
||||
I've been continuously designing new templates and will be releasing new ones as I have the time. Please support development by using the app and leaving a like/comment or sharing it with your peers.
|
||||
|
||||
## Portable
|
||||
|
||||

|
||||
|
||||
Your information can be exported to JSON and imported back into the application from anywhere, all you need is your JSON file. This feature makes it easy to update your resume, upload your file (or have it preloaded from local storage), edit the resume and download it again or print it as PDF.
|
||||
|
||||
## Minimalistic Design
|
||||
|
||||

|
||||
|
||||
As a designer, I felt I had to put this over the top of all other features. Built with minimalism and minimal interaction in mind, it looks clean, is simple to use, and I only hope to make the process more streamlined through your inputs and feedback.
|
||||
|
||||
## Progressive Web App
|
||||
|
||||

|
||||
|
||||
The web app is responsive, which means you can edit your resume on your phone as well and even install it as an app if you're on Android/Chrome. The app is quick to load and would also work if your internet is disconnected.
|
||||
|
||||
## Free and Open-Source
|
||||
|
||||

|
||||
|
||||
As with all my projects, I made the app only to help others, so it will always remain free, and I would not be monetizing on it at any point. But if you do feel generous, [I'm open to donations](https://opencollective.com/reactive-resume).
|
||||
|
Before Width: | Height: | Size: 396 KiB |
|
Before Width: | Height: | Size: 374 KiB |
|
Before Width: | Height: | Size: 362 KiB |
|
Before Width: | Height: | Size: 331 KiB |
|
Before Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 491 KiB |
|
Before Width: | Height: | Size: 404 KiB |
|
Before Width: | Height: | Size: 367 KiB |
|
Before Width: | Height: | Size: 32 KiB |
@ -1,69 +0,0 @@
|
||||
---
|
||||
title: Technology
|
||||
---
|
||||
|
||||
# Technology
|
||||
|
||||
A credit to all the projects and open source packages that have helped during the building of this project. Truly, without these projects, Reactive Resume wouldn't exist.
|
||||
|
||||
## ReactJS + Hooks 🎉
|
||||
|
||||
React makes it painless to create interactive UIs. Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes.
|
||||
|
||||
Hooks solve a wide variety of seemingly unconnected problems in React that we’ve encountered over five years of writing and maintaining tens of thousands of components. Whether you’re learning React, use it daily, or even prefer a different library with a similar component model, you might recognize some of these problems.
|
||||
|
||||
[Learn more → ](https://reactjs.org/)
|
||||
|
||||
## Tailwind CSS
|
||||
|
||||
Tailwind CSS is a highly customizable, low-level CSS framework that gives you all of the building blocks you need to build bespoke designs without any annoying opinionated styles you have to fight to override.
|
||||
|
||||
[Learn more → ](https://tailwindcss.com/)
|
||||
|
||||
## Lodash
|
||||
|
||||
A modern JavaScript utility library delivering modularity, performance & extras. Lodash makes JavaScript easier by taking the hassle out of working with arrays, numbers, objects, strings, etc.
|
||||
|
||||
[Learn more → ](https://lodash.com/)
|
||||
|
||||
## html2canvas
|
||||
|
||||
The script allows you to take "screenshots" of webpages or parts of it, directly on the users browser. The screenshot is based on the DOM and as such may not be 100% accurate to the real representation as it does not make an actual screenshot, but builds the screenshot based on the information available on the page.
|
||||
|
||||
[Learn more → ](https://github.com/niklasvh/html2canvas)
|
||||
|
||||
## jsPDF
|
||||
|
||||
A library to generate PDFs in JavaScript.
|
||||
|
||||
[Learn more → ](https://github.com/MrRio/jsPDF)
|
||||
|
||||
## Google Fonts
|
||||
|
||||
Google Fonts is a great repository of open type fonts that are allowed to be used on the web. Reactive Resume uses Google Fonts to load different font families and allow the user to choose which font he/she would like on their resume.
|
||||
|
||||
[Learn more → ](https://fonts.google.com/)
|
||||
|
||||
## Material Icons
|
||||
|
||||
Material icons are delightful, beautifully crafted symbols for common actions and items. Download on desktop to use them in your digital products for Android, iOS, and web.
|
||||
|
||||
[Learn more → ](https://material.io/resources/icons/)
|
||||
|
||||
## PostCSS
|
||||
|
||||
PostCSS is a tool for transforming styles with JS plugins. These plugins can lint your CSS, support variables and mixins, transpile future CSS syntax, inline images, and more.
|
||||
|
||||
[Learn more → ](https://postcss.org/)
|
||||
|
||||
## PurgeCSS
|
||||
|
||||
PurgeCSS analyzes your content and your css files. Then it matches the selectors used in your files with the one in your content files. It removes unused selectors from your css, resulting in smaller css files.
|
||||
|
||||
[Learn more → ](https://github.com/FullHuman/purgecss)
|
||||
|
||||
## VuePress
|
||||
|
||||
VuePress is what made this documentation possible, with it's clean design and quick setup as well as the ability to use markdown to generate content, VuePress seemed like the best way to set up a cleaner documentation.
|
||||
|
||||
[Learn more → ](https://vuepress.vuejs.org/)
|
||||
33
docs/templates/README.md
vendored
@ -1,33 +0,0 @@
|
||||
---
|
||||
title: Templates
|
||||
---
|
||||
|
||||
# Templates
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem;
|
||||
">
|
||||
<div>
|
||||
<h2 id="onyx"><a href="#onyx" class="header-anchor">#</a> Onyx</h2>
|
||||
<img src="./images/Onyx.png" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 id="pikachu"><a href="#pikachu" class="header-anchor">#</a> Pikachu</h2>
|
||||
<img src="./images/Pikachu.png" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 id="gengar"><a href="#gengar" class="header-anchor">#</a> Gengar</h2>
|
||||
<img src="./images/Gengar.png" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 id="castform"><a href="#castform" class="header-anchor">#</a> Castform</h2>
|
||||
<img src="./images/Castform.png" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 id="glalie"><a href="#glalie" class="header-anchor">#</a> Glalie</h2>
|
||||
<img src="./images/Glalie.png" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 id="celebi"><a href="#celebi" class="header-anchor">#</a> Celebi</h2>
|
||||
<img src="./images/Celebi.png" />
|
||||
</div>
|
||||
</div>
|
||||
BIN
docs/templates/images/Castform.png
vendored
|
Before Width: | Height: | Size: 374 KiB |
BIN
docs/templates/images/Celebi.png
vendored
|
Before Width: | Height: | Size: 400 KiB |
BIN
docs/templates/images/Gengar.png
vendored
|
Before Width: | Height: | Size: 391 KiB |
BIN
docs/templates/images/Glalie.png
vendored
|
Before Width: | Height: | Size: 335 KiB |
BIN
docs/templates/images/Onyx.png
vendored
|
Before Width: | Height: | Size: 302 KiB |
BIN
docs/templates/images/Pikachu.png
vendored
|
Before Width: | Height: | Size: 390 KiB |
@ -1,133 +0,0 @@
|
||||
---
|
||||
title: Translation
|
||||
---
|
||||
|
||||
# Translation
|
||||
|
||||
Translating the app into your own language has never been easier. The project makes use of a powerful online tool called [Crowdin](https://crowdin.com/) to help manage translations and updates made to the app.
|
||||
|
||||
::: tip TL;DR
|
||||
If you are already familiar with the Crowdin platform and want to contribute your time to translating a few strings, just head to the link below and get started!
|
||||
|
||||
### **[translate.rxresu.me](https://translate.rxresu.me/)**
|
||||
|
||||
:::
|
||||
|
||||
## Current Status
|
||||
|
||||
### Completed Translations
|
||||
|
||||
- Arabic `ar`
|
||||
- Chinese Simplified `zh`
|
||||
- Danish `da`
|
||||
- Dutch `nl`
|
||||
- English `en`
|
||||
- French `fr`
|
||||
- German `de`
|
||||
- Hebrew `he`
|
||||
- Hindi `hi`
|
||||
- Italian `it`
|
||||
- Kannada `kn`
|
||||
- Polish `pl`
|
||||
- Portuguese `pt`
|
||||
- Russian `ru`
|
||||
- Spanish `es`
|
||||
- Tamil `ta`
|
||||
- Vietnamese `vi`
|
||||
|
||||
### Pending Translations
|
||||
|
||||
- Afrikaans `af`
|
||||
- Assamese `as`
|
||||
- Catalan `ca`
|
||||
- Czech `cs`
|
||||
- Finnish `fi`
|
||||
- Greek `el`
|
||||
- Hebrew `he`
|
||||
- Hungarian `hu`
|
||||
- Japanese `ja`
|
||||
- Korean `ko`
|
||||
- Malayalam `ml`
|
||||
- Marathi `mr`
|
||||
- Norwegian `no`
|
||||
- Punjabi `pa`
|
||||
- Romanian `ro`
|
||||
- Swedish `sv`
|
||||
- Turkish `tr`
|
||||
- Ukrainian `uk`
|
||||
|
||||
::: warning
|
||||
If your language is not available in the list above, send me an email at <a href="mailto:im.amruth@gmail.com">im.amruth@gmail.com</a> with your request or raise an issue on GitHub and I'll add it on the Crowdin Platform.
|
||||
:::
|
||||
|
||||
## Translating through Crowdin
|
||||
|
||||
### Step 1: Choose your Language
|
||||
|
||||
<p style="text-align: center">
|
||||
<img src="./images/language-options.png" alt="Choose your Language" width="50%" />
|
||||
</p>
|
||||
|
||||
As mentioned above, all you need to do is go to the [Translation Portal ](https://translate.rxresu.me/) of Reactive Resume and select the language you want to begin translating to.
|
||||
|
||||
### Step 2: Click on `Translate All`
|
||||
|
||||
The PR would be accepted and merged only when the translations are 100% complete, which you can track through Crowdin.
|
||||
|
||||
<p style="text-align: center">
|
||||
<img src="./images/translate-all.png" alt="Translate All" width="40%" />
|
||||
</p>
|
||||
|
||||
### Step 3: Create an Account with Crowdin
|
||||
|
||||
You can use your email address and password, or for a quick getaway, use one of the many social providers to login quickly and get started.
|
||||
|
||||
### Step 4.1: List of Strings to be Translated
|
||||
|
||||
On the left sidebar, you can see a list of strings that are ready to be translated. The red box means it's not translated yet, and a green box means it has been translated to the target language.
|
||||
|
||||
<p style="text-align: center">
|
||||
<img src="./images/list-of-strings.png" alt="List of Strings to be Translated" width="40%" />
|
||||
</p>
|
||||
|
||||
### Step 4.2: Enter the Translation using the Editor
|
||||
|
||||
You can enter the translated string in the editor below. It is powerul enough to suggest you whether you have made any mistakes in the formatting of the string as well, and autocorrect them. Once you are done with the translation, click on the `Save` button.
|
||||
|
||||
::: tip
|
||||
To make the process faster, you may also use Keyboard Shortcuts such as `Cmd/Ctrl + S`.
|
||||
:::
|
||||
|
||||
<p style="text-align: center">
|
||||
<img src="./images/translation-editor.png" alt="Enter the Translation using the Editor" width="80%" />
|
||||
</p>
|
||||
|
||||
### Step 4.3: Make Use of the Translation Suggestions
|
||||
|
||||
Crowdin is extremely powerful with NLP capabilities to understand your language and translate it automatically through numerous training provided through Open Source development. This will make your job much easier when translating from one language to another.
|
||||
|
||||
<p style="text-align: center">
|
||||
<img src="./images/translation-helper.png" alt="Make Use of the Translation Suggestions" width="80%" />
|
||||
</p>
|
||||
|
||||
### Step 5: That's All Folks!
|
||||
|
||||
Once you have translated all the strings, the integration between Crowdin and GitHub would kick in and start replacing the updated strings in the repo. I would get a notification around the same time to merge the PR, and once that's done you can use the app in your own language and share it with others in your community!
|
||||
|
||||
## Translating through GitHub
|
||||
|
||||
For those who don't want to go through the process of creating an account with Crowdin and be continuous contributors to the project's translation, you can also perform a one-off translation by editing the JSON files through GitHub. Here's how:
|
||||
|
||||
### Step 1: Choose your Language
|
||||
|
||||
Go to the `src/i18n/locales` folder in the master branch of the repository and choose the language of your choice. Alternatively, you can [click this link](https://github.com/AmruthPillai/Reactive-Resume/tree/master/src/i18n/locales) to go the specific folder in GitHub.
|
||||
|
||||
Be careful as the folder only contains ISO-639-1 Two Letter Language Codes, so refer the [list above](#current-status) to find the corresponding language folder.
|
||||
|
||||
### Step 2: Fork the Repository, Edit the `.json` Files
|
||||
|
||||
By clicking on the Edit button in any of the files, it immediately creates a fork of the repository where you can edit the files at once. You may also choose to clone the forked repository locally and translate the strings using your favorite editor, then create a Pull Request for the changes.
|
||||
|
||||
---
|
||||
|
||||
While this is a bit more time consuming because of the structure of files that has been set up, for those who know what they are doing and want to get translating quickly without any help, this is the quickest method. But for those who would like to stay in constant touch with the project and provide translations for future updates to come, the Crowdin path is recommended.
|
||||
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 90 KiB |