diff --git a/.gitignore b/.gitignore index 80704f4378..6d8270a952 100755 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,7 @@ Thumbs.db # ignore node/grunt dependency directories node_modules/ +migrations/ # webpack output dist/* diff --git a/Pipfile b/Pipfile index 44e04f14ff..5903f3966a 100644 --- a/Pipfile +++ b/Pipfile @@ -17,9 +17,10 @@ gunicorn = "*" cloudinary = "*" flask-admin = "*" typing-extensions = "*" -flask-jwt-extended = "==4.6.0" wtforms = "==3.1.2" sqlalchemy = "*" +flask-jwt-extended = "*" +flask-mail = "*" [requires] python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index b201c3decc..fa578d2936 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d2e672e650278aeeee2fe49bd76d76497d8b65a50f8b5dbb121d265cbc6ef4e5" + "sha256": "b442b84d151e30e7f38c6a763287027ca60d24914f9af5b0cbea537b813d560b" }, "pipfile-spec": 6, "requires": { @@ -42,11 +42,11 @@ }, "click": { "hashes": [ - "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", - "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" + "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", + "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6" ], - "markers": "python_version >= '3.7'", - "version": "==8.1.8" + "markers": "python_version >= '3.10'", + "version": "==8.3.1" }, "cloudinary": { "hashes": [ @@ -58,11 +58,12 @@ }, "flask": { "hashes": [ - "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", - "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136" + "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", + "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c" ], "index": "pypi", - "version": "==3.1.0" + "markers": "python_version >= '3.9'", + "version": "==3.1.2" }, "flask-admin": { "hashes": [ @@ -82,11 +83,21 @@ }, "flask-jwt-extended": { "hashes": [ - "sha256:63a28fc9731bcc6c4b8815b6f954b5904caa534fc2ae9b93b1d3ef12930dca95", - "sha256:9215d05a9413d3855764bcd67035e75819d23af2fafb6b55197eb5a3313fdfb2" + "sha256:52f35bf0985354d7fb7b876e2eb0e0b141aaff865a22ff6cc33d9a18aa987978", + "sha256:8085d6757505b6f3291a2638c84d207e8f0ad0de662d1f46aa2f77e658a0c976" + ], + "index": "pypi", + "markers": "python_version >= '3.9' and python_version < '4'", + "version": "==4.7.1" + }, + "flask-mail": { + "hashes": [ + "sha256:44083e7b02bbcce792209c06252f8569dd5a325a7aaa76afe7330422bd97881d", + "sha256:a451e490931bb3441d9b11ebab6812a16bfa81855792ae1bf9c1e1e22c4e51e7" ], "index": "pypi", - "version": "==4.6.0" + "markers": "python_version >= '3.8'", + "version": "==0.10.0" }, "flask-migrate": { "hashes": [ @@ -209,11 +220,11 @@ }, "jinja2": { "hashes": [ - "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", - "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb" + "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", + "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" ], "markers": "python_version >= '3.7'", - "version": "==3.1.5" + "version": "==3.1.6" }, "mako": { "hashes": [ @@ -225,70 +236,98 @@ }, "markupsafe": { "hashes": [ - "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", - "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", - "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", - "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", - "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", - "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", - "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", - "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", - "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", - "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", - "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", - "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", - "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", - "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", - "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", - "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", - "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", - "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", - "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", - "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", - "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", - "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", - "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", - "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", - "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", - "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", - "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", - "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", - "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", - "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", - "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", - "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", - "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", - "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", - "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", - "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", - "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", - "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", - "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", - "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", - "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", - "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", - "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", - "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", - "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", - "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", - "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", - "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", - "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", - "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", - "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", - "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", - "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", - "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", - "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", - "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", - "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", - "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", - "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", - "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", - "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" + "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", + "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", + "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", + "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", + "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", + "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", + "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", + "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", + "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", + "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", + "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", + "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", + "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", + "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", + "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", + "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", + "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", + "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", + "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", + "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", + "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", + "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", + "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", + "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", + "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", + "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", + "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", + "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", + "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", + "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", + "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", + "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", + "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", + "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", + "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", + "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", + "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", + "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", + "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", + "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", + "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", + "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", + "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", + "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", + "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", + "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", + "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", + "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", + "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", + "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", + "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", + "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", + "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", + "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", + "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", + "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", + "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", + "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", + "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", + "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", + "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", + "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", + "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", + "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", + "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", + "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", + "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", + "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", + "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", + "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", + "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", + "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", + "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", + "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", + "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", + "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", + "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", + "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", + "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", + "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", + "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", + "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", + "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", + "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", + "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", + "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", + "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", + "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", + "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50" ], "markers": "python_version >= '3.9'", - "version": "==3.0.2" + "version": "==3.0.3" }, "packaging": { "hashes": [ diff --git a/README.md b/README.md index 6b782b220d..63d4ff2e7b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Build web applications using React.js for the front end and python/flask for you ### 1) Installation: -> If you use Github Codespaces (recommended) or Gitpod this template will already come with Python, Node and the Posgres Database installed. If you are working locally make sure to install Python 3.10, Node +> If you use Github Codespaces (recommended) or Gitpod this template will already come with Python, Node and the Posgres Database installed. If you are working locally make sure to install Python 3.10, Node It is recomended to install the backend first, make sure you have Python 3.10, Pipenv and a database engine (Posgress recomended) @@ -61,11 +61,11 @@ And you will see the following message: ### **Important note for the database and the data inside it** -Every Github codespace environment will have **its own database**, so if you're working with more people eveyone will have a different database and different records inside it. This data **will be lost**, so don't spend too much time manually creating records for testing, instead, you can automate adding records to your database by editing ```commands.py``` file inside ```/src/api``` folder. Edit line 32 function ```insert_test_data``` to insert the data according to your model (use the function ```insert_test_users``` above as an example). Then, all you need to do is run ```pipenv run insert-test-data```. +Every Github codespace environment will have **its own database**, so if you're working with more people eveyone will have a different database and different records inside it. This data **will be lost**, so don't spend too much time manually creating records for testing, instead, you can automate adding records to your database by editing `commands.py` file inside `/src/api` folder. Edit line 32 function `insert_test_data` to insert the data according to your model (use the function `insert_test_users` above as an example). Then, all you need to do is run `pipenv run insert-test-data`. ### Front-End Manual Installation: -- Make sure you are using node version 20 and that you have already successfully installed and runned the backend. +- Make sure you are using node version 20 and that you have already successfully installed and runned the backend. 1. Install the packages: `$ npm install` 2. Start coding! start the webpack dev server `$ npm run start` diff --git a/index.html b/index.html index 27a99f796e..644cfd5cb9 100644 --- a/index.html +++ b/index.html @@ -2,11 +2,11 @@ - + - Hello Rigo + TaskFlow
diff --git a/migrations/versions/0763d677d453_.py b/migrations/versions/0763d677d453_.py deleted file mode 100644 index 88964176f1..0000000000 --- a/migrations/versions/0763d677d453_.py +++ /dev/null @@ -1,35 +0,0 @@ -"""empty message - -Revision ID: 0763d677d453 -Revises: -Create Date: 2025-02-25 14:47:16.337069 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '0763d677d453' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('user', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('email', sa.String(length=120), nullable=False), - sa.Column('password', sa.String(), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email') - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('user') - # ### end Alembic commands ### diff --git a/package-lock.json b/package-lock.json index 8d43d98ab7..25d5ea9bc2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "1.0.1", "license": "ISC", "dependencies": { + "cally": "^0.8.0", + "framer-motion": "^12.23.24", + "motion": "^12.23.24", "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -73,6 +76,7 @@ "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -884,7 +888,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -997,14 +1000,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/node": { - "version": "16.11.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", - "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -1018,6 +1013,7 @@ "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1066,6 +1062,7 @@ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1242,6 +1239,12 @@ "node": ">= 0.4" } }, + "node_modules/atomico": { + "version": "1.79.2", + "resolved": "https://registry.npmjs.org/atomico/-/atomico-1.79.2.tgz", + "integrity": "sha512-mshhLRMeIltNYbnQnqgnrvJ/uDa8XDfTQcjw3ymOygQqwHIQ4Sp0LcNYMCbACkV3DtV+eDXb9szwU4qMUuGwYQ==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -1294,6 +1297,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -1312,8 +1316,7 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "node_modules/call-bind": { "version": "1.0.8", @@ -1374,6 +1377,15 @@ "node": ">=6" } }, + "node_modules/cally": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cally/-/cally-0.8.0.tgz", + "integrity": "sha512-jvQ2QMrsZM/ZPG/LWTkJEUPrp/ew1uS2KjKA/E6ru7mVvTMY2JgSagci9IghLmuamFh1pDajrxXAX4Qgo4FHbw==", + "license": "MIT", + "dependencies": { + "atomico": "^1.76.1" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001697", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001697.tgz", @@ -1797,6 +1809,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2287,6 +2300,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/framer-motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", + "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3153,6 +3193,47 @@ "node": "*" } }, + "node_modules/motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.24.tgz", + "integrity": "sha512-Rc5E7oe2YZ72N//S3QXGzbnXgqNrTESv8KKxABR20q2FLch9gHLo0JLyYo2hZ238bZ9Gx6cWhj9VO0IgwbMjCw==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.23.24", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3486,6 +3567,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3498,6 +3580,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -3921,7 +4004,6 @@ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "optional": true, - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -3933,7 +4015,6 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -4081,7 +4162,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -4100,8 +4180,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "node_modules/text-table": { "version": "0.2.0", @@ -4109,6 +4188,12 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4278,6 +4363,7 @@ "integrity": "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", @@ -4500,6 +4586,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz", "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", "dev": true, + "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -4950,7 +5037,6 @@ "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, "optional": true, - "peer": true, "requires": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -5044,14 +5130,6 @@ "@babel/types": "^7.20.7" } }, - "@types/node": { - "version": "16.11.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", - "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==", - "dev": true, - "optional": true, - "peer": true - }, "@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -5063,6 +5141,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "dev": true, + "peer": true, "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -5098,7 +5177,8 @@ "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true + "dev": true, + "peer": true }, "acorn-jsx": { "version": "5.3.2", @@ -5215,6 +5295,11 @@ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true }, + "atomico": { + "version": "1.79.2", + "resolved": "https://registry.npmjs.org/atomico/-/atomico-1.79.2.tgz", + "integrity": "sha512-mshhLRMeIltNYbnQnqgnrvJ/uDa8XDfTQcjw3ymOygQqwHIQ4Sp0LcNYMCbACkV3DtV+eDXb9szwU4qMUuGwYQ==" + }, "available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -5245,6 +5330,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -5257,8 +5343,7 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "call-bind": { "version": "1.0.8", @@ -5298,6 +5383,14 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, + "cally": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cally/-/cally-0.8.0.tgz", + "integrity": "sha512-jvQ2QMrsZM/ZPG/LWTkJEUPrp/ew1uS2KjKA/E6ru7mVvTMY2JgSagci9IghLmuamFh1pDajrxXAX4Qgo4FHbw==", + "requires": { + "atomico": "^1.76.1" + } + }, "caniuse-lite": { "version": "1.0.30001697", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001697.tgz", @@ -5600,6 +5693,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "dev": true, + "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5941,6 +6035,16 @@ "is-callable": "^1.2.7" } }, + "framer-motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", + "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", + "requires": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6490,6 +6594,28 @@ "brace-expansion": "^1.1.7" } }, + "motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.24.tgz", + "integrity": "sha512-Rc5E7oe2YZ72N//S3QXGzbnXgqNrTESv8KKxABR20q2FLch9gHLo0JLyYo2hZ238bZ9Gx6cWhj9VO0IgwbMjCw==", + "requires": { + "framer-motion": "^12.23.24", + "tslib": "^2.4.0" + } + }, + "motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "requires": { + "motion-utils": "^12.23.6" + } + }, + "motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -6702,6 +6828,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "requires": { "loose-envify": "^1.1.0" } @@ -6710,6 +6837,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "requires": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6988,7 +7116,6 @@ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "optional": true, - "peer": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -6999,8 +7126,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "optional": true, - "peer": true + "optional": true } } }, @@ -7100,7 +7226,6 @@ "integrity": "sha512-GWANVlPM/ZfYzuPHjq0nxT+EbOEDDN3Jwhwdg1D8TU8oSkktp8w64Uq4auuGLxFSoNTRDncTq2hQHX1Ld9KHkA==", "dev": true, "optional": true, - "peer": true, "requires": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -7113,8 +7238,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "optional": true, - "peer": true + "optional": true } } }, @@ -7124,6 +7248,11 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7228,6 +7357,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.9.tgz", "integrity": "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==", "dev": true, + "peer": true, "requires": { "esbuild": "^0.18.10", "fsevents": "~2.3.2", diff --git a/package.json b/package.json index 0caab10749..43c4f931a9 100755 --- a/package.json +++ b/package.json @@ -8,10 +8,10 @@ "main": "index.js", "scripts": { "dev": "vite", - "start": "vite", - "build": "vite build", - "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "start": "vite", + "build": "vite build", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" }, "author": { "name": "Alejandro Sanchez", @@ -30,13 +30,13 @@ "license": "ISC", "devDependencies": { "@types/react": "^18.2.18", - "@types/react-dom": "^18.2.7", - "@vitejs/plugin-react": "^4.0.4", - "eslint": "^8.46.0", - "eslint-plugin-react": "^7.33.1", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.3", - "vite": "^4.4.8" + "@types/react-dom": "^18.2.7", + "@vitejs/plugin-react": "^4.0.4", + "eslint": "^8.46.0", + "eslint-plugin-react": "^7.33.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.3", + "vite": "^4.4.8" }, "babel": { "presets": [ @@ -54,9 +54,12 @@ ] }, "dependencies": { + "cally": "^0.8.0", + "framer-motion": "^12.23.24", + "motion": "^12.23.24", "prop-types": "^15.8.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.18.0" + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.18.0" } } diff --git a/src/api/admin.py b/src/api/admin.py index 3eecb64140..6787066c5e 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -1,17 +1,26 @@ - + import os from flask_admin import Admin -from .models import db, User +from .models import db, User, Task, TareasAsignadas, Mision, Evento, Prioridad, Estado, Grupo, Categoria, Clan from flask_admin.contrib.sqla import ModelView + def setup_admin(app): app.secret_key = os.environ.get('FLASK_APP_KEY', 'sample key') app.config['FLASK_ADMIN_SWATCH'] = 'cerulean' admin = Admin(app, name='4Geeks Admin', template_mode='bootstrap3') - # Add your models here, for example this is how we add a the User model to the admin admin.add_view(ModelView(User, db.session)) # You can duplicate that line to add mew models - # admin.add_view(ModelView(YourModelName, db.session)) \ No newline at end of file + # admin.add_view(ModelView(YourModelName, db.session)) + admin.add_view(ModelView(Task, db.session)) + admin.add_view(ModelView(TareasAsignadas, db.session)) + admin.add_view(ModelView(Mision, db.session)) + admin.add_view(ModelView(Evento, db.session)) + admin.add_view(ModelView(Prioridad, db.session)) + admin.add_view(ModelView(Estado, db.session)) + admin.add_view(ModelView(Grupo, db.session)) + admin.add_view(ModelView(Clan, db.session)) + admin.add_view(ModelView(Categoria, db.session)) diff --git a/src/api/extensions.py b/src/api/extensions.py new file mode 100644 index 0000000000..3d1d121617 --- /dev/null +++ b/src/api/extensions.py @@ -0,0 +1,3 @@ +from flask_mail import Mail + +mail = Mail() \ No newline at end of file diff --git a/src/api/models.py b/src/api/models.py index da515f6a1a..7a99a25920 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,19 +1,222 @@ from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import String, Boolean -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import String, Boolean, Integer, ForeignKey, DateTime +from sqlalchemy.orm import Mapped, mapped_column, relationship +from datetime import datetime db = SQLAlchemy() + class User(db.Model): - id: Mapped[int] = mapped_column(primary_key=True) - email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) - password: Mapped[str] = mapped_column(nullable=False) - is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False) + __tablename__ = 'user' + id: Mapped[int] = mapped_column(Integer, primary_key=True) + email: Mapped[str] = mapped_column(String(100), unique=True, nullable=False) + password: Mapped[str] = mapped_column(String(255), nullable=False) + name: Mapped[str] = mapped_column(String(80), nullable=False) + photo: Mapped[str] = mapped_column(String(255), nullable=True) + bio: Mapped[str] = mapped_column(String(250), nullable=True) + phone: Mapped[str] = mapped_column(String(20), nullable=True) + age: Mapped[int] = mapped_column(Integer, nullable=True) + city: Mapped[str] = mapped_column(String(80), nullable=True) + gender: Mapped[str] = mapped_column(String(80), nullable=True) + twitter: Mapped[str] = mapped_column(String(80), nullable=True) + facebook: Mapped[str] = mapped_column(String(80), nullable=True) + instagram: Mapped[str] = mapped_column(String(80), nullable=True) + db_clan_user: Mapped[list['Clan']] = relationship(back_populates='db_user_clan') + db_tareas_asignadas_user: Mapped[list['TareasAsignadas']] = relationship(back_populates='db_user_tareas_asignadas') + def __repr__(self): + return f'{self.email}' def serialize(self): return { "id": self.id, "email": self.email, - # do not serialize the password, its a security breach + "name": self.name, + "photo": self.photo, + "bio": self.bio, + "phone": self.phone, + "age": self.age, + "city": self.city, + "gender": self.gender, + "twitter": self.twitter, + "facebook": self.facebook, + "instagram": self.instagram, + } + + +class Task(db.Model): + __tablename__ = 'task' + id: Mapped[int] = mapped_column(Integer, primary_key=True) + title: Mapped[str] = mapped_column(String(80), nullable=False) + date: Mapped[datetime] = mapped_column(DateTime, nullable=True) + description: Mapped[str] = mapped_column(String(250), nullable=True) + lat: Mapped[str] = mapped_column(String(255), nullable=True) + lng: Mapped[str] = mapped_column(String(255), nullable=True) + estado_id: Mapped[int] = mapped_column(ForeignKey('estado.id'), nullable=True) + db_estado_tareas: Mapped['Estado'] = relationship(back_populates='db_tareas_estado') + evento_id: Mapped[int] = mapped_column(ForeignKey('evento.id'), nullable=True) + db_evento_tareas: Mapped['Evento'] = relationship(back_populates='db_tareas_evento') + prioridad_id: Mapped[int] = mapped_column(ForeignKey('prioridad.id'), nullable=True) + db_prioridad_tareas: Mapped['Prioridad'] = relationship(back_populates='db_tareas_prioridad') + db_mision_tareas: Mapped[list['Mision']] = relationship(back_populates='db_tareas_mision') + db_tareas_asignadas_tareas: Mapped[list['TareasAsignadas']] = relationship(back_populates='db_tareas_tareas_asignadas') + + def __repr__(self): + return f'{self.title}' + + def serialize(self): + return { + "id": self.id, + "title": self.title, + "date": self.date, + "description": self.description, + "lat": self.lat, + "lng": self.lng + } + + +class Estado(db.Model): + __tablename__ = 'estado' + id: Mapped[int] = mapped_column(Integer, primary_key=True) + tipo: Mapped[str] = mapped_column(String(20), nullable=True) + db_tareas_estado: Mapped[list['Task']] = relationship(back_populates='db_estado_tareas') + + def __repr__(self): + return f'{self.tipo}' + + def serialize(self): + return { + "id": self.id, + "tipo": self.tipo + } + + +class Evento(db.Model): + __tablename__ = 'evento' + id: Mapped[int] = mapped_column(Integer, primary_key=True) + titulo: Mapped[str] = mapped_column(String(20), nullable=False) + lugar: Mapped[str] = mapped_column(String(100), nullable=False) + db_tareas_evento: Mapped[list['Task']] = relationship(back_populates='db_evento_tareas') + + def __repr__(self): + return f'{self.titulo}' + + def serialize(self): + return { + "id": self.id, + "titulo": self.titulo, + "lugar": self.lugar + } + + +class Prioridad(db.Model): + __tablename__ = 'prioridad' + id: Mapped[int] = mapped_column(Integer, primary_key=True) + nivel: Mapped[str] = mapped_column(String(20), nullable=False) + db_tareas_prioridad: Mapped[list['Task']] = relationship(back_populates='db_prioridad_tareas') + + def __repr__(self): + return f'{self.nivel}' + + def serialize(self): + return { + "id": self.id, + "nivel": self.nivel, + } + + +class Grupo(db.Model): + __tablename__ = 'grupo' + id: Mapped[int] = mapped_column(Integer, primary_key=True) + nombre: Mapped[str] = mapped_column(String(100), unique=True, nullable=False) + categoria_id: Mapped[int] = mapped_column(ForeignKey('categoria.id')) + db_categoria_grupo: Mapped['Categoria'] = relationship(back_populates='db_grupo_categoria') + fecha: Mapped[datetime] = mapped_column(DateTime, nullable=True) + codigo: Mapped[int] = mapped_column(Integer, nullable=True) + db_clan_grupo: Mapped[list['Clan']] = relationship(back_populates='db_grupo_clan') + db_mision_grupo: Mapped[list['Mision']] = relationship(back_populates='db_grupo_mision') + + def __repr__(self): + return f'{self.nombre}' + + def serialize(self): + return { + "id": self.id, + "nombre": self.nombre, + "categoria_id": self.categoria_id, + "fecha": self.fecha, + "codigo": self.codigo + } + + +class Categoria(db.Model): + __tablename__ = 'categoria' + id: Mapped[int] = mapped_column(Integer, primary_key=True) + nombre: Mapped[str] = mapped_column(String(80), nullable=False) + db_grupo_categoria: Mapped[list['Grupo']] = relationship(back_populates='db_categoria_grupo') + + def __repr__(self): + return f'{self.nombre}' + + def serialize(self): + return { + "id": self.id, + "nombre": self.nombre, + } + + +class Clan(db.Model): + __tablename__ = 'clan' + id: Mapped[int] = mapped_column(Integer, primary_key=True) + user_id: Mapped[int] = mapped_column(ForeignKey('user.id')) + db_user_clan: Mapped['User'] = relationship(back_populates='db_clan_user') + grupo_id: Mapped[int] = mapped_column(ForeignKey('grupo.id')) + db_grupo_clan: Mapped['Grupo'] = relationship(back_populates='db_clan_grupo') + + def __repr__(self): + return f'User = {self.user_id} y Grupo = {self.grupo_id}' + + def serialize(self): + return { + "id": self.id, + "user_id": self.user_id, + "grupo_id": self.grupo_id + } + + +class Mision(db.Model): + __tablename__ = 'mision' + id: Mapped[int] = mapped_column(Integer, primary_key=True) + tareas_id: Mapped[int] = mapped_column(ForeignKey('task.id')) + db_tareas_mision: Mapped['Task'] = relationship(back_populates='db_mision_tareas') + grupo_id: Mapped[int] = mapped_column(ForeignKey('grupo.id')) + db_grupo_mision: Mapped['Grupo'] = relationship(back_populates='db_mision_grupo') + + def __repr__(self): + return f'Grupo = {self.grupo_id} y Tarea = {self.tareas_id}' + + def serialize(self): + return { + "id": self.id, + "tareas_id": self.tareas_id, + "grupo_id": self.grupo_id + } + + +class TareasAsignadas(db.Model): + __tablename__ = 'tareas_asignadas' + id: Mapped[int] = mapped_column(Integer, primary_key=True) + user_id: Mapped[int] = mapped_column(ForeignKey('user.id')) + db_user_tareas_asignadas: Mapped['User'] = relationship(back_populates='db_tareas_asignadas_user') + tareas_id: Mapped[int] = mapped_column(ForeignKey('task.id')) + db_tareas_tareas_asignadas: Mapped['Task'] = relationship(back_populates='db_tareas_asignadas_tareas') + + def __repr__(self): + return f'User = {self.user_id} y Tarea = {self.tareas_id}' + + def serialize(self): + return { + "id": self.id, + "user_id": self.user_id, + "tareas_id": self.tareas_id } \ No newline at end of file diff --git a/src/api/routes.py b/src/api/routes.py index 029589a3a1..aaf10078df 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -1,22 +1,54 @@ -""" -This module takes care of starting the API Server, Loading the DB and Adding the endpoints -""" -from flask import Flask, request, jsonify, url_for, Blueprint +from flask import request, jsonify, Blueprint +import secrets +from werkzeug.security import generate_password_hash from api.models import db, User -from api.utils import generate_sitemap, APIException -from flask_cors import CORS - +import os +from flask_mail import Message +from api.extensions import mail api = Blueprint('api', __name__) -# Allow CORS requests to this API -CORS(api) +reset_tokens = {} +url_front = os.getenv("VITE_FRONTEND") + + +@api.route('/forgot-password', methods=['POST']) +def forgot_password(): + data = request.get_json() + email = data.get('email') + if not email: + return jsonify({"msg": "Falta correo"}), 400 + user = User.query.filter_by(email=email).first() + if not user: + return jsonify({"msg": "Usuario no encontrado o no registrado"}), 404 + + reset_email = f"{url_front}resetPassword/token" + + msg = Message( + 'Recupera contraseña', + html=f"

Da click Aqui para recuperar tu contraseña.

", + recipients=[email], + sender='taskflowproyect@gmail.com' + ) + mail.send(msg) -@api.route('/hello', methods=['POST', 'GET']) -def handle_hello(): + return jsonify({"msg": "Correo enviado exitosamente"}), 200 - response_body = { - "message": "Hello! I'm a message that came from the backend, check the network tab on the google inspector and you will see the GET request" - } - return jsonify(response_body), 200 +@api.route('/reset-password', methods=['POST']) +def reset_password(): + data = request.get_json() + email = data.get('email') + token = data.get('token') + new_password = data.get('new_password') + if not email or not token or not new_password: + return jsonify({"msg": "Faltan datos"}), 400 + if reset_tokens.get(email) != token: + return jsonify({"msg": "Token inválido"}), 401 + user = User.query.filter_by(email=email).first() + if not user: + return jsonify({"msg": "Usuario no encontrado"}), 404 + user.password = generate_password_hash(new_password) + db.session.commit() + reset_tokens.pop(email) + return jsonify({"msg": "Contraseña cambiada correctamente"}), 200 \ No newline at end of file diff --git a/src/api/routesTasks.py b/src/api/routesTasks.py new file mode 100644 index 0000000000..8da44fa139 --- /dev/null +++ b/src/api/routesTasks.py @@ -0,0 +1,221 @@ +from flask import request, jsonify, Blueprint +import secrets +from datetime import datetime, timedelta, timezone +import jwt +from werkzeug.security import check_password_hash +from api.models import db, Task, TareasAsignadas, Mision, Evento, Prioridad, Estado, User +from werkzeug.security import generate_password_hash + + +api_tasks = Blueprint('apiTasks', __name__) + +# Allow CORS requests to this API + + +@api_tasks.route('/tareas', methods=['GET']) +def get_tareas(): + varTareas = Task.query.all() + tareas_serialized = [] + for cicloFor_varTareas in varTareas: + tareas_serialized.append(cicloFor_varTareas.serialize()) + response_body = { + "Lista de Tareas": tareas_serialized + } + return jsonify(response_body), 200 + + +@api_tasks.route('//tareas', methods=['GET']) +def get_tareas_user(user_id): + varUser = User.query.get(user_id) + print(varUser) + if varUser is None: + return ({'msg': f'El usuario con ID {user_id} no existe'}), 404 + varTareas_lista = varUser.db_tareas_asignadas_user + lista_tareas_serialized = [] + + for cicloFor_varTareas_lista in varTareas_lista: + tabla_tareas_asignadas = cicloFor_varTareas_lista.db_tareas_tareas_asignadas + lista_tareas_serialized.append(tabla_tareas_asignadas.serialize()) + + response_body = { + "Lista de todas las Tareas del usario": lista_tareas_serialized + } + return jsonify(response_body), 200 + + +@api_tasks.route('//tareas/', methods=['POST']) +def asignar_tarea_user(user_id, tareas_id): + varUser = User.query.get(user_id) + varTareas = Task.query.get(tareas_id) + + if varUser is None: + return jsonify({'msg': f'Usuario con ID {user_id} no existe'}), 404 + if varTareas is None: + return jsonify({'msg': f'No hay Tarea con ID {tareas_id} Revisalo ramon'}), 404 + + new_task = TareasAsignadas(user_id=user_id, tareas_id=tareas_id) + db.session.add(new_task) + db.session.commit() + return jsonify({'msg': f'Se ha agregado la tarea {varTareas.titulo} correctamente en el Usuario {varUser.email}', + 'Nueva Asignacion': new_task.serialize()}), 200 + + +@api_tasks.route('//tareas', methods=['POST']) +def agregar_tarea_user(user_id): + data = request.get_json() + + varUser = User.query.get(user_id) + if varUser is None: + return jsonify({"msg": f"Usuario con ID {user_id} no existe"}), 404 + + title = data.get("title") + description = data.get("description") + lat = data.get("lat") + lng = data.get("lng") + estado_id = data.get("estado_id") + evento_id = data.get("evento_id") + prioridad_id = data.get("prioridad_id") + + if not title: + return jsonify({"msg": "El title es obligatorio"}), 400 + + nueva_tarea = Task( + title=title, + description=description, + lat=lat, + lng=lng, + estado_id=estado_id, + evento_id=evento_id, + prioridad_id=prioridad_id + ) + + db.session.add(nueva_tarea) + db.session.commit() + + asignacion = TareasAsignadas( + user_id=user_id, + tareas_id=nueva_tarea.id + ) + + db.session.add(asignacion) + db.session.commit() + + return jsonify({ + "msg": f"Tarea '{title}' creada y asignada al usuario {varUser.email}", + "tarea": nueva_tarea.serialize(), + "asignacion": asignacion.serialize() + }), 201 + + +@api_tasks.route('//tareas//editar', methods=['PUT']) +def editar_tarea_user(user_id, tareas_id): + varTarea = Task.query.get(tareas_id) + varUser = User.query.get(user_id) + if varTarea is None: + return jsonify({'msg': f'La tarea con ID {tareas_id} no existe'}), 404 + if varUser is None: + return jsonify({'msg': f'Usuario con ID {user_id} no existe'}), 404 + + body = request.get_json(silent=True) + if body is None: + return jsonify({'msg': 'No se encuentra body, no hay datos que actualizar'}), 400 + + if "title" in body: + varTarea.title = body["title"] + if "description" in body: + varTarea.description = body["description"] + if "date" in body: + try: + varTarea.date = datetime.fromisoformat(body["date"]) + except: + return jsonify({'msg': 'Fecha incorrecta. Usa formato ISO: YYYY-MM-DD HH:MM:SS'}), 400 + if "lat" in body: + varTarea.lat = body["lat"] + if "lng" in body: + varTarea.lng = body["lng"] + if "estado_id" in body: + varTarea.estado_id = body["estado_id"] + if "evento_id" in body: + varTarea.evento_id = body["evento_id"] + if "prioridad_id" in body: + varTarea.prioridad_id = body["prioridad_id"] + + db.session.commit() + + return jsonify({'msg': 'Se ha editado la Tarea Correctamente', 'Tarea': varTarea.serialize()}), 202 + + +@api_tasks.route('//tareas//editar/eventos/', methods=['PUT']) +def editar_evento_user(user_id, tareas_id, evento_id): + varTarea = Task.query.get(tareas_id) + varUser = User.query.get(user_id) + varEvento = Evento.query.get(evento_id) + if varTarea is None: + return jsonify({'msg': f'La tarea con ID {tareas_id} no existe'}), 404 + if varUser is None: + return jsonify({'msg': f'Usuario con ID {user_id} no existe'}), 404 + if varEvento is None: + return jsonify({'msg': f'El Evento con ID {evento_id} no existe'}), 404 + + body = request.get_json(silent=True) + if body is None: + return jsonify({'msg': 'No se encuentra body, no hay datos que actualizar'}), 400 + + if "titulo" in body: + varEvento.titulo = body["titulo"] + if "lugar" in body: + varEvento.lugar = body["lugar"] + + db.session.commit() + + return jsonify({'msg': 'Se ha editado el evento Correctamente', 'Evento': varEvento.serialize()}), 202 + + +@api_tasks.route('//tareas//desasignar', methods=['DELETE']) +def desasignar_tarea(user_id, tareas_id): + varUser = User.query.get(user_id) + if varUser is None: + return jsonify({'msg': f'El usuario con ID {user_id} no existe'}), 404 + + varTareasAsignadas = TareasAsignadas.query.filter_by( + user_id=user_id, + tareas_id=tareas_id + ).first() + + if varTareasAsignadas is None: + return jsonify({'msg': 'La tarea no está asignada a este usuario'}), 404 + + db.session.delete(varTareasAsignadas) + db.session.commit() + + return jsonify({ + 'msg': f'La tarea con ID {tareas_id} ha sido des-asignada del Usuario {user_id}' + }), 200 + + +@api_tasks.route('//tareas//eliminar', methods=['DELETE']) +def eliminar_tarea(user_id, tareas_id): + varUser = User.query.get(user_id) + if varUser is None: + return jsonify({'msg': f'El usuario con ID {user_id} no existe'}), 404 + + varTarea = Task.query.get(tareas_id) + if varTarea is None: + return jsonify({'msg': f'La tarea con ID {tareas_id} no existe'}), 404 + + TareasAsignadas.query.filter_by(tareas_id=tareas_id).delete() + + db.session.delete(varTarea) + db.session.commit() + + return jsonify({'msg': f'La tarea con ID {tareas_id} ha sido eliminada correctamente'}), 200 + + +@api_tasks.route('/hello', methods=['POST', 'GET']) +def handle_hello(): + + response_body = { + "message": "Hola! I'm a message that came from the backend, check the network tab on the google inspector and you will see the GET request" + } + + return jsonify(response_body), 200 diff --git a/src/api/routesUser.py b/src/api/routesUser.py new file mode 100644 index 0000000000..61e4768816 --- /dev/null +++ b/src/api/routesUser.py @@ -0,0 +1,141 @@ +from flask import request, jsonify, Blueprint +from datetime import datetime, timedelta, timezone +from api.models import db, User +from werkzeug.security import generate_password_hash, check_password_hash +import jwt +from flask_cors import CORS + +api_user = Blueprint('apiUser', __name__) +SECRET_KEY = "super-secret-key" +CORS (api_user) +def token_requerido(f): + def wrapper(*args, **kwargs): + if request.method == 'OPTIONS': + return jsonify({}), 200 + auth = request.headers.get('Authorization') + if not auth or not auth.startswith('Bearer '): + return jsonify({"msg": "Token requerido"}), 401 + token = auth.split(' ')[1] + try: + jwt.decode(token, SECRET_KEY, algorithms=['HS256']) + except Exception: + return jsonify({"msg": "Token inválido"}), 401 + return f(*args, **kwargs) + wrapper.__name__ = f.__name__ + return wrapper + +@api_user.route('/register', methods=['POST']) +def create_profile(): + print ("hola") + body = request.get_json() + email = body.get('email') + password = body.get('password') + name = body.get('name') + if not email or not password or not name: + return jsonify({"msg": "Falta correo, contraseña o nombre"}), 400 + if User.query.filter_by(email=email).first(): + return jsonify({"msg": "El usuario ya existe"}), 400 + hashed_password = generate_password_hash(password) + nuevo_perfil = User( + email=email, + password=hashed_password, + name=name, + photo=body.get("photo"), + bio=body.get("bio"), + phone=body.get("phone"), + age=body.get("age"), + city=body.get("city"), + gender=body.get("gender"), + twitter=body.get("twitter"), + facebook=body.get("facebook"), + instagram=body.get("instagram"), + ) + db.session.add(nuevo_perfil) + db.session.commit() + return jsonify({"msg": "Perfil creado correctamente", "perfil": nuevo_perfil.serialize()}), 201 + +@api_user.route('/login', methods=['POST']) +def login(): + data = request.get_json() + email = data.get('email') + password = data.get('password') + if not email or not password: + return jsonify({"msg": "Falta correo o contraseña"}), 400 + user = User.query.filter_by(email=email).first() + if not user or not check_password_hash(user.password, password): + return jsonify({"msg": "Usuario o contraseña incorrectos"}), 401 + token = jwt.encode({ + 'user_id': user.id, + 'exp': datetime.now(timezone.utc) + timedelta(minutes=15) + }, SECRET_KEY, algorithm="HS256") + return jsonify({"token": token, "user": user.serialize()}), 200 + +@api_user.route('/', methods=['GET']) +@token_requerido +def get_user(): + auth = request.headers.get('Authorization') + token = auth.split(' ')[1] + payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256']) + user = User.query.get(payload['user_id']) + if not user: + return jsonify({"msg": "Usuario no encontrado"}), 404 + return jsonify(user.serialize()), 200 + + +@api_user.route('/', methods=['PUT', 'OPTIONS']) +@token_requerido +def update_user(user_id): + if request.method == 'OPTIONS': + return jsonify({}), 200 + + data = request.get_json() or {} + auth = request.headers.get('Authorization', '') + if not auth.startswith('Bearer '): + return jsonify({"msg": "Token no proporcionado"}), 401 + + token = auth.split(' ')[1] + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256']) + except jwt.ExpiredSignatureError: + return jsonify({"msg": "Token expirado"}), 401 + except jwt.InvalidTokenError: + return jsonify({"msg": "Token inválido"}), 401 + + if payload['user_id'] != user_id: + return jsonify({"msg": "No autorizado para actualizar este usuario"}), 403 + + user = User.query.get(user_id) + if not user: + return jsonify({"msg": "Usuario no encontrado"}), 404 + + email = data.get('email') + if email: + if User.query.filter(User.email == email, User.id != user.id).first(): + return jsonify({"msg": "El email ya está en uso"}), 400 + user.email = email + + password = data.get('password') + if password: + user.password = generate_password_hash(password) + + user.name = data.get('name', user.name) + user.photo = data.get('photo', user.photo) + user.bio = data.get('bio', user.bio) + user.phone = data.get('phone', user.phone) + user.age = data.get('age', user.age) + user.city = data.get('city', user.city) + user.gender = data.get('gender', user.gender) + user.twitter = data.get('twitter', user.twitter) + user.facebook = data.get('facebook', user.facebook) + user.instagram = data.get('instagram', user.instagram) + + db.session.commit() + + return jsonify({"msg": "Usuario actualizado correctamente", "perfil": user.serialize()}), 200 + +@api_user.route('/Saluda', methods=['POST', 'GET']) +def handle_hello(): + response_body = { + "message": "Este ya es el endpoint de Los usuarios Osea de cada user de la tabla" + } + return jsonify(response_body), 200 \ No newline at end of file diff --git a/src/app.py b/src/app.py index 1b3340c0fa..c4ce89df95 100644 --- a/src/app.py +++ b/src/app.py @@ -1,6 +1,3 @@ -""" -This module takes care of starting the API Server, Loading the DB and Adding the endpoints -""" import os from flask import Flask, request, jsonify, url_for, send_from_directory from flask_migrate import Migrate @@ -8,18 +5,30 @@ from api.utils import APIException, generate_sitemap from api.models import db from api.routes import api +from api.routesUser import api_user +from flask_cors import CORS from api.admin import setup_admin from api.commands import setup_commands +from api.routesTasks import api_tasks +from api.extensions import mail +from flask_mail import Mail -# from models import Person ENV = "development" if os.getenv("FLASK_DEBUG") == "1" else "production" static_file_dir = os.path.join(os.path.dirname( os.path.realpath(__file__)), '../dist/') app = Flask(__name__) -app.url_map.strict_slashes = False -# database condiguration + +app.url_map.strict_slashes = False +CORS(app, resources={ + r"/api/*": { + "origins": "*", + "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + "allow_headers": ["Content-Type", "Authorization"], + "supports_credentials": True + } +}) db_url = os.getenv("DATABASE_URL") if db_url is not None: app.config['SQLALCHEMY_DATABASE_URI'] = db_url.replace( @@ -28,27 +37,36 @@ app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:////tmp/test.db" app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +app.config.update(dict( + DEBUG=False, + MAIL_SERVER='smtp.gmail.com', + MAIL_PORT=587, + MAIL_USE_TLS=True, + MAIL_USE_SSL=False, + MAIL_USERNAME='taskflowproyect@gmail.com', + MAIL_PASSWORD=os.getenv('MAIL_PASSWORD') +)), + + MIGRATE = Migrate(app, db, compare_type=True) db.init_app(app) -# add the admin -setup_admin(app) -# add the admin +mail.init_app(app) + +setup_admin(app) setup_commands(app) -# Add all endpoints form the API with a "api" prefix app.register_blueprint(api, url_prefix='/api') - -# Handle/serialize errors like a JSON object +app.register_blueprint(api_user, url_prefix='/api/users') +app.register_blueprint(api_tasks, url_prefix='/api/users') @app.errorhandler(APIException) def handle_invalid_usage(error): return jsonify(error.to_dict()), error.status_code -# generate sitemap with all your endpoints - @app.route('/') def sitemap(): @@ -56,17 +74,16 @@ def sitemap(): return generate_sitemap(app) return send_from_directory(static_file_dir, 'index.html') -# any other endpoint will try to serve it like a static file + @app.route('/', methods=['GET']) def serve_any_other_file(path): if not os.path.isfile(os.path.join(static_file_dir, path)): path = 'index.html' response = send_from_directory(static_file_dir, path) - response.cache_control.max_age = 0 # avoid cache memory + response.cache_control.max_age = 0 return response -# this only runs if `$ python src/main.py` is executed if __name__ == '__main__': PORT = int(os.environ.get('PORT', 3001)) - app.run(host='0.0.0.0', port=PORT, debug=True) + app.run(host='0.0.0.0', port=PORT, debug=True) \ No newline at end of file diff --git a/src/front/components/Footer.jsx b/src/front/components/Footer.jsx index f06302dbd2..8361fa8a93 100644 --- a/src/front/components/Footer.jsx +++ b/src/front/components/Footer.jsx @@ -1,11 +1,5 @@ export const Footer = () => ( ); diff --git a/src/front/components/Form.jsx b/src/front/components/Form.jsx new file mode 100644 index 0000000000..37eb07bb2e --- /dev/null +++ b/src/front/components/Form.jsx @@ -0,0 +1,213 @@ +import React, { useState, useEffect } from "react"; +import "../styles/Form.css"; +import { Link, useLocation } from "react-router-dom"; + +const Form = ({ mode, onSubmit, successMessage, userData }) => { + const [email, setEmail] = useState(userData?.email || ""); + const [name, setName] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [newEmail, setNewEmail] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmNewPassword, setConfirmNewPassword] = useState(""); + const [errorMsn, setErrorMsn] = useState(null); + const location = useLocation(); + + useEffect(() => { + if (successMessage) setErrorMsn(successMessage); + if (userData) { + setEmail(userData.email); + setNewEmail(userData.email); + } + }, [successMessage, userData]); + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (mode === "register" && password !== confirmPassword) { + setErrorMsn("Las contraseñas no coinciden"); + return; + } + + if (mode === "config" && newPassword && newPassword !== confirmNewPassword) { + setErrorMsn("Las nuevas contraseñas no coinciden"); + return; + } + + try { + if (mode === "config") { + await onSubmit({ newEmail, newPassword, setErrorMsn }); + setPassword(""); + setConfirmPassword(""); + setNewPassword(""); + setConfirmNewPassword(""); + setName(""); + } else { + if (mode === "register") { + await onSubmit({ email, password, name, setErrorMsn }); + } else { + await onSubmit({ email, password, setErrorMsn }); + } + + setEmail(""); + setPassword(""); + setConfirmPassword(""); + setName(""); + } + } catch (error) { + setErrorMsn(error.message || "Error"); + } + }; + + return ( +
+
+ {mode !== "config" && ( +
+ Visual +
+ )} + +
+ +
+

+ {mode === "register" && "Registrarse"} + {mode === "login" && "Iniciar Sesión"} + {mode === "config" && "Configuración"} +

+ + {errorMsn && ( +
+ {errorMsn} +
+ )} + +
+ {mode === "register" && ( +
+ + setName(e.target.value)} + required + /> +
+ )} + + {(mode === "register" || mode === "login") && ( + <> +
+ + setEmail(e.target.value)} + required + /> +
+ +
+ + setPassword(e.target.value)} + required + /> +
+ + {mode === "register" && ( +
+ + setConfirmPassword(e.target.value)} + required + /> +
+ )} + + )} + + {mode === "config" && ( + <> +
+ + +
+ +
+ + setNewEmail(e.target.value)} + /> +
+ +
+ + setNewPassword(e.target.value)} + /> + setConfirmNewPassword(e.target.value)} + /> +
+ + )} + + + + {mode === "register" ? ( +

+ ¿Ya tienes cuenta? Inicia sesión +

+ ) : mode === "login" ? ( +

+ ¿No tienes cuenta? Regístrate +

+ ) : null} + + {mode === "config" && ( + + Cancelar + + )} +
+
+
+
+
+ ); +}; + +export default Form; \ No newline at end of file diff --git a/src/front/components/GoogleMaps.jsx b/src/front/components/GoogleMaps.jsx new file mode 100644 index 0000000000..f404749326 --- /dev/null +++ b/src/front/components/GoogleMaps.jsx @@ -0,0 +1,143 @@ +import React, { useState, useRef, useEffect } from "react"; + + +function GoogleMaps({ lat, lng, setLat, setLng, address, setAddress }) { + const mapRef = useRef(null); + const markerRef = useRef(null); + const [search, setSearch] = useState(""); + + const fetchAddress = (latitude, longitude) => { + const key = import.meta.env.VITE_GOOGLE_MAPS_KEY || ''; + fetch(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&key=${key}`) + .then(res => res.json()) + .then(data => { + if (data.status === "OK" && data.results.length > 0) { + // Filtrar para evitar códigos Plus y 'Sin Nombre' + const result = data.results.find(r => + !/^([A-Z0-9]{4,}\+\w{2,})/.test(r.formatted_address) && + !r.formatted_address.includes('Sin Nombre') + ); + setAddress(result ? result.formatted_address : ""); + } else { + setAddress(""); + } + }) + .catch(() => setAddress("")); + }; + + const fetchCoords = (direccion) => { + const key = import.meta.env.VITE_GOOGLE_MAPS_KEY || ''; + fetch(`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(direccion)}&key=${key}`) + .then(res => res.json()) + .then(data => { + if (data.status === "OK" && data.results.length > 0) { + const loc = data.results[0].geometry.location; + setLat(loc.lat); + setLng(loc.lng); + setAddress(data.results[0].formatted_address); + } + }); + }; + + useEffect(() => { + fetchAddress(lat, lng); + }, [lat, lng]); + + useEffect(() => { + function safeInitMap() { + if (window.google && window.google.maps && typeof window.google.maps.Map === "function") { + const center = { lat: lat ? parseFloat(lat) : 20, lng: lng ? parseFloat(lng) : -99 }; + const map = new window.google.maps.Map(mapRef.current, { + center, + zoom: 5, + mapId: "12fb4a783b70dc8349a13bb3" + }); + if (window.google.maps.marker && window.google.maps.marker.AdvancedMarkerElement) { + markerRef.current = new window.google.maps.marker.AdvancedMarkerElement({ + position: center, + map, + draggable: true, + }); + map.addListener("click", (e) => { + markerRef.current.position = e.latLng; + setLat(e.latLng.lat()); + setLng(e.latLng.lng()); + fetchAddress(e.latLng.lat(), e.latLng.lng()); + }); + markerRef.current.addListener("dragend", (e) => { + setLat(e.latLng.lat()); + setLng(e.latLng.lng()); + fetchAddress(e.latLng.lat(), e.latLng.lng()); + }); + } else { + markerRef.current = new window.google.maps.Marker({ + position: center, + map, + draggable: true, + }); + map.addListener("click", (e) => { + markerRef.current.setPosition(e.latLng); + setLat(e.latLng.lat()); + setLng(e.latLng.lng()); + fetchAddress(e.latLng.lat(), e.latLng.lng()); + }); + markerRef.current.addListener("dragend", (e) => { + setLat(e.latLng.lat()); + setLng(e.latLng.lng()); + fetchAddress(e.latLng.lat(), e.latLng.lng()); + }); + } + } else { + setTimeout(safeInitMap, 100); + } + } + + const key = import.meta.env.VITE_GOOGLE_MAPS_KEY || ''; + const scriptId = "google-maps-script"; + const existingScript = document.getElementById(scriptId); + if (!window.google) { + if (!existingScript) { + const script = document.createElement("script"); + script.id = scriptId; + script.src = `https://maps.googleapis.com/maps/api/js?key=${key}&libraries=marker&loading=async`; + script.async = true; + script.defer = true; + script.onload = safeInitMap; + script.onerror = function () { + if (mapRef.current) { + mapRef.current.innerHTML = '
No se pudo cargar el mapa. Verifica tu conexión o la clave de Google Maps.
'; + } + }; + document.body.appendChild(script); + } else { + existingScript.onload = safeInitMap; + existingScript.onerror = function () { + if (mapRef.current) { + mapRef.current.innerHTML = '
No se pudo cargar el mapa. Verifica tu conexión o la clave de Google Maps.
'; + } + }; + } + } else { + safeInitMap(); + } + }, [lat, lng]); + + return ( + <> + { + setAddress(e.target.value); + setSearch(e.target.value); + }} + onBlur={() => search && fetchCoords(search)} + style={{ width: "100%", marginBottom: 8, borderRadius: 8, padding: 8 }} + placeholder="Dirección" + /> +
+ + ); +} + +export default GoogleMaps; diff --git a/src/front/components/LandingCards.jsx b/src/front/components/LandingCards.jsx new file mode 100644 index 0000000000..c392adf3e3 --- /dev/null +++ b/src/front/components/LandingCards.jsx @@ -0,0 +1,54 @@ +import React from "react"; +import { motion } from "framer-motion"; +import "../styles/Landing.css"; + +const cards = [ + { + title: "Organiza tus tareas", + desc: "Crea, asigna y sigue el progreso de tus tareas en tu equipo.", + img: "https://img.freepik.com/foto-gratis/papeles-comerciales-naturaleza-muerta-varias-piezas-mecanismo_23-2149352652.jpg?semt=ais_hybrid&w=740&q=80" + }, + { + title: "Comunicación centralizada", + desc: "Muro interno para chatear y mantener todo en un solo lugar.", + img: "https://media.istockphoto.com/id/1406124833/es/vector/problemas-en-el-concepto-de-vector-de-comunicaci%C3%B3n.jpg?s=612x612&w=0&k=20&c=5pkMR8J1QN8VWhnOBmEHk5n7ydB_9DfoBDf8khIEec4=" + }, + { + title: "Integración con Google Maps", + desc: "Nunca te perderás: Google Maps te guía en cada tarea y en cada rincón.", + img: "https://i.blogs.es/635f55/maps/1366_2000.jpg" + }, + { + title: "Multiplataforma", + desc: "Accede desde móvil, tablet o escritorio sin perder información.", + img: "https://kinsta.com/es/wp-content/uploads/sites/8/2020/09/diseno-de-paginas-web-sensibles-1024x512.jpg" + }, +]; + +export default function LandingCards() { + return ( +
+ {cards.map((card, idx) => ( + + +

{card.title}

+

{card.desc}

+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/src/front/components/ModalCreateTask.jsx b/src/front/components/ModalCreateTask.jsx new file mode 100644 index 0000000000..7fd6711b5c --- /dev/null +++ b/src/front/components/ModalCreateTask.jsx @@ -0,0 +1,198 @@ +import React, { useState, useEffect } from "react"; +import useGlobalReducer from "../hooks/useGlobalReducer"; +import GoogleMaps from "../components/GoogleMaps"; + +function ModalCreateTask({ setShowTaskModal, taskType, taskToEdit = null }) { + const { store, dispatch } = useGlobalReducer(); + const activeClanId = store.activeClanId; + + const isEditing = !!taskToEdit; + const modalTitle = isEditing + ? (taskType === 'user' ? "Editar Tarea Personal" : "Editar Tarea de Clan") + : (taskType === 'user' ? "Nueva Tarea Personal" : "Nueva Tarea de Clan"); + + + + // Estados + const [titulo, setTitulo] = useState(""); + const [fecha, setFecha] = useState(""); + const [descripcion, setDescripcion] = useState(""); + const [direccion, setDireccion] = useState(""); + const [date, setDate] = useState(""); + const [lat, setLat] = useState(""); + const [lng, setLng] = useState(""); + + const [msg, setMsg] = useState(""); + + useEffect(() => { + if (taskToEdit) { + setTitulo(taskToEdit.title || ""); + setFecha(taskToEdit.date || ""); + setDescripcion(taskToEdit.description || ""); + setDireccion(taskToEdit.address || ""); + setLat(taskToEdit.lat || ""); + setLng(taskToEdit.lng || ""); + } else { + setTitulo(""); + setFecha(""); + setDescripcion(""); + setDireccion(""); + } + }, [taskToEdit]); + + // Solo un input de dirección, sincronizado con el mapa + + useEffect(() => { + if (taskToEdit) { + setTitulo(taskToEdit.title || ""); + setDescripcion(taskToEdit.description || ""); + setDireccion(taskToEdit.address || ""); + setLat(taskToEdit.lat || ""); + setLng(taskToEdit.lng || ""); + } else { + setTitulo(""); + setDescripcion(""); + setDireccion(""); + } + }, [taskToEdit]); + + // Sincronización: si el mapa cambia la dirección, actualiza el input + const handleMapAddressChange = (address) => { + setDireccion(address); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setMsg(""); + + const backendUrl = import.meta.env.VITE_BACKEND_URL; + + const payloadData = { + title: titulo, + description: descripcion, + lat: lat, + lng: lng, + estado_id: null, + evento_id: null, + prioridad_id: null + }; + + try { + if (taskType === "user") { + + // CREAR + if (!isEditing) { + const response = await fetch( + `${backendUrl}/api/users/${store.user.id}/tareas`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${store.token}` + }, + body: JSON.stringify(payloadData) + } + ); + + // --- DEBUG --- + console.log("STATUS:", response.status); + console.log("HEADERS:", response.headers); + + // usamos text() en lugar de json() para ver TODO lo que responde Flask + const text = await response.text(); + console.log("RAW BACKEND RESPONSE:", text); + + let data; + try { + data = JSON.parse(text); + } catch (err) { + console.error("La respuesta NO es JSON válido:", text); + } + // --- FIN DEBUG --- + + if (!response.ok) { + setMsg(data.msg || "Error creando tarea"); + return; + } + + dispatch({ + type: "ADD_USER_TASK", + payload: data.tarea + }); + + } + // EDITAR + else { + const response = await fetch( + `${backendUrl}/api/users/${store.user.id}/tareas/${taskToEdit.id}/editar`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${store.token}` + }, + body: JSON.stringify(payloadData) + } + ); + + const data = await response.json(); + + if (!response.ok) { + setMsg(data.msg || "Error editando tarea"); + return; + } + + dispatch({ + type: "UPDATE_USER_TASK", + payload: data.Tarea + }); + } + } + + setShowTaskModal(false); + + } catch (error) { + console.error("Error conectando con backend:", error); + setMsg("No se pudo conectar con el servidor"); + } + }; + + return ( +
+
+
+
+
+ {modalTitle} +
+ +
+ setTitulo(e.target.value)} + style={{ width: "100%", marginBottom: 12, border: "1px solid #1e91ed", borderRadius: 8, padding: 10 }} + /> + +
+
+
+
+
+
+
+
+
Redes
+
+
+
+
+
+ +
+ + +
+ + + + + )} + + {showBoteModal && ( +
+
+
+
+
+
Editar Saldo del Bote Personal
+ +
+
+
+ + setBoteAmount(e.target.value)} placeholder="0" /> +
+
+
+ + +
+
+
+
+
+ )} + +
+
+
+ + + + + + +
+
Amigos activos
+ {store.friends.map(friend => ())} +
+
+ +
+
+
{tasksCompleted}
Tareas completadas
+
{tasksPending}
Tareas sin hacer
+
{clanCount}
Clan
+
+ +
+
+
+

Detalles

+

{store.profile.bio}

+
+

{store.profile.city}

+

{store.profile.age} años

+

{store.profile.phone}

+

{store.profile.gender}

+
+
+ +
+
+

Mensajes

+

No hay mensajes nuevos.

+
+
+ +
+
+

Otras redes

+

{store.profile.instagram || ""}

+

{store.profile.twitter || ""}

+

{store.profile.facebook || ""}

+
+
+ +
+
+

Gastos del Mes

+ +

{totalExpenses.toFixed(2)}€

+ Ver Detalles +
+
+
+ +
+
+
+

Saldo del Bote Personal

+ +
+

{store.personalBote.toFixed(2)} €

+
+
+
+
+
+ + + ); +}; \ No newline at end of file diff --git a/src/front/pages/Register.jsx b/src/front/pages/Register.jsx new file mode 100644 index 0000000000..7ae707869e --- /dev/null +++ b/src/front/pages/Register.jsx @@ -0,0 +1,43 @@ +import React from "react"; +import { useNavigate } from "react-router-dom"; +import Form from "../components/Form.jsx"; + +const Register = () => { + const navigate = useNavigate(); + + const handleSignup = async ({ email, password, name, setErrorMsn }) => { + setErrorMsn(null); + const backendUrl = import.meta.env.VITE_BACKEND_URL; + + try { + const response = await fetch(`${backendUrl}/api/users/register`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password, name }), + }); + + const data = await response.json(); + + if (!response.ok) { + setErrorMsn(data.msg || "Error en el registro"); + console.error(data.msg || "Error en el registro"); + return; + } + + navigate("/login", { + state: { successMessage: "Registro exitoso. Ahora puedes iniciar sesión." }, + }); + } catch (error) { + setErrorMsn("Error al conectar con el servidor"); + console.error("Error al conectar con el servidor", error); + } + }; + + return ( +
+
+
+ ); +}; + +export default Register; \ No newline at end of file diff --git a/src/front/routes.jsx b/src/front/routes.jsx index 0557df6141..c08b0ef31c 100644 --- a/src/front/routes.jsx +++ b/src/front/routes.jsx @@ -1,30 +1,89 @@ // Import necessary components and functions from react-router-dom. import { - createBrowserRouter, - createRoutesFromElements, - Route, + createBrowserRouter, + createRoutesFromElements, + Route, } from "react-router-dom"; +import PrivateRoute from "./components/PrivateRoute"; import { Layout } from "./pages/Layout"; -import { Home } from "./pages/Home"; +import { Dashboard } from "./pages/Dashboard"; import { Single } from "./pages/Single"; import { Demo } from "./pages/Demo"; - +import Register from "./pages/Register"; +import Login from "./pages/Login"; +import { Profile } from "./pages/Profile"; +import { Groups } from "./pages/Groups"; +import { Finances } from "./pages/Finances"; +import { Config } from "./pages/Config" +import Home from "./pages/Home"; +import { Chat } from "./pages/Chat"; export const router = createBrowserRouter( - createRoutesFromElements( + createRoutesFromElements( // CreateRoutesFromElements function allows you to build route elements declaratively. // Create your routes here, if you want to keep the Navbar and Footer in all views, add your new routes inside the containing Route. // Root, on the contrary, create a sister Route, if you have doubts, try it! // Note: keep in mind that errorElement will be the default page when you don't get a route, customize that page to make your project more attractive. // Note: The child paths of the Layout element replace the Outlet component with the elements contained in the "element" attribute of these child paths. - // Root Route: All navigation will start from here. - } errorElement={

Not found!

} > + // Root Route: All navigation will start from here. + } errorElement={

Not found!

} > + } /> + } /> {/* Ruta explícita */} + {/* Nested Routes: Defines sub-routes within the BaseHome component. */} - {/* Nested Routes: Defines sub-routes within the BaseHome component. */} - } /> - } /> {/* Dynamic route for single items */} - } /> - - ) + } /> {/* Dynamic route for single items */} + } /> + } /> + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + ) ); \ No newline at end of file diff --git a/src/front/store.js b/src/front/store.js index 3062cd222d..4772cbc00c 100644 --- a/src/front/store.js +++ b/src/front/store.js @@ -1,38 +1,222 @@ -export const initialStore=()=>{ - return{ - message: null, - todos: [ - { - id: 1, - title: "Make the bed", - background: null, - }, - { - id: 2, - title: "Do my homework", - background: null, - } - ] - } -} +export const initialStore = () => { + const savedUser = localStorage.getItem("user"); + const savedProfile = localStorage.getItem("profile"); + const savedToken = localStorage.getItem("token"); + + return { + token: savedToken || null, + user: savedUser ? JSON.parse(savedUser) : null, + profile: savedProfile ? JSON.parse(savedProfile) : null, + friends: [], + clans: [], + activeClanId: null, + userTasks: [], + clanTasks: [], + personalBote: 0, + personalExpenses: [], + expenses: [], + commonBote: {}, + balances: [], + chatMessages: [] + }; +}; export default function storeReducer(store, action = {}) { - switch(action.type){ - case 'set_hello': + switch (action.type) { + + case "RESET_STORE": + localStorage.removeItem("token"); + localStorage.removeItem("user"); + localStorage.removeItem("profile"); + return initialStore(); + + case "LOAD_DATA_FROM_BACKEND": + const { user, profile, userTasks, clans, clanTasks, token } = action.payload; + if (token) localStorage.setItem("token", token); + localStorage.setItem("user", JSON.stringify(user)); + localStorage.setItem("profile", JSON.stringify(profile)); + return { + ...store, + token: token || store.token, + user, + profile, + userTasks: userTasks || [], + clans: clans || [], + clanTasks: clanTasks || [] + }; + + case "SET_TOKEN": + localStorage.setItem("token", action.payload.token); + return { ...store, token: action.payload.token }; + + case "LOGOUT": + localStorage.removeItem("token"); + localStorage.removeItem("user"); + localStorage.removeItem("profile"); + return initialStore(); + + case "ADD_USER_TASK": + const newUserTask = { + id: action.payload.id, + title: action.payload.title, + description: action.payload.description || "", + address: action.payload.address || "", + date: new Date().toISOString().split("T")[0], + time: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), + latitude: action.payload.lat, + longitude: action.payload.lng, + guests: [], + completed: false + }; + return { ...store, userTasks: [...store.userTasks, newUserTask] }; + + case "UPDATE_USER_TASK": + return { + ...store, + userTasks: store.userTasks.map(task => + task.id === action.payload.id ? { ...task, ...action.payload } : task + ) + }; + + case "TOGGLE_USER_TASK": + return { + ...store, + userTasks: store.userTasks.map(task => + task.id === action.payload.taskId ? { ...task, completed: !task.completed } : task + ) + }; + + case "DELETE_USER_TASK": + return { + ...store, + userTasks: store.userTasks.filter(t => t.id !== action.payload.taskId) + }; + + case "ADD_TASK_TO_CLAN": + if (!store.activeClanId) return store; + const newClanTask = { + id: new Date().getTime(), + clanId: action.payload.clanId || store.activeClanId, + title: action.payload.title, + description: action.payload.description || "", + address: action.payload.address || "", + date: new Date().toISOString().split("T")[0], + time: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), + completed: false + }; + return { ...store, clanTasks: [...store.clanTasks, newClanTask] }; + + case "UPDATE_CLAN_TASK": + return { + ...store, + clanTasks: store.clanTasks.map(task => + task.id === action.payload.id ? { ...task, ...action.payload } : task + ) + }; + + case "TOGGLE_CLAN_TASK": + return { + ...store, + clanTasks: store.clanTasks.map(task => + task.id === action.payload.taskId ? { ...task, completed: !task.completed } : task + ) + }; + + case "DELETE_CLAN_TASK": + return { + ...store, + clanTasks: store.clanTasks.filter(t => t.id !== action.payload.taskId) + }; + + case "SET_ACTIVE_CLAN": + return { ...store, activeClanId: action.payload.clanId }; + + case "CREATE_CLAN": + const newClan = { + id: new Date().getTime(), + members: 1, + created: new Date().toISOString().split("T")[0], + ...action.payload + }; + return { ...store, clans: [...store.clans, newClan] }; + + case "DELETE_CLAN": + if (!store.activeClanId) return store; + const remainingClans = store.clans.filter(c => c.id !== store.activeClanId); + return { + ...store, + clans: remainingClans, + activeClanId: remainingClans.length > 0 ? remainingClans[0].id : null + }; + + case "UPDATE_PROFILE": + const updatedProfile = { + ...store.profile, + ...action.payload + }; + localStorage.setItem("profile", JSON.stringify(updatedProfile)); + return { ...store, profile: updatedProfile }; + + case "UPDATE_PERSONAL_BOTE": + return { ...store, personalBote: parseFloat(action.payload.newBote) }; + + case "ADD_PERSONAL_EXPENSE": + const newPExpense = { + id: new Date().getTime(), + concept: action.payload.concept, + amount: parseFloat(action.payload.amount), + date: new Date().toISOString().split("T")[0] + }; return { ...store, - message: action.payload + personalBote: store.personalBote - newPExpense.amount, + personalExpenses: [newPExpense, ...store.personalExpenses] }; - - case 'add_task': - const { id, color } = action.payload + case "ADD_TO_BOTE": + if (!store.activeClanId) return store; + const currentAmount = store.commonBote[store.activeClanId] || 0; + return { + ...store, + commonBote: { + ...store.commonBote, + [store.activeClanId]: currentAmount + parseFloat(action.payload.amount) + } + }; + case "ADD_EXPENSE": + if (!store.activeClanId) return store; + const newCExpense = { + id: new Date().getTime(), + clanId: store.activeClanId, + concept: action.payload.concept, + amount: parseFloat(action.payload.amount), + paidBy: store.profile?.name, + date: new Date().toISOString().split("T")[0] + }; + const currentBote = store.commonBote[store.activeClanId] || 0; return { ...store, - todos: store.todos.map((todo) => (todo.id === id ? { ...todo, background: color } : todo)) + expenses: [newCExpense, ...store.expenses], + commonBote: { + ...store.commonBote, + [store.activeClanId]: currentBote - newCExpense.amount + } }; + + case "SEND_MESSAGE": + const newMsg = { + id: new Date().getTime(), + clanId: store.activeClanId, + userId: store.user?.id, + userName: store.profile?.name, + text: action.payload.text, + time: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), + isMe: true + }; + return { ...store, chatMessages: [...store.chatMessages, newMsg] }; + default: - throw Error('Unknown action.'); - } -} + return store; + } +} \ No newline at end of file diff --git a/src/front/styles/Form.css b/src/front/styles/Form.css new file mode 100644 index 0000000000..86765d3026 --- /dev/null +++ b/src/front/styles/Form.css @@ -0,0 +1,139 @@ +.Form { + width: 100%; + max-width: 600px; + min-height: 60vh; + background: white; + border-radius: 16px; + padding: 60px 40px; + box-shadow: 0 10px 32px rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; + justify-content: center; +} + +.Form-login { + width: 100%; + max-width: 600px; + min-height: 60vh; + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); + border-radius: 16px; + padding: 60px 40px; + box-shadow: 0 10px 32px rgba(0, 0, 0, 0.50); + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; +} +.Form-config { + max-width: 800px; + width: 100%; + background: #ffffff; + padding: 2rem; + border-radius: 16px; + box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.12); + animation: fadeIn 0.3s ease-in-out; +} + +.Form-config h1 { + font-size: 1.8rem; + font-weight: 700; + color: #333; +} + +.Form-config .form-label { + font-weight: 600; + color: #444; +} + +.Form-config .form-control { + border-radius: 8px; + padding: 0.75rem; + border: 1px solid #bbb; + transition: border-color 0.25s, box-shadow 0.25s; +} + +.Form-config .form-control:focus { + border-color: #6e8efb; + box-shadow: 0 0 0 3px rgba(110, 142, 251, 0.25); +} + +.Form-config .button { + background: #6e8efb; + border-radius: 12px; + padding: 0.8rem; + color: white; + font-size: 1.1rem; + font-weight: 600; + border: none; + transition: 0.25s; +} + +.Form-config .button:hover { + background: #5676e8; +} + +/* Cancel button */ +.Form-config .btn-danger { + background: #d9534f !important; + border-radius: 12px; + font-weight: 600; +} + +/* Fade animation */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + + +.button { + background-color: #6366f1; + color: white; + border: none; + border-radius: 8px; + padding: 10px 20px; + font-size: 16px; + cursor: pointer; + transition: transform 0.2s ease, background-color 0.3s ease, color 0.3s ease; +} + +.button:hover { + transform: scale(1.05); + background-color: #4f46e5; +} + +h1, +h2, +h3, +p { + transition: color 0.5s ease; +} + +.card { + background: rgba(255, 255, 255, 0.9); + color: #000; + padding: 20px; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + min-height: 150px; + display: flex; + flex-direction: column; + justify-content: center; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2); +} + +.section { + transition: background 0.5s ease, color 0.5s ease; +} diff --git a/src/front/styles/Landing.css b/src/front/styles/Landing.css new file mode 100644 index 0000000000..e1280c1969 --- /dev/null +++ b/src/front/styles/Landing.css @@ -0,0 +1,93 @@ +.landing-container { + padding: 40px 20px; + margin: 0 auto; + min-height: 100vh; + color: white; +} + +.landing-header { + text-align: center; + margin-bottom: 40px; +} + +.landing-header h1 { + font-size: 48px; + font-weight: 700; +} + +.landing-header p { + font-size: 18px; + margin-top: 12px; +} + +.landing-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; +} + +.landing-card { + background: rgba(255, 255, 255, 0.9); + color: #000; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 1); + overflow: hidden; + display: flex; + flex-direction: column; + padding: 0; +} + +.landing-card-img-container { + width: 100%; + height: 180px; + overflow: hidden; +} + +.landing-card-img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.landing-card h3 { + font-size: 22px; + font-weight: 600; + margin: 15px 20px 8px 20px; +} + +.landing-card p { + color: #333; + font-size: 16px; + margin: 0 20px 20px 20px; +} + +.landing-why { + margin-top: 60px; + text-align: center; +} + +.landing-why h2 { + font-size: 32px; + font-weight: 700; +} + +.landing-why p { + font-size: 18px; + max-width: 800px; + margin: 20px auto; +} + +.landing-cta { + margin-top: 60px; + text-align: center; +} + +.landing-cta button { + padding: 14px 32px; + font-size: 18px; + background: #6366f1; + color: white; + border: none; + border-radius: 8px; + cursor: pointer; +} diff --git a/src/front/styles/ModalOverride.css b/src/front/styles/ModalOverride.css new file mode 100644 index 0000000000..ef055fd264 --- /dev/null +++ b/src/front/styles/ModalOverride.css @@ -0,0 +1,18 @@ +.modal[tabindex="-1"] { + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100vw !important; + height: 100vh !important; + z-index: 2147483647 !important; + background: rgba(0, 0, 0, 0.5) !important; + display: block !important; + overflow-y: auto !important; + pointer-events: auto !important; +} + +.modal[tabindex="-1"], +.modal[tabindex="-1"] * { + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; +} diff --git a/src/front/styles/ProfileGroups.css b/src/front/styles/ProfileGroups.css new file mode 100644 index 0000000000..3f09058895 --- /dev/null +++ b/src/front/styles/ProfileGroups.css @@ -0,0 +1,710 @@ +body { + min-height: 100vh; + margin: 0; + + + background-repeat: no-repeat; + background-attachment: fixed; + color: white; + font-family: 'Arial', sans-serif; + +} +/* Contenedor principal para las páginas */ +.page-container { + padding: 2rem; + color: white; + font-family: Arial, sans-serif; + min-height: calc(100vh - 56px); +} + +/*ESTILO "GLASS" BLANCO OPaco */ +.main-box { + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border-radius: 16px; + padding: 25px; + box-shadow: 0 10px 32px rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.3); + color: #333333; +} +.main-box h2 { + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + padding-bottom: 10px; + font-weight: bold; + color: #333333; +} + +.detail-box { + background: rgba(255, 255, 255, 0.6); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 10px; + padding: 20px; + height: 100%; + color: #333333; +} +.detail-box h4 { + color: #4f46e5; + font-weight: bold; +} + +/* ESTILOS DE TABLA (Página Grupos)*/ +.clans-table { + background: rgba(255, 255, 255, 0.6); + border-radius: 8px; + overflow: hidden; + color: #333333; + border: 1px solid rgba(255, 255, 255, 0.2); +} +.clans-table th, .clans-table td { + background-color: transparent !important; + color: #333333 !important; + border-color: rgba(0, 0, 0, 0.1) !important; + vertical-align: middle; +} +.clans-table th { + background-color: rgba(255, 255, 255, 0.4) !important; +} +.clans-table tr.active-clan { + background-color: rgba(99, 102, 241, 0.2) !important; + border-left: 4px solid #6366F1; +} + +/* ESTILOS DE TAREAS (Página Grupos) */ +.task-list-item { + background-color: rgba(255, 255, 255, 0.5); + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 10px 15px; + border-radius: 8px; + margin-bottom: 10px; + display: flex; + justify-content: space-between; + align-items: center; + color: #333333; +} +.task-list-item .form-check-input { + margin-right: 10px; + border: 1px solid #888; + background-color: rgba(255, 255, 255, 0.3); +} +.task-list-item .form-check-input:checked { + background-color: #6366F1; + border-color: #6366F1; +} +.task-list-item.completed { + text-decoration: line-through; + opacity: 0.7; + color: #555; +} +.task-list-item .btn-delete-task { + color: #D9534F; + padding: 0 5px; +} +.task-list-item .btn-delete-task:hover { + color: #C9302C; +} + +/* LISTA DE AMIGOS (Página Perfil) */ +.friend-list { + background: rgba(255, 255, 255, 0.6); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 8px; + padding: 15px; + margin-top: 20px; + color: #333; +} +.friend-list-item { + display: flex; + align-items: center; + margin-bottom: 15px; + padding-bottom: 15px; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); +} +.friend-list-item:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; +} +.friend-list-item img { + width: 40px; + height: 40px; + border-radius: 50%; + margin-right: 15px; + border: 2px solid #6366F1; +} +.friend-list-item .friend-info { + flex-grow: 1; +} +.friend-list-item .friend-info strong { + display: block; + color: #333333; +} +.friend-list-item .friend-info .status { + font-size: 0.8rem; + color: #555; +} +.friend-list-item .friend-info .status.online { + color: #28a745; +} +.friend-list-item .friend-info .status.online::before { + content: '●'; + margin-right: 5px; +} +.friend-list-item .friend-info .status.offline::before { + content: '●'; + margin-right: 5px; +} + +/* BOTONES */ +.btn-custom-blue, .btn-custom-purple, .btn-invite-user { + background-color: #6366F1; + border-color: #6366F1; + color: white; + font-weight: bold; + border-radius: 8px; + transition: transform 0.2s ease, background-color 0.3s ease; +} +.btn-custom-blue:hover, .btn-custom-purple:hover, .btn-invite-user:hover { + background-color: #4f46e5; + border-color: #4f46e5; + color: white; + transform: scale(1.05); +} + +/* MODALES (Estilo "Glass" blanco opaco) */ +.modal-content-dark { + background: rgba(255, 255, 255, 0.95); + color: #333333; /* Texto oscuro */ + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 16px; + box-shadow: 0 10px 32px rgba(0, 0, 0, 0.3); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} +.modal-content-dark .modal-header, +.modal-content-dark .modal-footer { + border-color: rgba(0, 0, 0, 0.1); +} +.modal-content-dark .btn-close { + filter: invert(0); +} +.modal-content-dark .form-control { + background-color: #F0F0F0; + color: #333333; + border: 1px solid #CCC; +} +.modal-content-dark .form-control:focus { + background-color: #FFF; + color: #333333; + border-color: #6366F1; + box-shadow: 0 0 0 0.25rem rgba(99, 102, 241, 0.25); +} +.modal-content-dark .form-label { + color: #333333; +} + +/* RESPONSIVE*/ +@media (max-width: 991px) { + .profile-grid-item { + margin-bottom: 1.5rem; + } +} +@media (max-width: 768px) { + .page-container { + padding: 1rem; + } + .main-box h2 { + font-size: 1.75rem; + } + .responsive-button-group { + flex-direction: column; + gap: 15px; + } + .responsive-button-group .btn { + width: 100%; + } + .profile-column-left { + text-align: center; + margin-bottom: 2rem; + } + .clans-table-wrapper { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + padding-bottom: 10px; + } + .clans-table-wrapper .table { + min-width: 600px; + } +} + +/* ESTILOS DEL DASHBOARD */ +.dashboard-container { + display: flex; + min-height: 100vh; + background: transparent; + color: white; +} +.dashboard-sidebar { + width: 260px; + background-color: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + box-shadow: 0 10px 32px rgba(0, 0, 0, 0.3); + border-right: 1px solid rgba(255, 255, 255, 0.3); + padding: 20px; + display: flex; + position: sticky; + top: 0; + height: 100vh; + flex-direction: column; + min-height: 100vh; /* asegura que llegue hasta abajo */ + z-index: 1000; + transition: width 0.3s ease; +} + +/* Header */ +.sidebar-header { + text-align: center; + margin-bottom: 30px; +} +.sidebar-header .logo { + color: #4f46e5; + font-size: 2rem; + font-weight: bold; + letter-spacing: 1px; + text-decoration: none; +} + +/* User Profile */ +.user-profile-summary { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + margin-bottom: 30px; + padding: 15px; + background-color: rgba(0, 0, 0, 0.05); + border-radius: 10px; +} +.user-avatar { + width: 80px; + height: 80px; + border-radius: 50%; + object-fit: cover; + border: 3px solid #6366f1; + margin-bottom: 10px; + background: #6366f1; + display: flex; + align-items: center; + justify-content: center; + color: white; +} +.user-avatar img { + width: 100%; + height: 100%; + border-radius: 50%; +} +.user-avatar span { + font-size: 3rem; +} +.username { + display: block; + font-size: 1.1rem; + font-weight: bold; + color: #333333; +} +.user-email { + font-size: 0.85rem; + color: #555555; +} + +/* Navigation */ +.sidebar-nav ul { + list-style: none; + padding: 0; + margin: 0; +} +.sidebar-nav li { + margin-bottom: 10px; +} +.sidebar-nav a { + display: flex; + align-items: center; + padding: 12px 15px; + color: #555555; + text-decoration: none; + border-radius: 8px; + transition: background-color 0.3s ease, color 0.3s ease; +} +.sidebar-nav a:hover { + background-color: rgba(99, 102, 241, 0.1); + color: #4f46e5; +} +.sidebar-nav a.active { + background-color: #6366F1; + color: #FFFFFF; + font-weight: bold; + box-shadow: 0 4px 10px rgba(99, 102, 241, 0.3); +} +.sidebar-nav a i { + font-size: 1rem; + width: 25px; + margin-right: 10px; +} + +/* Responsive: colapsar en pantallas pequeñas */ +@media (max-width: 768px) { + .dashboard-sidebar { + width: 60px; /* ancho colapsado */ + padding: 10px 0; + } + + .sidebar-header, + .user-profile-summary, + .sidebar-nav a span { + display: none; /* oculta texto y avatar */ + } + + .sidebar-nav a { + justify-content: center; + padding: 12px 0; + border-radius: 50%; /* icono redondo */ + margin: 5px auto; + width: 40px; + height: 40px; + } + + .sidebar-nav a i { + margin: 0; + font-size: 1.2rem; + } +} + +/* Contenido Principal del Dashboard */ +.dashboard-main-content { + flex-grow: 1; + display: flex; + flex-direction: column; + overflow-y: auto; +} + +/* (Navbar) */ +.dashboard-navbar { + background-color: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + padding: 15px 30px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(255, 255, 255, 0.3); + position: sticky; + top: 0; + z-index: 999; +} +.search-bar { + display: flex; + align-items: center; + background-color: rgba(0, 0, 0, 0.05); + border-radius: 20px; + padding: 8px 15px; + border: 1px solid rgba(0, 0, 0, 0.1); +} +.search-bar i { + color: #888888; + margin-right: 10px; +} +.search-bar input { + background: transparent; + border: none; + color: #333333; + width: 250px; + outline: none; +} +.search-bar input::placeholder { + color: #888888; +} +.navbar-icons i { + font-size: 1.2rem; + color: #555555; + margin-left: 20px; + cursor: pointer; + transition: color 0.3s ease; +} +.navbar-icons i:hover { + color: #6366F1; +} +.dashboard-content-area { + padding: 30px; + flex-grow: 1; +} +.welcome-section { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; +} +.welcome-section h2 { + color: #FFFFFF; + font-size: 1.8rem; + font-weight: bold; + text-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + + +/* Tarjetas del Dashboard */ +.dashboard-card { + background: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 16px; + padding: 25px; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: 0 10px 32px rgba(0, 0, 0, 0.3); + height: 100%; + display: flex; + flex-direction: column; + color: #333333; +} +.dashboard-card h3 { + color: #333333; + font-size: 1.3rem; + font-weight: bold; + margin-bottom: 15px; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + padding-bottom: 10px; +} +.card-header-actions { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + padding-bottom: 10px; +} +.card-header-actions h3 { + margin-bottom: 0; + border-bottom: none; + padding-bottom: 0; +} +.btn-icon-only { + background-color: transparent; + border: 1px solid #888; + color: #333; + border-radius: 50%; + width: 35px; + height: 35px; + display: inline-flex; + align-items: center; + justify-content: center; + transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; +} +.btn-icon-only:hover { + background-color: #6366F1; + color: white; + border-color: #6366F1; +} + +/* Listas de Tareas en Dashboard */ +.task-list { + list-style: none; + padding: 0; + margin: 0; +} +.task-list li { + padding: 10px 0; + border-bottom: 1px dashed rgba(0, 0, 0, 0.1); + color: #333333; + font-size: 0.95rem; + display: flex; + align-items: center; + justify-content: space-between; +} +.task-list li:last-child { + border-bottom: none; +} +.task-list li i { + margin-left: 10px; + font-size: 1.1rem; +} +.task-list .text-success { color: #28a745 !important; } +.task-list .text-danger { color: #D9534F !important; } +.text-muted { + color: #555 !important; +} +.text-info { + color: #4f46e5 !important; +} + +/* ESTILOS MODAL INVITAR */ +.invite-modal-content { + background: rgba(255, 255, 255, 0.95); + color: #333333; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 10px; + padding: 0; + box-shadow: 0 10px 32px rgba(0, 0, 0, 0.3); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} +.invite-modal-header { + background-color: rgba(255, 255, 255, 0.5); + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + padding: 15px 20px; + display: flex; + justify-content: space-between; + align-items: center; +} +.invite-modal-header .modal-title { + color: #333333; + font-size: 1.1rem; + font-weight: bold; +} +.btn-go-back { + background: none; + border: 1px solid #555; + color: #333; + font-size: 0.9rem; + padding: 5px 10px; + border-radius: 5px; + transition: background-color 0.2s; +} +.btn-go-back:hover { + background-color: #333; + color: #FFF; +} +.invite-modal-body { + padding: 20px; +} +.invite-label { + color: #333333; + font-weight: bold; + margin-bottom: 8px; + display: block; +} +.invite-input { + background-color: #F0F0F0; + border: 1px solid #CCC; + color: #333; + border-radius: 5px; + padding: 10px 12px; +} +.invite-input:focus { + background-color: #FFF; + border-color: #6366F1; + box-shadow: 0 0 0 0.2rem rgba(99, 102, 241, 0.25); + color: #333; +} +/* Botones de acción del modal */ +.btn-send-invite, .btn-copy-link { + background-color: #6366F1; + border-color: #6366F1; + color: white; + font-weight: 600; + padding: 10px 15px; + border-radius: 8px; + transition: transform 0.2s ease, background-color 0.3s ease; +} +.btn-send-invite:hover, .btn-copy-link:hover { + background-color: #4f46e5; + border-color: #4f46e5; + transform: scale(1.05); +} +/* Lista de miembros del modal */ +.member-list { + background-color: rgba(0, 0, 0, 0.05); /* Gris muy sutil */ + border-radius: 5px; + padding: 15px; +} +.member-item { + display: flex; + align-items: center; + margin-bottom: 15px; + padding-bottom: 15px; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); +} +.member-item:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; +} +.member-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; + margin-right: 15px; + border: 2px solid #6366F1; +} +.member-info { + flex-grow: 1; + color: #333; + font-size: 0.9rem; +} +.member-info strong { + display: block; + font-weight: bold; +} +.member-info span { + color:#555; +} +.member-role { + font-size: 0.9rem; + color: #555; +} + +/* ESTILOS PÁGINA FINANZAS (Tema "Glass" claro) */ +.list-group-item.expense-item, +.list-group-item.balance-item { + background-color: rgba(255, 255, 255, 0.5); + border: 1px solid rgba(255, 255, 255, 0.2); + color: #333333; + margin-bottom: 10px; + border-radius: 8px; + display: flex; + justify-content: space-between; + align-items: center; +} +.expense-details { + display: flex; + flex-direction: column; +} +.expense-concept { + font-weight: bold; + font-size: 1rem; +} +.expense-paidby { + font-size: 0.8rem; + color: #555; +} +.expense-amount { + font-weight: bold; + color: #D9534F; +} +.balance-item span:first-child { + font-weight: bold; +} +.balance-positive { + font-weight: bold; + color: #28a745; +} +.balance-negative { + font-weight: bold; + color: #D9534F; +} +calendar-month { + --color-accent: #007bff; + --color-text-on-accent: #ffffff; +} + +calendar-month::part(range-inner) { + background-color: #0056b3; + border-radius: 0; +} + +calendar-month::part(button) { + border-radius: 3px; +} +.footer{ + background-color: white; +} \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index a93495e81e..2eb5e0998c 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,14 +1,12 @@ -import { - defineConfig -} from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; export default defineConfig({ - plugins: [react()], - server: { - port: 3000 - }, - build: { - outDir: 'dist' - } -}) \ No newline at end of file + plugins: [react()], + server: { + port: 3000, + }, + build: { + outDir: "dist", + }, +}); \ No newline at end of file