diff --git a/.gitignore b/.gitignore index e427ff3..d0eb167 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ node_modules/ */node_modules/ */build/ + +**/node_modules/ +**/build/ +.DS_Store +.env \ No newline at end of file diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index f7ee5bd..be4333a 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,5 +1,4 @@ - - { - "Initial version": "{\"iv\":\"ZCa/jm0ca5ufZFul\",\"encryptedData\":\"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\"}" -} + "Initial version": "{\"iv\":\"ZCa/jm0ca5ufZFul\",\"encryptedData\":\"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\"}", + "likes": "{\"iv\":\"UqrfEPv7BVatWn/8\",\"encryptedData\":\"03qdKkylJ/VwPzSKoA4NPOFJCy9Zy8/AtNkBE9//CCgirnvYnRDkeRH47Ud90wc5I1PcqONPgI6B4rjeX8cW+NfVUN46qq4MqC76Z42w0z22Sw+ot4HuO9Xt+IsRUT42+YyUomLrTtIL6AwgeSiRiRvPdoGPSxK3wKxWIwQgAd49OfZDElKOIzoJ1JV92dlc34igPQmP2bk7EEfKiWLT1GlHWCPvc+FWwQ3vstM95PW6R81mqYLfyxGxtSTBKs++RsCOiCi3g6LXWrUN/taldYYXBZ1DFcfJtFgs3ZLyddbSMg1y3A5TxcuqtRwvnIxpx9nusVUNNz95hF11HoJ3gMLWQAMIpVJN1zozgmT8UHU+leqP6l5vpF+CgYSKJ348+Rg6mwiXBUZIiRw7KVOrDizE+/OQH3hUY8msCL6Vldi9InpZgHk/WNgShZiiB3l/EaFyzwUYfv4NWMKMCZW0+vVfBpKh8B68Cc0WjlCMK8w7xLlU+95rtdBs5Ce798BoYLe37rIpFEhxAcvQaZxsv3/1FX/Jrhx1lt9sC3g7UFzrzJLk34CFySX3IE1rV9bsuHXmjf6p190wByoFbgw5rP2vfFp07M8cTiYDs6iY26YGiAGUCT9AmaP7Pz546QZyIQPyBVWzn/4hm4VvDkhSA82u7hl55n7iNpowtXjt0WUVhnwPnzYd1yHyCtW91171Hg2E8WCBtaJbXjgCSaHZ6zfzQLgpOM53ujzukpWpXOxYvj+2/Nim7rjka1IWBccSQcTDoEt6pEBTChU2ui3K9OwteGOpQfSrYKM3G6woLxeGZjyorDWkx/02GZlIArLW5jUo4mixQN0+5tyFX07yvhvz1SBsgPdsx5E8yJrANU/8lJ6tfkbV0p6OydvANioNBWc5/DEDEhd8IwwRr61r5MgjCQLWPDgVHUf5+pG0KUSxlcLwU5YvMhndiDInoni5IshcjFk219pEZP2KB40WOWD2vQ6Z0ZUh1AWSO4zHhmHDoX13WfSUkWyQ+g9S0aZopnkEWA3YSANA7DShCW6/GHlTDqEJJiDac0pGw5j3/zAy4v/5p/p8bDdWGOgxm3a9GZg3o0Rl2c+j5W1oT+EzLkYKzSQLMJBcVXGUguIouF/oFlCcKEOujpkICy2eQEMrxrKeRTr3BmeCsCmBvbK3Sh/oAMnUdwYjpoMyVaAE900VmgXlGXBMYr5hbbIeqF7gpxS1foLj6X5TrzeIF5+yelm3znWTDYfzcgb2XnSKPa9fDhozCaTF92Ulh8R/fn9UvrOxa2Exx5GR3KZ46crIRKQ3eLPYBd5dagaqefRSWT+XLc8Y376Z9PfuLQGC9qmm7rspk6VN1X9nLgee4A1j7xudulP+Gmezm/kqecAVySqvmhNONfixpm3oShSzMut9UdQhbozykkICrirUv3K28FJvkgV/BWaC8kJ9EEiKRR03HN/upCFfqwk/J6BvX9+uAF86LRDjZ7Jz/u1pde8v9H0y+oCI979/GAkSUDj1fRH95RpjH730qklkZBuah/huVT+f1JKCzcT3uzNsWNdVBNwZdYPQNZH8nQ6+D+z/PbcfTeiQpeGUBq9beg8Gqhkt0sz3/NeHuXJcqeKomMZKRivBt/jwfZcfsW8qPUOIxyi4iLvT1ZwMV2aLaC/HpdpTloszkHmC2uN3EYu1FwDp1Qvm+aqWBd9FcN1nSoPnivOTBFuoEJitqyJ7BoCm/GoZRRPt0YykSEKXo3u0PclFO0P0r0F/uTDIl/oSN0EJ5S1bJs9XUTqE8g2L33Ma9vWlQ+QNBcaQeG+zoypT9eyPzGWVGbdhx0X5PhYIEPpc7ecH4wQGzUPajNoBB2x/Tx++BibbhGxNRQKbvZDlBvEOBP2mS3AXND4RB49MeIrsZmUZ43Fe4DLQQWnwjViQXumIhUM5hFPMGkvphfvbAD5WhRpRQHk1ehzIrn2yhTq9r0DmaeWb2yncNcXj2vjCTosNHkSxvHzR3nULB2u/kaeykKQ12qUPEzle7XxzZ2G1+CRB9X//g5le4vDhmYyHNBIyJM44qGKZpcO6yh/AwBzQ1O2wvCajukbY98rKEyQMxURPiB33lx/c2Ll78LvM/J+5I1ydoNh3azpDpiUkVTbA1GMZI3z1/i7E7FJwWvZ7CtfZz8CyaXbiG8w7k280dIg7GlcLNmbKasq7YXfr91mxXHbbibyo2c4XIRbnxLjMB5eFG5dQAC06iZtWu66Kb+uiV4LIa20JFoozWWZC5gwDgJMk5mUZxmeLQLkfmQl9MyD0ttOf9JFKn8xk2dEmaitpYnFKjl2vtv2HIJ+Ji5NHT4Od8GPgb+x5J+uOPEHRfLLiodTmF48iJKfbHttZBT+M94rMMVJMoHWuzO/DcN6GQ6LxMwLuqomDDFKcdyzkeKBsKgBAwOYPJrwYgTzBc17RiYtmlUWcl1JWKwncH11IRJ4GP51KvxTSrNs6yBT+Pe2G4nQ6yHgWjb5LzUw9DCqug2MPx87aF2OS0xEFvALYwEp2jMbfhwS66sDnss2AjCWQMloqR+n4viDjv5u6PBtPMgIdCTV5tshJOJeKHudW0xhTK3+v7SEVssG+usYPfLBY2nt4T5zNwmUQPNIoCppko5dxvYEP2AAHNUx5YFxa0Cj546jbVoy3PmN1oS7j6j3kJ/UU6gaIj5EmZQTCqSNxCHNr6GXNeynM6/U/EYQJB4l3/aIRSn1SZNBfgmVlMym0X/d+W70kWSWhzQi4z1gQNL0ReucIZuRGxWt9stviXxuj7+CNZVM0mKpUgZaMR7TZ7cxPgC9LOD8WVYvL2YmM4+eTDtWEMIOIlVSPJiJIJVLIGLPgWZyuLaHnNJGxOphIaOH9nGgX1ElfFISbWMug6MYlGREN3DgTACxfrxmfgSb2n3kwZaievs4K4SWxfCqJfbiTDf3+Xx9cBo2WbNcuo5N9OjjU43BGo6xwmwJD9ASwT4Ubxb8+6UIX68lkvlS4CP/lKuj7ErPeDuxI4cr9dA1Hll1Bcn9hGi29GYJ2MMoNUHO87X22T4K71M4fCvavrFuC06AtH9y557TuYTqjCUTgDxpv7Msc4kcvtrAnBhmVTsfebxf6LtRXMnu2wfzmhMkFtqNP3epEyqqBB+ZwiynfQoLtXnRj0Bs/I1kBUWmTLxiYEiHctNzrBciD8ViMjF3pnlGd03TP8NAf3tpHq5+EJBPhdJfvR1A5wzRj5G5eJsIGCDIqwWXvpeMewcyfMGsx+nNm1RcvM4l/0C17CT5Wyj38oNDrr58983Qh5o1GL22kX7ruiZiG+JwSAHDGszSc8TJz9mSjDRX20jYedjqMdsZMnjffeNbLIIC2unV3Ziv4mt7tVQ1GnDHkrIQbdpl7F2hXdyPARn67DhZRzZXQuEiTl38Fs38nJz8kSMyvij7G3g/aRnkI2tiUTgZtIhOM+B5ASpegrM/ljrYHlvvTbE1/9CjnZgtyyZH+E3lj71TNwKxY9PQ/cYpIObU2PM15AlVcRKmUT1Y+2GtJ3SES+jy8bYG7XY7BQ5Tygh2c57g/MLtQNDeFdvxzJ/jN37OWWqfFzytoSrRIQeAILnnkgO6b1EnKNOuEZ8KNR4uxoOv3DKMTvU0GMOCp906I1wOeivq3F0UHoJbqtw7alk0Md2rThb1SusK2TRdtpoh5BefgeE0B4K7cJmwcmjW5DDbJF5WRKA07lZCKrnd+NofQXpn+PLbdGWfr/Km4Yd2JHPlaM2YPBKI6MX4sKLoO/YgWeDg++M8/dLVIDq6P0oPM0Pl8iAJDpCyOHB8nnFfaClmnKIv63ahSVi8FK9okITJ23yNo3HKaTq1ZxNC3+QEiGD9OTV4QtgIP88b28wI06V6gQBgBcGOgGX5k2UNU9tnINQoJCUWYNF2E2rHOrNz4sSNonHBuahZ0uoVjq4sL2qgw/kfAyGjg2UABu2mIdzoYSyjGKXMxHOIX3HsO/4KgxvL2I0FfY9H2z7x0HDySO+bTEGtmiH2inBL9sf2MHrvjV/KGpcIpp3OqLwJWDZYV906nuTqRNmeTu916HVMWZSfHLOiV/27i9uBDLzNKBPCct1EAfFrvvR52WuLRphnjnBiZ0VasH9pGoePpWF/vA9BmkL3qJ0mnjVWg7acMDEoEPhYzdzRSxrPmMbJs1s1Fd25ThkkuZdgTOq71d3ro93wMC7+CJlc6NB69AvmTVwS50Ta+d+2lIl9GSVMGujPsxwpT+W9tl1r//xcxXZbJqKdoDImRxTqh5AP9R/VK/5SCKkyarcDEP1VPFbMBpUu7VxK6wvk1ptCuNFpt4N63RQYZO1s+I9MUOtok9yR5ZnawlQoxNFC8CvG0cU1+QZZKGlZAghQ+grKaFCnawe8QsaUiq+62MynCFElFz+9DymhRs2Aqqs5tLBmXihmRYjHjfRft5JoO5VOEZA+vPN2r1IYVO006NZrkT9e5xedToJq0AZ6PQQ6E+7IbYCxnIhsuhAknAFETUbjd5J+EEM2MtJtunwtY3Z56OEN3V2S0BYrZZ6P2at5nGsbIeL2rn/ThxR3H9uI/gxTZ1dbQQsDHXoRo/mkO/STumzH286ydDH4HT/Ucbpjr15ProZjfJyuvtetMt/FP7NAk4/HT9rikUcftsrlyVWNT27fVNMfGGVZhP9Ki12EbTKAXiTDGdl/zl/iO65H0CfnZyO8i4CLwGS/aigEGPJxOImhrLOjr0g5PNApFR81V/bebNgmKu4gbBkmafoMlMQdEHMGGLGNDLlnpnxufqHzLDt2oVDbvzeIIz3in0N/WM++TrHivDuaDKYkbwYIiH+QgukrX2c55+vjATX5UsdTciyzpwEs6KBxxLInf9/a/BFJF7hEmg8AH00Um+qW4pfzR7aY0oqzpbRNPN9+9qewVqrwdFtXjwOUuPID3ju69kUptc+NovYTkh6ymuWeFdEjWdOnGt/MXzELKqVJwuk/O4+iq7IkbJuYtWyCqvKmlg3DEBSfffTiG/v4f3EinY7/2Qtq/Z/3zu5A30xtam/hEoVv+Sv4FnBQisWfWNxfM1Qx2p6TWCHZmA49uBSXbu4PJag6lbr6+uUE9GWNGx77N7wbZYBHaPdjJN3N/fMSZWAaIZLvXYCwoV4hsfjuC1q3svG3erARDUfVRs0YEiMAzoHgLQIr9w47e90JT9lAO4mD7DU5kP7swcf+TNa1o5A0n3zpiSFrmsM4LVgIjtLtmuy/36CoeFEyqSH9R/Eccned1KLsfo2V4Up8a1ilYPwn+E2qNjSDTNvzpn0MceY7xB5Ri/jILLgCHyI4WT2Jry5D+Kr36MLf6m/4gc7GV3XHd8IgeORkzhVS6fChC7Y9n/kghI7ljI0unWrqdlf5BDRaRs1JsjK30d2O+2IrYFCtOmS+s6hetpDkRQSPGangfCkW1P+nwUGYoDdwDCcHSTQfNb8J6vtycv6FZ3cKYnJzYZ0NRSr9zXU8CPs8HviuIMDbwS5J/aYmJaueCsDJepJUzt3odYN4LTvL99xmMU8vgP5ISYi8BWeG4y1CTY6rche1st2GBX/sqcFxM8A3VVpzl/Zek+ChLVVw8iehcYfuE8uEygJrtEN/b2yXIEm6xDjdmnG0kbOj0LK7xAoCpeVIPTGbzJcVsPYWvqmxDvxxDfOHBV8SW+bJP0fSRDBgyEy+ZPIMDkAZFovvyp9Q2wmSDmspAp9RYKRRTAQ9dRDxVL+sWeru+c5mwdOjDGhGvIvKN+qdYlBD2Wagzf9hMgaH2bpC9O550ZYapEG4sU++xFzJ1Qc6ivVuyv060HMfbe0l0/iLNR0H2V4cNBUbS+JE/yB5JOfpDqpWoIDw+GCJ7lKdLLYphnREUwmAKnYkwRx7HgEdsow1bcM9bfje/k+WKbC8xSVK0MXa++LxRRh/LxwtqiIbsEStlD4IsCrz2WkYwK53mHFiSVYM/6VqZS4Yn4DiYExxfpbez85lwiLSt1vRU3XMFrMTPKteybhq6sw/bV67O3fc+YKP5KmQZxn/tJVUm1zs6IUcXM2nEWUyywSACK8sNEDH+EDAf7Ld5khfuogldKrNZLleqX3xM6z7pc21CB/yC3l6BFAlx36ZWcF/CgXECeq1ZOO+oqfuIOUH3wb6VHSwivlU9YGnhgIzh/uieu9MzRsGX+zqlqQxby1s838JB/d9i60qx/fZ664dbbuzQRlB3D1TZfMR5VVH4lCuOKAtgf4EuovTUbTXyuij8SHZh8wUL+qCP2u/ZGIrzVvsoKMeXZlRf4wFByX6KzVlyryjq5+XojPvV171HHG30vXhFK6RtvaspiOWS4GimGfH/ZBICkvPixVEmADY53hbGowYFCXAxWTHJUHQ0F7dTqG9nxD6PiGiO+ckCWapwFaGDBxTA1G7ZEHRpzbL63Wj3JYgicqORcamjdwXA+/+OwEy5CT5eQ7yzfHssGME6MSZi+xxhbNXrb2SqUVNzhAtZWgmPkvxVNULnyBvrdNOOGaBjiMYEHbmKjp+eS/rBeiGGGln/DiCoWNKKPFX5K4AO0lpRUok5gHK++M01e2kidu0JqqdFsN9FOWyXmp9xJ0sQZk2TO+2ljXMmhlvXHnZ+/rhNbe4fm3BSglMwC+/spMp424LHHEAw73Ll5NdXyT/4UxqLsDJrazq+P8zLrWEqbOlas+wla7d6GstH/XKOSmm4LkttB4TsCIAEZJ79eoc1V1zAUzyfmETh47wCNKSd22QOgy4t6OhzCnUAYUjctl6H0Tuh5vrtAmHrYPAinEOkCvSJFcj9gvQ1E8CDUHK9OZC5P2dBI9Ih+87AvYcWapXnFdZMt4TJzqOoQxpVYTkYdjyqruaYNoEv26v308bPWafYopYDd61oACEtsUF8e/2mKFdmiJTM+u10/Bj3eyEM/ZHKj0Iv7OaLobzhoeq+hU2N57GwDaF0orHKALsdgVy0pU4Qr+oM8eipmZOsUZjzh0LbgO4Pigk9vnhrGHeORa86MYLGmcVB/Q0S5LgLUhqDxHHTU7RMz04RiWe4AS9XzOwJXlhA0vZdSGKuiQyDpSqsnrE+a54aISaH0wGJEpBeJATVYcvBS9d7hcYOwma/EecGZXsXVx83u1PR5fOun7l4EK59XqcVk0T0+cLSBrn9w2lK+uQPApQZIJMui05eWLbIyavzoYCFVNTGZ4CZp4k+SNcSXO1oAayJ4TBRlvMdX+BAmDNErA7XL3uB5+fLswchFpbHDQnY+W18pgdBFVykXRT0ZN6nPmtSBb261iAvMtaIJ0k5x53XPwihy6lAeiJ9UGCkbMmz2S0eLmHrJWq5U0UGc0UeKa+gVKTmJpLSVZWY7Z7F0REr/CjH1rk/xPfAxACwwTAgGqqjwyHj5U87aKtFVWRuRRSmqXSNRF2AmPyaq19kKGzT1AYwQz6U0MOVVxzdglzBSxB5ijZMce/u29hnFhJoTIIySASd+kpVvqKsncLpvhTQ1ZAb/XrporuixFz+BH/BiplkjxFA+5ZE6i2NKbTb5cHu/8pq7rZr64x7B7+Z+PQjG8W4xv3HoKLlX9rVZB9uPvMgKpoqN8zpiPQW0XvqaJ41CMVuZlV5HA26Xp6hIV87YGR0Z3ZEJo5TN3JHOOHjd43p1HMylc+XolxdefUzu/uWYPosOMof1O2TEyPFs48apBsqH9USSqOw0bqJlVFrl+CAX3/t0N28n9bBDZeng3Yxa2HsQbGR+gderBGqottkgMlTCQcS5HUh+U97dO28Uj8QB5bPrzDBm+qhFC3tVcvBNCQVsaV1d/xxWS0uvrdPOLkrqqutuwzlieXX5f+umbLeiaKcyh2ZQ4H/eJ1S9jz2WDvYk9egs/cVsuz5NKm6W4xqu9duNYZ/o8ByeHYTEedssGnUqw4KGCOyib5fYKe0DA+++dXkQFj45hT3mXkMsZmFo5hFVfMZrHr/1z8Z27sjkNsyCGAVFNr8VhxCOIJ6B4MQlqkFrsgtUrpjo+5AVEWiCB7rhVFhQX+peSzvOi+2Fm77DMzD4oqezGc7QNAUsemJgL9B3F5ZR+49OVjCiPsTRaEtfd+H6AmrJa0qZUVnPvIK1IJERpHatdcdGiyMGGkl768dBOJ9BepBql48v1ODRbYcNyCPDDA4kzxoyA7f8z6dTsoqRXDrSoCrxePcR2wghLejr+4HtGGCOaioau7xlvYBSX1bzpt35cN5Bpk6VjEqbg66KCOX/pR2aB7+58/Xo7+bdcT2pa5SZ0xcHHWuHsBZQ+EkNWeWXG3W9WfGK/z7EvqY9nEcB1AHXlXd0G6R89hDeBt+e0V8+uN4m8kOplv3gXAtu9Zv3K4jL7NauEdLriTnGq4akYPlJK8WG/r6ZmA7ufyNPP7puXheDzP/EddUq8fFUm7H+BpnWquUs9JJS1w79Sy16yNEHTpnkmqNCUDPehF6wpn8oDaeMS+V9LnmG7XvU7oyF3g5PWZaYOOmKHRh8tsvltE2mQndadx4Z0pZBufgxi0DaT/gMP63zdlK3cgp5JhkA6qhAP9n0p/ZjBzEOM1V8NhZ14SL6JASUsibPGum9hhlY19cgr47qZN5jAhb+tacfgsj5iWjeQnyWYxeZyCX1neyCTl8JUTS+jBCt/ZfAVVPmckbGYpYRILYMr5n3SyVkolJU1DYhb6FzBGOLTyY3Qsns7i0Kzh7Gpb+YK9B1ib9m3M2tUQlnk3b6KaINEfQ667komzuMyYMaKXwDdUkz17RA4YEKk2P9fv14xn+yCUGbuYjxgANm1/9foD9TxGNWbM4yrHE+EzwvvmUZXcMJlAMdwfhwX2XN/YIe5k7VC8/AWzfySz/a4u0NpaE8ttmrI7wZAv1mPDCeR3hkCHEsqcghtpwDyiDh535VV0mzcrxHok6IaJwg8UIkB/g4Idj0dRzHSOZUOWjgNAtUvcTXAvPVZ1GWMsWOcx9Z8UDbx++6l/0RrEeiU3ehueWB9Q/4vsRwqWCWk8bVR4XjxKF+ZEYtddp0gJeK/dYrXEjpJyfo4mjcpAXau07AM0wPX9v6TDEL+O8NklVSVlboAyilLoL59PeT9jOXQPdDhGLrmUpv3lw4pncPkLE6t9cRkKIKh62uhfqd+mg8eFFMn+YmT9utrX4ARYFXsJRaswANf8DhanKyA8gjUHgLwcAWcUd2c+BMt6LF6IqnoPFCC59p+yQpQ1xTZ1amP3b7Xmcbr3QEbrfakyXASATjqlTPlQsWf1BiduFtxvZNH7bqizyCvYq3UreawUyN/8z+Fyw8WqqQapE5W/uVreph0McHAVkp2kL/1IV+xlsi48TxT4ZKbwT5W1OMkm6dVMknIkhCzI5p0t0HY1sg0cAtQFqEEjXW1V1cAWi71gr3n3PvNu0QJiwSFjveupDoBriMsY4BVKbOeOjInqJ9eQmRTcOB14XaTinkiTdASj4nn7BY7syWyxv0gMWkqPQpFS+sRaNA53dot2C6q7KG/aMxJ4gcEBWSLakZV4ZHqun6m7nhgMJYDalEkiEH5+CdEslKa/MXS8/0Le9fJQu9PMgm+Kw9Jtkxlr6r/24Pn/r9aXSijHg+77o0iOEsdFBgTlvyJ6vM6Y7Dv+YSqJyLbqBhZhrWDdo+QlznCzQCceikxBQI1LlMI5ArASCyPItX2CdmpYFHeGMJuXClQZEkd8F0Sd1Jr8pmCKNcxiTYJB8WyYsYCh4MtNWPxxWRu1cgOiip1xXqggcg2LI6ho/zGAwWroxPYSGRmfS+Jy6CiYtUf8taGSzz2l/vaQvYpJ3or4KNgOPlCSGo9hoHZ7T3fUigSIT9B47q7Tw8PoSoF3QHLgYOQa3b1yMYNPguKpfaGUVbCx+/j3q+Hk152yujbXD+5yVOTWGQNPCILiPaVWuiJlrL2XTcgEOZtcyJc6FylmehJ+f63A4pTDb8AYyExgPjVxs4yfkNvyCkpXrHiT5lHyzvENxHEw6atuVeYpLhJzvTKgFwe0SFczuAn1d/s2oouugnXMq0sywAVhsuxB1fiWYaa+vASeWpGt9DVG+yLbq0p5TG6i8y3XgVaFPys29IgFyGWgrF9FVPe3MCjfwQS02daC8jWl3kEl2Ok0u+kBzpMROgmODkwBcCbu+5YY5qxdaGg32NeU4U+0wgrBexPfZSBPwLR3cVCC5vaYEqnr9ruF9v2JHPAgp5QeGI/StBIStHKhdRvR7qlnEC562MW8KN7wcCuFK2GmFEx+3fFhEhb/nIcRV0yguisnEWPQ8mEoJ0yaLQRPn+mE+nNvZztSzg3gMMcpXbY88xRgLRL74vhc8GgmR+AHr2OmdKQZBcUthqswrHS5aNLISeU1hR60ryCF7SBLqckqNxGE/eQKldf8AxpMIMv/aFtZ7f1oU0AedsnAYw4YjTMvWiPOQvU9hVPo1mQQyNk5p9KqpriiEY42npjJYj1bw2+1spUWh+SUvQmVzQEFxLzDubUAUbEzvKSbmL0gVy4fh7hE2h59aEfPc7QcnGKXygHQhVBdhK+LKZeSoEjxdJsZK4q66uVOwE9SkVMNIaoQhw2Rl2471XEqjpubs/zX0skxQLZTfD3HewnN+w7Ay48whLeOaMYZwNL+tWS1tCHyp+ZZg9umuE2BfdxUgw7sm0mBU6RBWn0SIEhUEYQmxKr5jmgvQ1MAiz2iy6MEQ9JdRWhodLEdftsWgY9HtXdWq/cqxdt0j2B0A/HDmklh3kNuGSEke8VlXupc7qOC4h0va0Eabp8VR+/tlns4B7WoikMBfKP1ICPpJIdrGPjR/csMdM3FLU2YSVmxdldQt2OZHQs3duj9K2WSFDZAQsKq09A08ca4c9jc7bjqnHQyWAezWDnMtykIcoqYHInDPUURkmcXDR3MS3Qua4WoDYfkKXEsqBzsA4+lOmcaLwkQJAtUwLV1bG0+TJVxi+uilHW8SDKk7SmHTDSyjlm/WrQ1AC2b5yBETf6xGhbFuricW40S0JnMDe34TsQ8iWlmAEXbg0nNeaHK3mJK8Pry3o9Ytmcu9MMKipSndpE75DDdVUkzFHNOBPd4zgv+c1pSbugwaCLrmmAWn/rAnHJzeK/LET7+CxgnUVogYBg4M89F5kAipzF1whq6sYOzw3LadCdo+OeZDMSs34B8a1PtWBHOT3C+1d6GEwydnE6Cm7TgXH2V91Si0r7kEdiD7BihMJWoZfuFHfoxz3a8KyFwCafBYGpLIVXXh7f8didcu4J6VRqaWm+DFilYR6XzjP/vhlVaeKh4D6IoS0Ln4Wj9bALWCWChGUWEu7MfYHKs7uGxuIADZTB28BGYneUtecmtAWhAHFQzNDc0OlrVQgJakr6DqccU8fPOG4S60fwLlvW017ubu7+cE9TLxBoCMZlNYidIvF7WIedu8twldcWZcSrmZ1K789ETwryjCiHxgUoocfK+bW1ZS6vmxkkry/SZ5ZbNJndcYYkSQaz7ZfVeHwQIeUJ8l3i2H8WVe0ZF4MBkOtELn+LvbhHwdTmZYbPqd53qYrE2WtPbnTMf5hIxVh8W/atIFdHa3SOT5ohCT274l97/K4pwJHy17ETOldMn/ohoXDENlFlVQDoaa6A0a4H5FHIFNAiB506IBvTUWYX9Y+0MZvgf3ym5mjinJyEnaKM75I8cfkw6BXyOJfQc6/zEQxN/5UkKv4dQ4VvwzLYsU/bnq9UuGuNSA2t+uinh1Ro1bl6X5nu+kZOYqGJd23/QqovEOgbkJNTAyKlc+O93VMOBKdTCzCdmh9jmq2m8nfDmTL85R5Q+FyadUNK8rpRKQcO6U3RsWhNEowB7umSaHowpOH2oh9VPG6Cxlzz1bqt1S3YyTAJrkyAYHP8vM6LtKzi3n2o71dREmS6pWpasiJqTg8PrLIC1Y53sEISBh0s1F2ZXg6htzprMQZDrt9NEbbiaMKHler/spqX00uR9yl47Nwx0SVXK6R3yP7I019H5qu2kGEfyshx+5dQ9Yrt2OWUAWV9/2OU6eb3pQGCsV1zklL1s3I5T7k4luywG8y7o3nExaVdFYm5IAdXx7b5zJrhYU8lpKDr4Ol2D0wrQytba7jJ/e2atH01oDRtyMYsfrvKCm2n/PvGD/xB4kyNpGrBtzPzK7xynL2t5rbAePro73H379V1TMKFks1OS0zAaxflEC6dl5Ra4lQFAs3v8C/NrGEHUPsX+6N2/QRYmNHgCR6Gbz3YJ/c1eD9B2q9Z9wdcj5PhIZ5amyQPZQ6jXGz6V5CxJPCgZ+Oo+zswuNY+G8fOCl306CFY+qZypM2BCm7W6uxMDifA2ByjPc/t8gDgxhUO++FntPZlKhhnDQjYkVdJRDAnerloFb8/PcdV3KCtjUXLL09ZZis5NshSyg/qw4RjToD/6W1eVnXhjzeRPpZF83KFyZ8kQ0/iAJEZ09EZa1Xa1ymFa+HUa4YQPBncR4/0XOh8C/Zcn5fwCkW7WCR1tO4e33Wbm10PXackXPf4Yb/TsuVEXemgRmdY6AEnDyoUvP6NsDWSwp6lvT+vxM5sYyp71qyLqgEeAs6pkz37w/l23q49HUPukWYaxUdQeZ3IcW21FajHJuohSUKavlRyX8ZLHM8iDa07XTT+gPqIwS6y3eVlLvOOkMb3bsNZqozdqGJREWZ6DnJM9+O8RESWOeP2kwvsuTZBL863DJDjLv4sGWSqUwLVUZVIjvFapsxZ8RcvzAvXc96Me26f/7y7GQi9Lihvq05T1B2/EeyW7oxlBdWv2h9N346aPJ6MuBjcufIEWbw3lgLq2QMd+72+OdqGYYE9zukOWXZDD/s61MP6SW6g+Sq06IkKfBI/7RjTj0AcTIu3991mmtYEnMNM3pP9mlJidTSObzrkaRYry3TvnyDRxpJzXXKcPG3zEB4PuNvdRRS7TA0HWOUr2+gYzDkBfxoHEujupAyJ0Vd6XwFBFQC6JVIQu4wLuVJ/sSK1dExvUIneBDFtmYCEJJ8mGu6Tv9wJ1J3YBdZS3ldYdVSFYXVxWT4HYZI9YSA22vxTkmATkUC4Hv9WN3dGk/N9dBboaHjZ92wqLGOHFdkegk9bTmzSnboC7TGTkBTrdK5CkDPXSmDjSW4WW2mGOnAdOu4/+AmAfQeIEDN075fLyPY0QtjAX6wQuTLDVOMz2m2DFTAb8EscgH+FMK7JpfXlLChA8h9vtllmEcFlIKr/t3ZVzC8iVgRBy8mu/oKZ/pQGInAao7neH1e6HMQ0G04LVGeSbwVfdCFzl9c94MG1q7HIrz6kiIr2b9nkrsYUbK3t7NTWc2TVCa5a/EyHuc4VsDD1FuVszj9Y7rOAuwZiYnhNWxulcV3R2eFiZBj1hWMcCtzUhJmVOlg9Q+112PG+++ivxQufiFeCqRw36hWODhKR968b61Qh1njCWvO0x6LIw1DR/kHM6J3Klgmhge3qHZiqC9qWNutX+T26LpiBB0MONCSSJN7IN805oO/kBzjzCVZfYvu6VyRlQFc2CGUTzt9F0di4I0M5x51R2XUJvCdX2kPTo8ZEtRDNzlkn/pHSbIgl97OyhlI8TQQEewKCZkRbFd1bnkBFod9A+fVQ6ySfcZK7hGv21sGvRtpQOni8zRp5Mbfv4UO96FtY5DtWKlx14tnVUe5Sr+e9HGXoAKGvXN+o+1zbhbu8ahcYWJKC9Q+19LFW2wck7ItSvTYSJFeaGDvD3xK6YzuY9/pjmjhqi90zcR2YnYyzWgpNwCZFYCiraBF3xUjD0eKCAkqDLtbibqGPZ7/UFNq/YB73yh2PUlhNwb+9z3t9U9GpZw3oz887tR6f/RxYrG+C8nOFsF+p+jRalvGyDX19Jexqc/AYNqz5JpgQjVPsMq8Ij4kzBdoE14P+2ebTl7wokeWf6FXvpxxLRbAyRaCBqQTYWLSKlH10I45qZiZcKp+wSc7VD5iGFpWVoO+KYxupc82X7l3W73YEZ95jB1t/P4H6VT0EN/OQHGnbgjf7THYqOD4S4ff1HU8EQ0KzLbYK1vw1DgphTGVK7AuYFQMf6Bel032/TU1Mj+8krotZQLu1bOUgaMjxqenz1KaLXY98qek2IyjDu7PTDzCz6WOdCrBdy1OYSAYJKIiG9tj5AD4EXAKyAapHs5EyvDM1KubJ0tqYJxjllHDwOVUKkTeL9D0bozZPeutT5wVW0uyI+W9fOqi3ePJebBodgRMJ9A9piu0lgHgcx6hIZkBejD47qK0PAn3KIAMNbWeChNn9xM1cR9MyxHq4CqtXCHJnYUyQJtjc9wSVKqI3Wr7R72td499XcgnccMUWZdl964MrrwGi5xI4Tpjqc3ddZr1yUuttxRAWupncg7DudB3gIrRZJDfdyNMzm2uUt1GkFYL9u6VbxSe53Rx81yY9LyCrxQ5DoFblegMkklSLMfG3e7VTmI3r4PEktYgLoJ5da1QsWSddwBHFX2PhmbGpRtfA5Ctk0zjjE2RGtMLACsBubW9hq1fnlW1qBaeYY6rkc0TZmMGd8Ebn85Fjsq4TSg1U1Yp7qauWREVdpSxefoAwOEKHdrZjppOTDJg2yGXjYngS3C/nDQH9tjY5ABSyapRN5KcV5sGwjPoBnhgLjPaToUC13AK6+a3EqcZhKG/5mQujG4Je2IDwgXVCpkrQ0twdTK5WmgF39MEu6hTPElQnjml8y0RyE3d5A1bu46o1yPHJ082aimULLL0xQBy5hVx+iCJ4Uqd2OfKTbPAbFuA+stl7pcF9oNmffi9ztLQGaucqaqaENL/cIR8F3vY2ZgQ3GASIF1K9+i28xYm7T8IU9uwzI/Ybulrw5ZnVA207Cy22BzWd5XtwuOrfP1qe3aBNa8tj6vIZres376WFhKkNutFdMN7/mgSbVb6fQsxjcBoGksJdWdTkwDMqJd9XA+a8EXS+56lR4l3E3zdbcM6mMQKpL8pPb3sTzPt24PD8OGEcuv3TE2F2gyqDl57/mYlopBUe6mEhD3YKstKhaK2rD5OAZITtuXUnY/JKTgXVrTk9n0Iy7LWyNbdNrz2peY2WKal5R2IT/YikSQ7JB7mqPAN+OML8TPo7KKkCSjvwsIVUlb7B/zXo5GlpcqQEfz1OpPqOGFz9uPReLOgsOJ+zBH5OfZAoUuhHfT4fQS2SRhxMX5Pd8a3mYd51QQn5U0xSewn2lknoEFjO5t7YBrV09KIoyn3fsq8y1vDR5TkRAYPFlg6SrqLvGBxzNAHsjpomLhUlQ35PHU5UCN+rWrGBuE7CVgeKD0oqgogWQGlk33tn9pa+4Ujthj0mvtJcyV8WyRpFdIQ7Df3gKJi3vsERK5wzzQBBsEEGfMm62nzak5o/fEcmq1SJKB06EgtRnZkaGvgMl2NyighWQ18SPPLTyQ9wO2Bc/AYZiMl8vD14iIahU/Ev+u5nq6o8kWacrVThrG+z9ry+zOyruiCFw5WxvM0jELmf+og3+vp8o89XIuqvxLAtP1XQPCJbYSOA9hxRleFH7FjOrHV8H2NY5jsUlQ0dg69fVDLRiOjRySNLd2Lp51Ol68E/VIj0W7uvj1bV8nCOZABQ+6qYBcid/7+FijiXNY/QW2c42vwI+ZFw6yi44Ms4hKbjKymqesBxmuZfSi+jVX0c3X4RtbDrFax1CMyr6qO9MnTMVVpec27g/NymvruspUF8i4Y0e/zcEGHwVha1vyO3KqFQ5WFc9gLbGPORxAgT1H+HpoOvzzRHmagkBAefd9JH5mWLo3fUV4ac4QJbM0qyGfYaRzA4dN77h4enKp77r998QyHs8OTCWALUma1HxizrrYVCpIzZU3BicP5le9P3Jz0PhPlhS5w8mufdxhR78TTzOvlCUh1GjvvtokHsCvUpU31DrUVf1zCX890Lf60mB2+tlVrIAq9xA+wog6At19WKL1tdyZC056X89ssebMBiV/d+w60fNvTeQqSCj+DkJqqnmovIaJGtzy5EV8qqm2W8/XtZ68xUvmdkpoC2nxwlNQM7hc8HblkVAqAD1TClUeDTgnX+Perqv1E2YaS8nAP6cGXGtdpCT5tVAIIyCcNNPv/FzxJc7Yfwe4NPtt2Qy1z+GP4mUxk7d+MQyQGGwtdHJqSeGShKxnQji+t2DcZw7BENnX6Fd/Rgf+kwBW5J7Jeyt5bNP3Uvhckn/ApFhNALO8kE2IpUe5oz8Yz7fphRYflrN6mZ5t2ndxVMzF3eDyqQQTdA419FqmZABdLR3WBD7Rx7Zy09ibeXx7saX/Xca+6xWwOnSj2CYfSYZ6pZUCqaEoA7S34Xqswel44SsYv/aCXPAdhU1faF7Au4l7R/D0oLF+I2U3ZpeykfPz9LsnbiMzMy+PJCE84FLkRsW05Ari1hUeln6SVeH3wAyApktokm7k3smmVIl/aUxmWTgD/gofGKPQF09IFQw4OyhUFtAw7ykSnVXblaofGD3t4BGd9ZIuq65ANTp0xgwW6RTA110DMw7ff2bXt8KN/GdTC0tFKUXALeE8hSIsA8KpautEyI3FpDsbIRmvqQqMxLE+fFOXIKJhxtsanVZiYBhl7KIYO9V2xyxmLiK5t7faYsIC8OZoD3yYehnThIO6dfYnM86sEbxomOb8w43MzB5xpyE0m8C3PjLvIu3uSOsxHOIrOtHo/2o35x3QVULnRJ4XHQI+lFYGBz7vYQ7UoS5JgULshIVrglI4aiKVv0w/b6KJUFnnRWiRDB/FOnrC3uAfO8q2hhk2Zi7Gu8dQ3iITAL11wNWGQr7lHXIOjPWpzjbyHh3NE08dlJUN98THkKpmtmT2EYMVY9ySLDxIrZsLWMwe6tiMjt4UGeXr0h3yWzK9IkopP4MgDK60kDyOq6Of4gwVmc/4VVX5tTUniNwe+91xjER0H0EdtvWRnOrpuwA7NEj9aH6KvGLqA7Y8LGGEPctE+yu/gpx2sxNHTt/1WkRRtPIjH9xDIJ77wTGHOPvp4f92J920Aq6xzmFaz4aUSpRZ0liVahlUchYMUJszbUgfXDfLwljqDPJovq+ZVN0Y6J3IUpdDZbwuW3tEXmtXAut0SKlgglXz717nMk5o/M3jaBGKcnC92QJaEewPeSFofgUdE7N16bMIVT1y+8M+5YwJYVRH7eTF1Qq7Ip0Bl5ZKCfLepWMwHwS6x7kZvUshY8NNroBXjSKSEAeC40oOdAzY4H77m4pQ2hlzuHuraiNnp6dbuoKCHoE+u32qrzFsUL/9kMyFGOnDNb4dnLrW9j7+5bSS3k3oLA+zj0gMXq88Wp8e8kIVJNMH5tnHdphIpcsnXZxqO9UZ5CHHJLQtF1sNzHNF9Yd4nbNlG6m/HK6dtlwPoeSLq/cYDFTA6a0BMAr+J4vV5XQMOUqkWb3OY/AMxiCM4F/56VHczhIrd79ptbZ2TvCnOTueFloYLfO12Te3Id3re+0y101c0So2B6Vd4zQqwud/2kKdRZEva8ZldmaG1y+oLcAZ2HHVuFVNUAjJ+qRC2fJ77PB7By3ggl1+SeoBucYmIZQBHMDQ3PVy1c6dE2mGbLCeGEvVUJqPW9dXUpdw+B/1qJ+kn3VrI13GoD6bKs9nNPWmnRBJiGA1E3mXbNPZuXEGpxjMuUKlWsmW613DJe70gchA7dXUjGVSphvW7LffemYxeqKgKui71GLgnbsCfPC52jus8KEakcne7g0nmEGFxb9XMaHpaXrfAsSY2TqOiR248vKuG2x895frzECHutBOqC0VmpmUlpRL/dRiKwrClkxEG8c95Ogi/D5czFi8aEbtH/tCNkU8mYJLlaUlKsxq9xcZnw6xlbSQzfvgvUUYaG3ZPEOdihVVPIZV5YHDZkgyX8nzkeaelCm1xOYUhHODcyqyEwmZDs5S4H5JS1EAbpLqUNxDkj57+5v2/a74g9KtQkowL1uh2DL2aq4LHlj4PmmH/KFNi6Jd3twf02BoHsqxhyLBP2UHuBnDpqjuPJlaCnmITzaQxDab/Qkb8YYHPgAHaB+SLg860sDYYh/Ty1fYqJ2ztC1mMii3OZwTRFGAZZLcxBvHwPf9morwLjbogRv2AV9aRoCbOPHFbp0z2CPNbW/Lop/ItO4exENP7HfD5WqHb6los79jh18JqBo72fZ1hXCX7WXi7sdWU5syi4Xq04R6xK9ysPMR5733MFGNjIbqRNNZ/e4wInKwifsSXQ8Y/nAMevZjQd/D38ZYOMI3QhNnCPpZv31EPpB5fXYx6J32by9jjWOVY8XBrij/CaBKjfqu1THTxM1Q1wmNKBvDZ28dqqzA639qDUioCTo2s+Sd3rEXp+cX4b5uoxfRlBIVZhgNi0nqHUnSFGgeKklAMHuXq/azoPQaI2zarwuznb8gFPTIpKVxfIIASYNEbsTc5UCV0urMTws6r7c9JrWVqd4brXPljuQhJ6Wj638ygn3E7HgRlkrYhd2aNRyvsGRQDTmbaEjJKazGGK8FjmN6mP2W2mr7J/A7bv8pJurclik+85okbFM7Do27JxHVFoAgswgrpvjeYqNXRSGCYvm2/MOwia4p83ru0ByrclaltXfEfbGuapykEnYz8g38QdsmmWbFLLuasaZMDWKOlUNy5NUnkm8l7cYBhX38/XMLRgfcMaIq9XCYwGNFcLBWP8zVOUvCKRdrY/ljaAKTfG5UrEkba3oQBc4mG7VnUwhG9eFAL1hdga1UCoQvHPI6KoRcSHNdLRc6aVMKSl2PSLy4P3UPv9bgdwZ3cfr7hZN7LSIcWRoqAKD+EZQssf7YJqS1F+NNxygsMFd2hxgnRpmBgc3YWuUYsz3w8rOGBxV75uEoujSVZevexGaOCHzCvwtWnKKBd0fnN3FG3mUU8m+d9eFHnZ8iYFXkCS5/MBm0T78PKoteaMQ1R+J+SZbepjpPUsKU7asEtfYTC1SXDJrlbAHCsx3QbU2LRK/CZFg+OOsmouS2u+S9ecgq+tP942VmmoyiR5a/PN54qs76813iqZsIrxj8pDTpJ1lf9TpIv+VQAokh/ZPuamzsAYNbKjGDHN7sOj+4RpLbNvycMn1UUQLKoMLuYRkzfc43LvHgIi6xJ/qaygn8Q+kkRRsOGiq5pQIJL2yIyI8EYgNOCfdnFoRcxAhoXVkc431JEsaRqGIt2f127Bwi5CPIqrL6MPhtqfI9YO3cyooePqgwNQGcCvVYn5D3YMm8SvzdvRIyh9n5jzK2J6I04ZeS2V9YDw5c8d8GTiqb6gbQUmL9w4S9oOG3wtm+hFzRr0n5gLUIbyk6yt/XQSBabv4pg/tJLLrqx9RmrAczhUo72W4acIhs+qK/xrEqh9A1m23lU/Zuy8QfMPVxueE9b/Qn7mt3TxwhYXQgfaDh6d3Jh8Lx8TtRpLSXNflXyseSexn1siZdtOFdp51PnUH8srbCEcE4pf/dIHnW/ScY8a+q1zSMfYsqtc3qUt4OHhzT1xYmlZuvnJy580xEwIAgfPZdLa5YKxaY67TrgwXqETPGmivYIkxp9PmvfNpkN7hi5BbeR3n2994TpVR6JauUTC8k0gd/s120kZ6+VG9y+tpwc8AWjjJwlJn7zEG7XKXP96EQ81A4xs4xuAh7Gkn7THrC7a0vsXLle9U0gZ3plwVd1ri29VnwzKSfzVm54HJnOBC9xxgsYg6WB4niw5oGHsnWlkFMY4FTKXau2tKO6E8GV9hC3BUa0ANNAfvU3Gggx4hmTU9B4FGTjP+beM/x2rlAKQZzzKFHSUSCGIfWlo3w/PuqXRhrQ0+omYuM8/0U5Dizys0RL+yQ3Rq0Aff8g98ivrJwKKsoh9cSg9OBCbnLCcgvcX8aFDroKCvVzzeP5TztjneykBLpiPVU1RAZIdp51SJt4zrb20Nlfx6x5I4zp6UvH6OH1a2Jg2+bl//jxDeUuatQTGpWWhYRwXue6EKo4RyooiNxJWC4553lN5cChIzxpuDHgZ66iN0yrI0p85Yno5vTwNBlos/0+ZRoaQ7UHRRbaAgtLX71ADiSe+bOqcqICTiDJ/E2FVhE3Q+XILhTGKvdyj2rrljlIkMs2zycaHUHEBC7yjffSZd78L72DFOkYZlRbToBgYD3AJ1HsAIRecRVbNenD3Kf6v5OTGO2sVvUAEsyhrJhcUtRBax+SRosDtxIGY15twfrRnU45+WdpkueUbFwDFEw8KZnJ3Fr8gpC/0Zjoi7qQBGDrEkyS7V9zRUCoEYQHbOBN60WD2R89AhMrT+wD78gOv4ZX/gxFfVYsiYlMtpzAAO9xES6AMOhgdeK36E+tF/ogQUq7S/a0DSfiyf6A30Zej36RUAF6t59jOIsgDb1VG1PkHBoSZ0/u5Uqmyh3tp7Cii7V7y9Zedz5+L4GXGirTChKhX0ryf2c8ibkNZ++GvYLEq4=\"}" +} \ No newline at end of file diff --git a/backend/src/db/api/likes.js b/backend/src/db/api/likes.js new file mode 100644 index 0000000..6705688 --- /dev/null +++ b/backend/src/db/api/likes.js @@ -0,0 +1,235 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class LikesDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const likes = await db.likes.create( + { + id: data.id || undefined, + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return likes; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const likesData = data.map((item, index) => ({ + id: item.id || undefined, + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const likes = await db.likes.bulkCreate(likesData, { transaction }); + + // For each item created, replace relation files + + return likes; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const likes = await db.likes.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + updatePayload.updatedById = currentUser.id; + + await likes.update(updatePayload, { transaction }); + + return likes; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const likes = await db.likes.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of likes) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of likes) { + await record.destroy({ transaction }); + } + }); + + return likes; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const likes = await db.likes.findByPk(id, options); + + await likes.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await likes.destroy({ + transaction, + }); + + return likes; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const likes = await db.likes.findOne({ where }, { transaction }); + + if (!likes) { + return likes; + } + + const output = likes.get({ plain: true }); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.likes.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('likes', 'id', query), + ], + }; + } + + const records = await db.likes.findAll({ + attributes: ['id', 'id'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['id', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.id, + })); + } +}; diff --git a/backend/src/db/migrations/1744591367307.js b/backend/src/db/migrations/1744591367307.js new file mode 100644 index 0000000..3ab60dc --- /dev/null +++ b/backend/src/db/migrations/1744591367307.js @@ -0,0 +1,72 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.createTable( + 'likes', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.dropTable('likes', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/likes.js b/backend/src/db/models/likes.js new file mode 100644 index 0000000..884d1ba --- /dev/null +++ b/backend/src/db/models/likes.js @@ -0,0 +1,45 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const likes = sequelize.define( + 'likes', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + likes.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + //end loop + + db.likes.belongsTo(db.users, { + as: 'createdBy', + }); + + db.likes.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return likes; +}; diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index 54b0cb5..96f97b6 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -101,6 +101,7 @@ module.exports = { 'students', 'roles', 'permissions', + 'likes', , ]; await queryInterface.bulkInsert( @@ -948,6 +949,31 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_PERMISSIONS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_LIKES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_LIKES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_LIKES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_LIKES'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index 6b28e66..e9630ab 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -13,6 +13,8 @@ const Instructors = db.instructors; const Students = db.students; +const Likes = db.likes; + const AnalyticsData = [ { // type code here for "relation_many" field @@ -92,14 +94,6 @@ const DiscussionBoardsData = [ ]; const EnrollmentsData = [ - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - payment_status: 'overdue', - }, - { // type code here for "relation_one" field @@ -115,6 +109,14 @@ const EnrollmentsData = [ payment_status: 'pending', }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + payment_status: 'overdue', + }, ]; const InstructorsData = [ @@ -175,6 +177,8 @@ const StudentsData = [ }, ]; +const LikesData = [{}, {}, {}]; + // Similar logic for "relation_many" // Similar logic for "relation_many" @@ -312,6 +316,8 @@ module.exports = { await Students.bulkCreate(StudentsData); + await Likes.bulkCreate(LikesData); + await Promise.all([ // Similar logic for "relation_many" @@ -351,5 +357,7 @@ module.exports = { await queryInterface.bulkDelete('instructors', null, {}); await queryInterface.bulkDelete('students', null, {}); + + await queryInterface.bulkDelete('likes', null, {}); }, }; diff --git a/backend/src/db/seeders/20250414004247.js b/backend/src/db/seeders/20250414004247.js new file mode 100644 index 0000000..ddd1a6e --- /dev/null +++ b/backend/src/db/seeders/20250414004247.js @@ -0,0 +1,87 @@ +const { v4: uuid } = require('uuid'); +const db = require('../models'); +const Sequelize = require('sequelize'); +const config = require('../../config'); + +module.exports = { + /** + * @param{import("sequelize").QueryInterface} queryInterface + * @return {Promise} + */ + async up(queryInterface) { + const createdAt = new Date(); + const updatedAt = new Date(); + + /** @type {Map} */ + const idMap = new Map(); + + /** + * @param {string} key + * @return {string} + */ + function getId(key) { + if (idMap.has(key)) { + return idMap.get(key); + } + const id = uuid(); + idMap.set(key, id); + return id; + } + + /** + * @param {string} name + */ + function createPermissions(name) { + return [ + { + id: getId(`CREATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `CREATE_${name.toUpperCase()}`, + }, + { + id: getId(`READ_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `READ_${name.toUpperCase()}`, + }, + { + id: getId(`UPDATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `UPDATE_${name.toUpperCase()}`, + }, + { + id: getId(`DELETE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `DELETE_${name.toUpperCase()}`, + }, + ]; + } + + const entities = ['likes']; + + const createdPermissions = entities.flatMap(createPermissions); + + // Add permissions to database + await queryInterface.bulkInsert('permissions', createdPermissions); + // Get permissions ids + const permissionsIds = createdPermissions.map((p) => p.id); + // Get admin role + const adminRole = await db.roles.findOne({ + where: { name: config.roles.admin }, + }); + + if (adminRole) { + // Add permissions to admin role if it exists + await adminRole.addPermissions(permissionsIds); + } + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete( + 'permissions', + entities.flatMap(createPermissions), + ); + }, +}; diff --git a/backend/src/index.js b/backend/src/index.js index c553b7e..48a50a6 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -37,6 +37,8 @@ const rolesRoutes = require('./routes/roles'); const permissionsRoutes = require('./routes/permissions'); +const likesRoutes = require('./routes/likes'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -156,6 +158,12 @@ app.use( permissionsRoutes, ); +app.use( + '/api/likes', + passport.authenticate('jwt', { session: false }), + likesRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/likes.js b/backend/src/routes/likes.js new file mode 100644 index 0000000..ed6c9b6 --- /dev/null +++ b/backend/src/routes/likes.js @@ -0,0 +1,429 @@ +const express = require('express'); + +const LikesService = require('../services/likes'); +const LikesDBApi = require('../db/api/likes'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('likes')); + +/** + * @swagger + * components: + * schemas: + * Likes: + * type: object + * properties: + + */ + +/** + * @swagger + * tags: + * name: Likes + * description: The Likes managing API + */ + +/** + * @swagger + * /api/likes: + * post: + * security: + * - bearerAuth: [] + * tags: [Likes] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Likes" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Likes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await LikesService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Likes] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Likes" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Likes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await LikesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/likes/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Likes] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Likes" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Likes" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await LikesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/likes/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Likes] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Likes" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await LikesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/likes/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Likes] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Likes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await LikesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/likes: + * get: + * security: + * - bearerAuth: [] + * tags: [Likes] + * summary: Get all likes + * description: Get all likes + * responses: + * 200: + * description: Likes list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Likes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + + const currentUser = req.currentUser; + const payload = await LikesDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + }), +); + +/** + * @swagger + * /api/likes/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Likes] + * summary: Count all likes + * description: Count all likes + * responses: + * 200: + * description: Likes count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Likes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await LikesDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/likes/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Likes] + * summary: Find all likes that match search criteria + * description: Find all likes that match search criteria + * responses: + * 200: + * description: Likes list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Likes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await LikesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/likes/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Likes] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Likes" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await LikesDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/likes.js b/backend/src/services/likes.js new file mode 100644 index 0000000..d8f6f3e --- /dev/null +++ b/backend/src/services/likes.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const LikesDBApi = require('../db/api/likes'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class LikesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await LikesDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await LikesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let likes = await LikesDBApi.findBy({ id }, { transaction }); + + if (!likes) { + throw new ValidationError('likesNotFound'); + } + + const updatedLikes = await LikesDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedLikes; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await LikesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await LikesDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/frontend/json/runtimeError.json b/frontend/json/runtimeError.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/frontend/json/runtimeError.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/frontend/src/components/Likes/CardLikes.tsx b/frontend/src/components/Likes/CardLikes.tsx new file mode 100644 index 0000000..92fbddc --- /dev/null +++ b/frontend/src/components/Likes/CardLikes.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + likes: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardLikes = ({ + likes, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_LIKES'); + + return ( +
+ {loading && } +
    + {!loading && + likes.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
  • + ))} + {!loading && likes.length === 0 && ( +
    +

    No data to display

    +
    + )} +
+
+ +
+
+ ); +}; + +export default CardLikes; diff --git a/frontend/src/components/Likes/ListLikes.tsx b/frontend/src/components/Likes/ListLikes.tsx new file mode 100644 index 0000000..3c31b6b --- /dev/null +++ b/frontend/src/components/Likes/ListLikes.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + likes: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListLikes = ({ + likes, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_LIKES'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && + likes.map((item) => ( + +
+ dark:divide-dark-700 overflow-x-auto' + } + > + +
+
+ ))} + {!loading && likes.length === 0 && ( +
+

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListLikes; diff --git a/frontend/src/components/Likes/TableLikes.tsx b/frontend/src/components/Likes/TableLikes.tsx new file mode 100644 index 0000000..e544926 --- /dev/null +++ b/frontend/src/components/Likes/TableLikes.tsx @@ -0,0 +1,481 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/likes/likesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureLikesCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleLikes = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + likes, + loading, + count, + notify: likesNotify, + refetch, + } = useAppSelector((state) => state.likes); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (likesNotify.showNotification) { + notify(likesNotify.typeNotification, likesNotify.textNotification); + } + }, [likesNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `likes`, currentUser).then((newCols) => + setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
+ `datagrid--row`} + rows={likes ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
+ ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
+ <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
+
+
+ Filter +
+ + {filters.map((selectOption) => ( + + ))} + +
+ {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
+
Value
+ + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : ( +
+
+ Contains +
+ +
+ )} +
+
+ Action +
+ { + deleteFilter(filterItem.id); + }} + /> +
+
+ ); + })} +
+ + +
+ +
+
+
+ ) : null} + +

Are you sure you want to delete this item?

+
+ + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleLikes; diff --git a/frontend/src/components/Likes/configureLikesCols.tsx b/frontend/src/components/Likes/configureLikesCols.tsx new file mode 100644 index 0000000..1e16b07 --- /dev/null +++ b/frontend/src/components/Likes/configureLikesCols.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_LIKES'); + + return [ + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ + , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/WebPageComponents/Footer.tsx b/frontend/src/components/WebPageComponents/Footer.tsx index 081158b..3833078 100644 --- a/frontend/src/components/WebPageComponents/Footer.tsx +++ b/frontend/src/components/WebPageComponents/Footer.tsx @@ -20,7 +20,7 @@ export default function WebSiteFooter({ const style = FooterStyle.WITH_PROJECT_NAME; - const design = FooterDesigns.DESIGN_DIVERSITY; + const design = FooterDesigns.DEFAULT_DESIGN; return (
{ const [students, setStudents] = React.useState('Loading...'); const [roles, setRoles] = React.useState('Loading...'); const [permissions, setPermissions] = React.useState('Loading...'); + const [likes, setLikes] = React.useState('Loading...'); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -52,6 +53,7 @@ const Dashboard = () => { 'students', 'roles', 'permissions', + 'likes', ]; const fns = [ setUsers, @@ -63,6 +65,7 @@ const Dashboard = () => { setStudents, setRoles, setPermissions, + setLikes, ]; const requests = entities.map((entity, index) => { @@ -458,6 +461,38 @@ const Dashboard = () => {
)} + + {hasPermission(currentUser, 'READ_LIKES') && ( + +
+
+
+
+ Likes +
+
+ {likes} +
+
+
+ +
+
+
+ + )} diff --git a/frontend/src/pages/likes/[likesId].tsx b/frontend/src/pages/likes/[likesId].tsx new file mode 100644 index 0000000..acc8b15 --- /dev/null +++ b/frontend/src/pages/likes/[likesId].tsx @@ -0,0 +1,118 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/likes/likesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditLikes = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = {}; + const [initialValues, setInitialValues] = useState(initVals); + + const { likes } = useAppSelector((state) => state.likes); + + const { likesId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: likesId })); + }, [likesId]); + + useEffect(() => { + if (typeof likes === 'object') { + setInitialValues(likes); + } + }, [likes]); + + useEffect(() => { + if (typeof likes === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = likes[el])); + + setInitialValues(newInitialVal); + } + }, [likes]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: likesId, data })); + await router.push('/likes/likes-list'); + }; + + return ( + <> + + {getPageTitle('Edit likes')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + router.push('/likes/likes-list')} + /> + + +
+
+
+ + ); +}; + +EditLikes.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditLikes; diff --git a/frontend/src/pages/likes/likes-edit.tsx b/frontend/src/pages/likes/likes-edit.tsx new file mode 100644 index 0000000..c92db37 --- /dev/null +++ b/frontend/src/pages/likes/likes-edit.tsx @@ -0,0 +1,116 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/likes/likesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditLikesPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = {}; + const [initialValues, setInitialValues] = useState(initVals); + + const { likes } = useAppSelector((state) => state.likes); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof likes === 'object') { + setInitialValues(likes); + } + }, [likes]); + + useEffect(() => { + if (typeof likes === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = likes[el])); + setInitialValues(newInitialVal); + } + }, [likes]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/likes/likes-list'); + }; + + return ( + <> + + {getPageTitle('Edit likes')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + router.push('/likes/likes-list')} + /> + + +
+
+
+ + ); +}; + +EditLikesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditLikesPage; diff --git a/frontend/src/pages/likes/likes-list.tsx b/frontend/src/pages/likes/likes-list.tsx new file mode 100644 index 0000000..841dfa7 --- /dev/null +++ b/frontend/src/pages/likes/likes-list.tsx @@ -0,0 +1,160 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableLikes from '../../components/Likes/TableLikes'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/likes/likesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const LikesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_LIKES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getLikesCSV = async () => { + const response = await axios({ + url: '/likes?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'likesCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Likes')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +LikesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default LikesTablesPage; diff --git a/frontend/src/pages/likes/likes-new.tsx b/frontend/src/pages/likes/likes-new.tsx new file mode 100644 index 0000000..ac0fdf4 --- /dev/null +++ b/frontend/src/pages/likes/likes-new.tsx @@ -0,0 +1,92 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/likes/likesSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = {}; + +const LikesNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/likes/likes-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + router.push('/likes/likes-list')} + /> + + +
+
+
+ + ); +}; + +LikesNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default LikesNew; diff --git a/frontend/src/pages/likes/likes-table.tsx b/frontend/src/pages/likes/likes-table.tsx new file mode 100644 index 0000000..9a15606 --- /dev/null +++ b/frontend/src/pages/likes/likes-table.tsx @@ -0,0 +1,159 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableLikes from '../../components/Likes/TableLikes'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/likes/likesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const LikesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_LIKES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getLikesCSV = async () => { + const response = await axios({ + url: '/likes?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'likesCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Likes')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +LikesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default LikesTablesPage; diff --git a/frontend/src/pages/likes/likes-view.tsx b/frontend/src/pages/likes/likes-view.tsx new file mode 100644 index 0000000..cdfa75b --- /dev/null +++ b/frontend/src/pages/likes/likes-view.tsx @@ -0,0 +1,76 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/likes/likesSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +const LikesView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { likes } = useAppSelector((state) => state.likes); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View likes')} + + + + + + + + + router.push('/likes/likes-list')} + /> + + + + ); +}; + +LikesView.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default LikesView; diff --git a/frontend/src/pages/web_pages/home.tsx b/frontend/src/pages/web_pages/home.tsx index 73f1376..54ac063 100644 --- a/frontend/src/pages/web_pages/home.tsx +++ b/frontend/src/pages/web_pages/home.tsx @@ -139,7 +139,7 @@ export default function WebSite() { { + const { id, query } = data; + const result = await axios.get(`likes${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'likes/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('likes/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'likes/deleteLikes', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`likes/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'likes/createLikes', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('likes', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'likes/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('likes/bulk-import', data, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const update = createAsyncThunk( + 'likes/updateLikes', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`likes/${payload.id}`, { + id: payload.id, + data: payload.data, + }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const likesSlice = createSlice({ + name: 'likes', + initialState, + reducers: { + setRefetch: (state, action: PayloadAction) => { + state.refetch = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addCase(fetch.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(fetch.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(fetch.fulfilled, (state, action) => { + if (action.payload.rows && action.payload.count >= 0) { + state.likes = action.payload.rows; + state.count = action.payload.count; + } else { + state.likes = action.payload; + } + state.loading = false; + }); + + builder.addCase(deleteItemsByIds.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItemsByIds.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Likes has been deleted'); + }); + + builder.addCase(deleteItemsByIds.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(deleteItem.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItem.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Likes'.slice(0, -1)} has been deleted`); + }); + + builder.addCase(deleteItem.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(create.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Likes'.slice(0, -1)} has been created`); + }); + + builder.addCase(update.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(update.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Likes'.slice(0, -1)} has been updated`); + }); + builder.addCase(update.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(uploadCsv.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(uploadCsv.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Likes has been uploaded'); + }); + builder.addCase(uploadCsv.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + }, +}); + +// Action creators are generated for each case reducer function +export const { setRefetch } = likesSlice.actions; + +export default likesSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index aaf78cc..3a71946 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -13,6 +13,7 @@ import instructorsSlice from './instructors/instructorsSlice'; import studentsSlice from './students/studentsSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; +import likesSlice from './likes/likesSlice'; export const store = configureStore({ reducer: { @@ -30,6 +31,7 @@ export const store = configureStore({ students: studentsSlice, roles: rolesSlice, permissions: permissionsSlice, + likes: likesSlice, }, });