diff --git a/Pipfile b/Pipfile index 4d377014ae..1fa0efda95 100644 --- a/Pipfile +++ b/Pipfile @@ -20,6 +20,8 @@ typing-extensions = "*" flask-jwt-extended = "==4.6.0" wtforms = "==3.1.2" sqlalchemy = "*" +flask-mail = "*" +flask-bcrypt = "*" [requires] python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index d9e474e972..9fcfdf8cb3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ffbfb32d0afa5e4bcaba5c2d08c81381a97abd90f22284d2b76647365df5dc50" + "sha256": "34e9dc18f8e20c8abdc412b8fefb2789ba72dd185f691d08d34dcb62e1ab0965" }, "pipfile-spec": 6, "requires": { @@ -24,6 +24,75 @@ "markers": "python_version >= '3.10'", "version": "==1.17.1" }, + "bcrypt": { + "hashes": [ + "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4", + "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", + "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", + "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", + "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", + "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", + "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", + "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", + "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", + "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", + "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", + "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", + "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", + "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", + "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", + "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", + "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", + "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", + "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", + "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", + "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", + "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", + "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", + "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", + "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", + "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", + "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", + "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", + "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534", + "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", + "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", + "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", + "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", + "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", + "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", + "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", + "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", + "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", + "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", + "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", + "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", + "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", + "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", + "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", + "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", + "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", + "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", + "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", + "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911", + "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", + "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", + "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", + "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", + "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", + "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", + "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", + "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", + "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", + "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", + "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", + "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", + "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", + "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b" + ], + "markers": "python_version >= '3.8'", + "version": "==5.0.0" + }, "blinker": { "hashes": [ "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", @@ -74,6 +143,14 @@ "markers": "python_version >= '3.10'", "version": "==2.0.0" }, + "flask-bcrypt": { + "hashes": [ + "sha256:062fd991dc9118d05ac0583675507b9fe4670e44416c97e0e6819d03d01f808a", + "sha256:f07b66b811417ea64eb188ae6455b0b708a793d966e1a80ceec4a23bc42a4369" + ], + "index": "pypi", + "version": "==1.0.1" + }, "flask-cors": { "hashes": [ "sha256:c7b2cbfb1a31aa0d2e5341eea03a6805349f7a61647daee1a15c46bbe981494c", @@ -92,6 +169,15 @@ "markers": "python_version >= '3.7' and python_version < '4'", "version": "==4.6.0" }, + "flask-mail": { + "hashes": [ + "sha256:44083e7b02bbcce792209c06252f8569dd5a325a7aaa76afe7330422bd97881d", + "sha256:a451e490931bb3441d9b11ebab6812a16bfa81855792ae1bf9c1e1e22c4e51e7" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.10.0" + }, "flask-migrate": { "hashes": [ "sha256:1a336b06eb2c3ace005f5f2ded8641d534c18798d64061f6ff11f79e1434126d", @@ -121,6 +207,8 @@ "greenlet": { "hashes": [ "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", + "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", + "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", @@ -133,18 +221,23 @@ "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d", + "sha256:28a3c6b7cd72a96f61b0e4b2a36f681025b60ae4779cc73c1535eb5f29560b10", + "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", + "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", + "sha256:52206cd642670b0b320a1fd1cbfd95bca0e043179c1d8a045f2c6109dfe973be", "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b", "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4", + "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98", @@ -157,6 +250,7 @@ "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", + "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8", "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c", "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594", @@ -166,14 +260,18 @@ "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df", + "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", + "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb", "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", + "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", - "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968" + "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", + "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7" ], "markers": "python_version >= '3.9'", "version": "==3.2.4" diff --git a/index.html b/index.html index 27a99f796e..3531854c7e 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - Hello Rigo + MeetFit
diff --git a/package-lock.json b/package-lock.json index 8d43d98ab7..5702bda808 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,14 @@ "version": "1.0.1", "license": "ISC", "dependencies": { + "@react-google-maps/api": "^2.20.7", + "bootstrap": "^5.3.8", + "bootstrap-icons": "^1.13.1", "prop-types": "^15.8.1", "react": "^18.2.0", + "react-bootstrap": "^2.10.10", "react-dom": "^18.2.0", + "react-icons": "^5.5.0", "react-router-dom": "^6.18.0" }, "devDependencies": { @@ -73,6 +78,7 @@ "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -290,6 +296,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", @@ -804,6 +819,22 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@googlemaps/js-api-loader": { + "version": "1.16.8", + "resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.8.tgz", + "integrity": "sha512-CROqqwfKotdO6EBjZO/gQGVTbeDps5V7Mt9+8+5Q+jTg5CRMi3Ii/L9PmV3USROrt2uWxtGzJHORmByxyo9pSQ==", + "license": "Apache-2.0" + }, + "node_modules/@googlemaps/markerclusterer": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@googlemaps/markerclusterer/-/markerclusterer-2.5.3.tgz", + "integrity": "sha512-x7lX0R5yYOoiNectr10wLgCBasNcXFHiADIBdmn7jQllF2B5ENQw5XtZK+hIw4xnV0Df0xhN4LN98XqA5jaiOw==", + "license": "Apache-2.0", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "supercluster": "^8.0.1" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -884,7 +915,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -943,6 +973,62 @@ "node": ">= 8" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-google-maps/api": { + "version": "2.20.7", + "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-2.20.7.tgz", + "integrity": "sha512-ys7uri3V6gjhYZUI43srHzSKDC6/jiKTwHNlwXFTvjeaJE3M3OaYBt9FZKvJs8qnOhL6i6nD1BKJoi1KrnkCkg==", + "license": "MIT", + "dependencies": { + "@googlemaps/js-api-loader": "1.16.8", + "@googlemaps/markerclusterer": "2.5.3", + "@react-google-maps/infobox": "2.20.0", + "@react-google-maps/marker-clusterer": "2.20.0", + "@types/google.maps": "3.58.1", + "invariant": "2.2.4" + }, + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19", + "react-dom": "^16.8 || ^17 || ^18 || ^19" + } + }, + "node_modules/@react-google-maps/infobox": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-2.20.0.tgz", + "integrity": "sha512-03PJHjohhaVLkX6+NHhlr8CIlvUxWaXhryqDjyaZ8iIqqix/nV8GFdz9O3m5OsjtxtNho09F/15j14yV0nuyLQ==", + "license": "MIT" + }, + "node_modules/@react-google-maps/marker-clusterer": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-2.20.0.tgz", + "integrity": "sha512-tieX9Va5w1yP88vMgfH1pHTacDQ9TgDTjox3tLlisKDXRQWdjw+QeVVghhf5XqqIxXHgPdcGwBvKY6UP+SIvLw==", + "license": "MIT" + }, "node_modules/@remix-run/router": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.22.0.tgz", @@ -952,6 +1038,69 @@ "node": ">=14.0.0" } }, + "node_modules/@restart/hooks": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", + "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.9.4.tgz", + "integrity": "sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@popperjs/core": "^2.11.8", + "@react-aria/ssr": "^3.5.0", + "@restart/hooks": "^0.5.0", + "@types/warning": "^3.0.3", + "dequal": "^2.0.3", + "dom-helpers": "^5.2.0", + "uncontrollable": "^8.0.4", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/@restart/ui/node_modules/@restart/hooks": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.5.1.tgz", + "integrity": "sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui/node_modules/uncontrollable": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz", + "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.14.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -997,27 +1146,24 @@ "@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/google.maps": { + "version": "3.58.1", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", + "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==", + "license": "MIT" }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", - "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1033,6 +1179,21 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/warning": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", + "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==", + "license": "MIT" + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -1066,6 +1227,7 @@ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1264,6 +1426,41 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bootstrap": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", + "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/bootstrap-icons": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz", + "integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1294,6 +1491,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -1312,8 +1510,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", @@ -1395,6 +1592,12 @@ ], "license": "CC-BY-4.0" }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1420,7 +1623,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/data-view-buffer": { @@ -1537,6 +1739,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1549,6 +1760,16 @@ "node": ">=6.0.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1797,6 +2018,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", @@ -2215,8 +2437,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", @@ -2630,6 +2851,15 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -3099,6 +3329,12 @@ "node": ">=4.0" } }, + "node_modules/kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "license": "ISC" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3452,6 +3688,19 @@ "react-is": "^16.13.1" } }, + "node_modules/prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "license": "MIT", + "dependencies": { + "react-is": "^16.3.2", + "warning": "^4.0.0" + }, + "peerDependencies": { + "react": ">=0.14.0" + } + }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -3486,6 +3735,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" }, @@ -3493,11 +3743,43 @@ "node": ">=0.10.0" } }, + "node_modules/react-bootstrap": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.10.tgz", + "integrity": "sha512-gMckKUqn8aK/vCnfwoBpBVFUGT9SVQxwsYrp9yDHt0arXMamxALerliKBxr1TPbntirK/HGrUAHYbAeQTa9GHQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@restart/hooks": "^0.4.9", + "@restart/ui": "^1.9.4", + "@types/prop-types": "^15.7.12", + "@types/react-transition-group": "^4.4.6", + "classnames": "^2.3.2", + "dom-helpers": "^5.2.1", + "invariant": "^2.2.4", + "prop-types": "^15.8.1", + "prop-types-extra": "^1.1.0", + "react-transition-group": "^4.4.5", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "@types/react": ">=16.14.8", + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-dom": { "version": "18.3.1", "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" @@ -3506,11 +3788,26 @@ "react": "^18.3.1" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -3553,6 +3850,22 @@ "react-dom": ">=16.8" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -3921,7 +4234,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 +4245,6 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -4061,6 +4372,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -4081,7 +4401,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -4100,8 +4419,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 +4427,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", @@ -4232,6 +4556,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", @@ -4278,6 +4617,7 @@ "integrity": "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", @@ -4328,6 +4668,15 @@ } } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4500,6 +4849,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", @@ -4651,6 +5001,11 @@ "@babel/helper-plugin-utils": "^7.25.9" } }, + "@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==" + }, "@babel/template": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", @@ -4898,6 +5253,20 @@ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true }, + "@googlemaps/js-api-loader": { + "version": "1.16.8", + "resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.8.tgz", + "integrity": "sha512-CROqqwfKotdO6EBjZO/gQGVTbeDps5V7Mt9+8+5Q+jTg5CRMi3Ii/L9PmV3USROrt2uWxtGzJHORmByxyo9pSQ==" + }, + "@googlemaps/markerclusterer": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@googlemaps/markerclusterer/-/markerclusterer-2.5.3.tgz", + "integrity": "sha512-x7lX0R5yYOoiNectr10wLgCBasNcXFHiADIBdmn7jQllF2B5ENQw5XtZK+hIw4xnV0Df0xhN4LN98XqA5jaiOw==", + "requires": { + "fast-deep-equal": "^3.1.3", + "supercluster": "^8.0.1" + } + }, "@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -4950,7 +5319,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" @@ -4998,11 +5366,96 @@ "fastq": "^1.6.0" } }, + "@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "peer": true + }, + "@react-aria/ssr": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "requires": { + "@swc/helpers": "^0.5.0" + } + }, + "@react-google-maps/api": { + "version": "2.20.7", + "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-2.20.7.tgz", + "integrity": "sha512-ys7uri3V6gjhYZUI43srHzSKDC6/jiKTwHNlwXFTvjeaJE3M3OaYBt9FZKvJs8qnOhL6i6nD1BKJoi1KrnkCkg==", + "requires": { + "@googlemaps/js-api-loader": "1.16.8", + "@googlemaps/markerclusterer": "2.5.3", + "@react-google-maps/infobox": "2.20.0", + "@react-google-maps/marker-clusterer": "2.20.0", + "@types/google.maps": "3.58.1", + "invariant": "2.2.4" + } + }, + "@react-google-maps/infobox": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-2.20.0.tgz", + "integrity": "sha512-03PJHjohhaVLkX6+NHhlr8CIlvUxWaXhryqDjyaZ8iIqqix/nV8GFdz9O3m5OsjtxtNho09F/15j14yV0nuyLQ==" + }, + "@react-google-maps/marker-clusterer": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-2.20.0.tgz", + "integrity": "sha512-tieX9Va5w1yP88vMgfH1pHTacDQ9TgDTjox3tLlisKDXRQWdjw+QeVVghhf5XqqIxXHgPdcGwBvKY6UP+SIvLw==" + }, "@remix-run/router": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.22.0.tgz", "integrity": "sha512-MBOl8MeOzpK0HQQQshKB7pABXbmyHizdTpqnrIseTbsv0nAepwC2ENZa1aaBExNQcpLoXmWthhak8SABLzvGPw==" }, + "@restart/hooks": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", + "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", + "requires": { + "dequal": "^2.0.3" + } + }, + "@restart/ui": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.9.4.tgz", + "integrity": "sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA==", + "requires": { + "@babel/runtime": "^7.26.0", + "@popperjs/core": "^2.11.8", + "@react-aria/ssr": "^3.5.0", + "@restart/hooks": "^0.5.0", + "@types/warning": "^3.0.3", + "dequal": "^2.0.3", + "dom-helpers": "^5.2.0", + "uncontrollable": "^8.0.4", + "warning": "^4.0.3" + }, + "dependencies": { + "@restart/hooks": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.5.1.tgz", + "integrity": "sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q==", + "requires": { + "dequal": "^2.0.3" + } + }, + "uncontrollable": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz", + "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==", + "requires": {} + } + } + }, + "@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "requires": { + "tslib": "^2.8.0" + } + }, "@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -5044,25 +5497,21 @@ "@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/google.maps": { + "version": "3.58.1", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", + "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==" }, "@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", - "dev": true + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==" }, "@types/react": { "version": "18.3.18", "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" @@ -5075,6 +5524,17 @@ "dev": true, "requires": {} }, + "@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "requires": {} + }, + "@types/warning": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", + "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==" + }, "@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -5098,7 +5558,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", @@ -5230,6 +5691,17 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "bootstrap": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", + "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", + "requires": {} + }, + "bootstrap-icons": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz", + "integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -5245,6 +5717,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 +5730,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", @@ -5304,6 +5776,11 @@ "integrity": "sha512-GwNPlWJin8E+d7Gxq96jxM6w0w+VFeyyXRsjU58emtkYqnbwHqXm5uT2uCmO0RQE9htWknOP4xtBlLmM/gWxvQ==", "dev": true }, + "classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5324,8 +5801,7 @@ "csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "data-view-buffer": { "version": "1.0.2", @@ -5397,6 +5873,11 @@ "object-keys": "^1.1.1" } }, + "dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -5406,6 +5887,15 @@ "esutils": "^2.0.2" } }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5600,6 +6090,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", @@ -5883,8 +6374,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-json-stable-stringify": { "version": "2.1.0", @@ -6164,6 +6654,14 @@ "side-channel": "^1.1.0" } }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -6451,6 +6949,11 @@ "object.assign": "^4.1.2" } }, + "kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==" + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6686,6 +7189,15 @@ "react-is": "^16.13.1" } }, + "prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "requires": { + "react-is": "^16.3.2", + "warning": "^4.0.0" + } + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -6702,24 +7214,57 @@ "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" } }, + "react-bootstrap": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.10.tgz", + "integrity": "sha512-gMckKUqn8aK/vCnfwoBpBVFUGT9SVQxwsYrp9yDHt0arXMamxALerliKBxr1TPbntirK/HGrUAHYbAeQTa9GHQ==", + "requires": { + "@babel/runtime": "^7.24.7", + "@restart/hooks": "^0.4.9", + "@restart/ui": "^1.9.4", + "@types/prop-types": "^15.7.12", + "@types/react-transition-group": "^4.4.6", + "classnames": "^2.3.2", + "dom-helpers": "^5.2.1", + "invariant": "^2.2.4", + "prop-types": "^15.8.1", + "prop-types-extra": "^1.1.0", + "react-transition-group": "^4.4.5", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + } + }, "react-dom": { "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" } }, + "react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "requires": {} + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -6743,6 +7288,17 @@ "react-router": "6.29.0" } }, + "react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, "reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6988,7 +7544,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 +7554,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 } } }, @@ -7088,6 +7642,14 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "requires": { + "kdbush": "^4.0.2" + } + }, "supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -7100,7 +7662,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 +7674,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 +7684,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", @@ -7204,6 +7769,17 @@ "which-boxed-primitive": "^1.1.1" } }, + "uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "requires": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + } + }, "update-browserslist-db": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", @@ -7228,6 +7804,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", @@ -7235,6 +7812,14 @@ "rollup": "^3.27.1" } }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 0caab10749..359839d920 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,14 @@ ] }, "dependencies": { + "@react-google-maps/api": "^2.20.7", + "bootstrap": "^5.3.8", + "bootstrap-icons": "^1.13.1", "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-bootstrap": "^2.10.10", + "react-dom": "^18.2.0", + "react-icons": "^5.5.0", + "react-router-dom": "^6.18.0" } } diff --git a/src/api/models.py b/src/api/models.py index da515f6a1a..68eac0dba0 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,19 +1,157 @@ +from datetime import datetime, timezone, timedelta +import secrets from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import String, Boolean -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import (String, Float, DateTime, Text, Date, Time, Integer, ForeignKey, Table, Boolean) +from sqlalchemy.orm import mapped_column, relationship +from werkzeug.security import generate_password_hash, check_password_hash db = SQLAlchemy() +# Tabla intermedia (relación muchos a muchos) +activity_user = Table( + "activity_user", + db.Model.metadata, + db.Column("id", Integer, primary_key=True), + db.Column("user_id", Integer, ForeignKey("user.id")), + db.Column("activity_id", Integer, ForeignKey("activity.id")), + db.Column("joined_at", DateTime, default=lambda: datetime.now(timezone.utc)) +) + +# MODELO: User 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_column(Integer, primary_key=True) + name = mapped_column(String(50), nullable=False) + email = mapped_column(String(120), unique=True, nullable=False) + password_hash = mapped_column(String(200), nullable=False) + avatar_url = mapped_column(String(200)) + biography = mapped_column(Text) + sports = mapped_column(String(200)) + level = mapped_column(String(20)) + latitude = mapped_column(Float) + longitude = mapped_column(Float) + created_at = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc)) + + # Relaciones + activities_created = relationship("Activity", back_populates="creator", lazy=True) + activities_joined = relationship("Activity", secondary=activity_user, back_populates="participants") + messages_sent = relationship("Message", back_populates="sender", lazy=True) + reset_tokens = relationship("PasswordResetToken", back_populates="user", cascade="all, delete-orphan") + + # Métodos de seguridad + def set_password(self, password): + self.password_hash = generate_password_hash(password) + def check_password(self, password): + return check_password_hash(self.password_hash, password) def serialize(self): return { "id": self.id, + "name": self.name, "email": self.email, - # do not serialize the password, its a security breach - } \ No newline at end of file + "avatar_url": self.avatar_url, + "biography": self.biography, + "sports": self.sports, + "level": self.level, + "latitude": self.latitude, + "longitude": self.longitude, + "created_at": self.created_at.isoformat(), + } + + +# MODELO: Activity +class Activity(db.Model): + __tablename__ = "activity" + + id = mapped_column(Integer, primary_key=True) + title = mapped_column(String(100), nullable=False) + sport = mapped_column(String(50), nullable=False) + description = mapped_column(Text) + latitude = mapped_column(Float, nullable=False) + longitude = mapped_column(Float, nullable=False) + date = mapped_column(Date, nullable=False) + time = mapped_column(Time, nullable=False) + created_by = mapped_column(Integer, ForeignKey("user.id")) + max_participants = mapped_column(Integer) + created_at = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc)) + + # Relaciones + creator = relationship("User", back_populates="activities_created") + participants = relationship("User", secondary=activity_user, back_populates="activities_joined") + messages = relationship("Message", back_populates="activity", lazy=True) + + def serialize(self): + return { + "id": self.id, + "title": self.title, + "sport": self.sport, + "description": self.description, + "latitude": self.latitude, + "longitude": self.longitude, + "date": str(self.date), + "time": str(self.time), + "created_by": self.created_by, + "creator_name": self.creator.name if self.creator else None, + "max_participants": self.max_participants, + "created_at": self.created_at.isoformat(), + "participants": [p.id for p in self.participants], + } + + +# MODELO: Message +class Message(db.Model): + __tablename__ = "message" + + id = mapped_column(Integer, primary_key=True) + activity_id = mapped_column(Integer, ForeignKey("activity.id")) + sender_id = mapped_column(Integer, ForeignKey("user.id")) + message = mapped_column(Text, nullable=False) + created_at = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc)) + + sender = relationship("User", back_populates="messages_sent") + activity = relationship("Activity", back_populates="messages") + + def serialize(self): + return { + "id": self.id, + "activity_id": self.activity_id, + "sender_id": self.sender_id, + "message": self.message, + "created_at": self.created_at.isoformat(), + } + +# MODELO: PasswordResetToken +class PasswordResetToken(db.Model): + __tablename__ = "password_reset_token" + + id = mapped_column(Integer, primary_key=True) + user_id = mapped_column(Integer, ForeignKey("user.id", ondelete="CASCADE"), nullable=False) + token = mapped_column(String(128), unique=True, nullable=False) + expires_at = mapped_column(DateTime(timezone=True), nullable=False) + used = mapped_column(Boolean, default=False) + created_at = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) + + user = relationship("User", back_populates="reset_tokens") + + @staticmethod + def generate_token(user_id, expiration_minutes=30): + """Genera un nuevo token de recuperación con validez temporal.""" + token = secrets.token_urlsafe(64) + expires_at = datetime.now(timezone.utc) + timedelta(minutes=expiration_minutes) + return PasswordResetToken(user_id=user_id, token=token, expires_at=expires_at) + + def is_valid(self): + """Valida si el token sigue activo y no se ha usado.""" + return not self.used and datetime.now(timezone.utc) < self.expires_at + + def serialize(self): + return { + "id": self.id, + "user_id": self.user_id, + "token": self.token, + "expires_at": self.expires_at.isoformat(), + "used": self.used, + "created_at": self.created_at.isoformat(), + } diff --git a/src/app.py b/src/app.py index 1b3340c0fa..6724370ab2 100644 --- a/src/app.py +++ b/src/app.py @@ -6,11 +6,20 @@ from flask_migrate import Migrate from flask_swagger import swagger from api.utils import APIException, generate_sitemap -from api.models import db +from api.models import db, User, Activity, Message, PasswordResetToken from api.routes import api from api.admin import setup_admin from api.commands import setup_commands +from flask_jwt_extended import create_access_token +from flask_jwt_extended import get_jwt_identity +from flask_jwt_extended import jwt_required +from flask_jwt_extended import JWTManager + +from flask_bcrypt import Bcrypt + +from flask_mail import Mail, Message + # from models import Person ENV = "development" if os.getenv("FLASK_DEBUG") == "1" else "production" @@ -19,6 +28,27 @@ app = Flask(__name__) app.url_map.strict_slashes = False +# Setup the Flask-JWT-Extended extension +app.config["JWT_SECRET_KEY"] = "ESTA_ES_NUESTRA_LLAVE" + +GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY") + +jwt = JWTManager(app) + +bcrypt = Bcrypt(app) + +app.config.update(dict( + DEBUG=False, + MAIL_SERVER='smtp.gmail.com', + MAIL_PORT=587, + MAIL_USE_TLS=True, + MAIL_USE_SSL=False, + MAIL_USERNAME='meetfitfspt119@gmail.com', + MAIL_PASSWORD=os.getenv('MAIL_PASSWORD') +)) + +mail = Mail(app) + # database condiguration db_url = os.getenv("DATABASE_URL") if db_url is not None: @@ -66,6 +96,92 @@ def serve_any_other_file(path): return response +#prueba send-mail +@app.route('/api/send-mail', methods=['GET']) +def send_mail(): + msg = Message( + subject='Prueba de correo de proyecto', + sender='meetfitfspt119@gmail.com', + recipients=['meetfitfspt119@gmail.com'], + ) + msg.html = '

Testeando envio de correo

' + mail.send(msg) + return jsonify({'msg': 'Correo enviado con exito'}), 200 + +@app.route('/api/register', methods=['POST']) +def register(): + body = request.get_json(silent=True) + + if body is None: + return jsonify({'msg': 'POST method needs a body or email/password not found.'}), 400 + if 'email' not in body: + return jsonify({'msg': 'email field is mandatory'}), 400 + if 'password' not in body: + return jsonify({'msg': 'password field is mandatory'}), 400 + if 'name' not in body: + return jsonify({'msg': 'name field is mandatory'}) + email = body.get("email") + password = body.get("password") + name = body.get("name") + + user = User.query.filter_by(email=email).first() + if user is not None: + return jsonify({'msg': 'Usuario ya registrado!'}), 400 + + pw_hash = bcrypt.generate_password_hash(password).decode('utf-8') + user = User(email=email, name=name, password_hash=pw_hash) + db.session.add(user) + db.session.commit() + + + # access_token = create_access_token(identity=str(user.id)) + return jsonify({'msg': 'User Registered successfully'}) + +@app.route('/api/login', methods=['POST']) +def login(): + + body = request.get_json(silent=True) + # validar body + if body is None: + return jsonify({'msg': 'POST method needs a body or email/password not found.'}), 400 + if 'email' not in body: + return jsonify({'msg': 'email field is mandatory'}), 400 + if 'password' not in body: + return jsonify({'msg': 'password field is mandatory'}), 400 + # buscar usuario + email = body['email'] + password = body['password'] + + user = User.query.filter_by(email=email).first() + if not user: + return jsonify({'msg':'User or password incorrect'}), 400 + # validar password + is_password = bcrypt.check_password_hash(user.password_hash, password) + + if not is_password: + return jsonify({'msg': 'User or password incorrect'}), 400 + # crear token + access_token = create_access_token(identity=str(user.id)) + + return jsonify(access_token=access_token) + + +@app.route("/api/me", methods=["GET"]) +@jwt_required() +def me(): + + + current_user = get_jwt_identity() + user = User.query.get(current_user) + print(user) + return jsonify(user.serialize()), 200 + + +#endPoint completo ACTIVITY ----> GET POST PUT DELETE + + + + # this only runs if `$ python src/main.py` is executed if __name__ == '__main__': PORT = int(os.environ.get('PORT', 3001)) diff --git a/src/front/components/AuthNotice.jsx b/src/front/components/AuthNotice.jsx new file mode 100644 index 0000000000..426c636e95 --- /dev/null +++ b/src/front/components/AuthNotice.jsx @@ -0,0 +1,15 @@ +// Lo he creado para mostrar un aviso genérico relacionado con la autenticación +export function AuthNotice({ icon = "📩", title = "Mira tu email", text = "te hemos enviado las instrucciones a tu email" }) { + return ( +
+
{icon}
+
+
{title}
+
{text}
+
+
+ ); +} diff --git a/src/front/components/AuthShell.jsx b/src/front/components/AuthShell.jsx new file mode 100644 index 0000000000..2354e011e3 --- /dev/null +++ b/src/front/components/AuthShell.jsx @@ -0,0 +1,14 @@ +// Lo he creado para envolver las pantallas de autenticación con un diseño útil +export default function AuthShell({ children, title, subtitle }) { + return ( +
+
+
+ {title &&
{title}
} + {subtitle &&
{subtitle}
} + {children} +
+
+
+ ); +} diff --git a/src/front/components/EditProfile.jsx b/src/front/components/EditProfile.jsx new file mode 100644 index 0000000000..5096ecfb0c --- /dev/null +++ b/src/front/components/EditProfile.jsx @@ -0,0 +1,140 @@ +import React, { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Form, Button, Container, Card, Spinner } from "react-bootstrap"; + +export const EditProfile = () => { + const [formData, setFormData] = useState({ + name: "", + biography: "", + sports: "", + level: "", + avatar_url: "", + latitude: "", + longitude: "" + }); + const [loading, setLoading] = useState(true); + const navigate = useNavigate(); + + const token = localStorage.getItem("token"); + const userId = localStorage.getItem("user_id"); + + useEffect(() => { + const fetchUser = async () => { + try { + const resp = await fetch(`${process.env.BACKEND_URL}/api/user/${userId}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!resp.ok) throw new Error("Error fetching user"); + const data = await resp.json(); + setFormData(data); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + }; + if (userId) fetchUser(); + }, [userId, token]); + + const handleChange = (e) => + setFormData({ ...formData, [e.target.name]: e.target.value }); + + const handleSubmit = async (e) => { + e.preventDefault(); + try { + const resp = await fetch(`${process.env.BACKEND_URL}/api/user/${userId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(formData), + }); + if (!resp.ok) throw new Error("Error updating profile"); + navigate("/profile"); + } catch (err) { + console.error(err); + } + }; + + if (loading) + return ( +
+ +
+ ); + + return ( + + +

Edit Profile

+ +
+ + Name + + + + + Biography + + + + + Sports + + + + + Level + + + + + Avatar URL + + + +
+ + +
+
+
+
+ ); +}; diff --git a/src/front/components/ProfileView.jsx b/src/front/components/ProfileView.jsx new file mode 100644 index 0000000000..8b4d622e3a --- /dev/null +++ b/src/front/components/ProfileView.jsx @@ -0,0 +1,69 @@ +import React, { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Card, Button, Container, Row, Col, Spinner } from "react-bootstrap"; + +export const ProfileView = () => { + const [user, setUser] = useState(null); + const navigate = useNavigate(); + + const token = localStorage.getItem("token"); + const userId = localStorage.getItem("user_id"); // guarda este valor al hacer login + + useEffect(() => { + const fetchUser = async () => { + try { + const resp = await fetch(`${process.env.BACKEND_URL}/api/user/${userId}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!resp.ok) throw new Error("Error fetching user"); + const data = await resp.json(); + setUser(data); + } catch (err) { + console.error(err); + } + }; + if (userId) fetchUser(); + }, [userId, token]); + + if (!user) + return ( +
+ +
+ ); + + return ( + + +
+ avatar +
+

{user.name}

+

{user.email}

+

{user.biography || "No biography yet"}

+ +
+
+

Sports: {user.sports || "Not specified"}

+

Level: {user.level || "Not specified"}

+
+ + +
+
+ ); +}; diff --git a/src/front/components/TextInput.jsx b/src/front/components/TextInput.jsx new file mode 100644 index 0000000000..c4561012d9 --- /dev/null +++ b/src/front/components/TextInput.jsx @@ -0,0 +1,43 @@ +//Input que permite texto y opcionalmente toggle para mostrar/ocultar (para passwords) + +import { useState } from "react"; + +export default function TextInput({ + label, + type = "text", + name, + value, + onChange, + placeholder, + withToggle = false, + required = true, +}) { + const [show, setShow] = useState(false); + const finalType = withToggle ? (show ? "text" : "password") : type; + + return ( +
+ {label && } + + {withToggle && ( + + )} +
+ ); +} diff --git a/src/front/index.css b/src/front/index.css index e69de29bb2..b13c814e6f 100644 --- a/src/front/index.css +++ b/src/front/index.css @@ -0,0 +1,128 @@ +/* Tema de autenticación oscuro */ +.auth-theme { + + --bg: #0f1b2b; + --card: #1a2740; + --muted: #a3b1c6; + --text: #e8eef7; + --accent: #ff6b4a; + --accent-hover: #ff5832; + --input: #0e1a2a; + --radius: 22px; + + + background: #f7eded; + min-height: 100vh; +} + +/* Layout y tarjeta */ +.auth-theme .auth-shell { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: #f7eded; +} + +.auth-theme .auth-card { + width: 360px; + max-width: 92vw; + background: var(--bg); + border-radius: var(--radius); + padding: 28px 22px; + color: var(--text); + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.25); +} + +.auth-theme .auth-title { + font-size: 22px; + font-weight: 700; + text-align: center; + margin: 6px 0 18px; +} +.auth-theme .auth-sub { + color: var(--muted); + font-size: 13px; + text-align: center; + margin-bottom: 12px; +} + +/* Inputs y labels dentro del scope */ +.auth-theme .form-label { + color: var(--muted); + font-size: 13px; + margin-bottom: 6px; +} +.auth-theme .input-dark.form-control { + background: var(--input); + border: 1px solid rgba(255, 255, 255, 0.06); + color: var(--text); + border-radius: 12px; + height: 44px; +} +.auth-theme .input-dark.form-control::placeholder { + color: #7f8ba1; +} +.auth-theme .input-dark.form-control:focus { + background: var(--input); + color: var(--text); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: 0 0 0 3px rgba(255, 107, 74, 0.15); +} + +/* Botones y links (solo auth) */ +.auth-theme .btn-accent { + background: var(--accent); + border: none; + color: #fff; + height: 46px; + border-radius: 12px; + font-weight: 600; +} +.auth-theme .btn-accent:hover { + background: var(--accent-hover); +} + +.auth-theme .link-muted { + color: var(--muted); + font-size: 13px; +} +.auth-theme .link-accent { + color: var(--accent); + text-decoration: none; + font-weight: 600; +} +.auth-theme .link-accent:hover { + color: var(--accent-hover); +} + +/* Icono ojo y notas */ +.auth-theme .eye-btn { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + background: transparent; + border: 0; + color: #9ab0cc; +} +.auth-theme .small-note { + font-size: 12px; + color: var(--muted); + margin-top: 6px; +} + +/* Select oscuro */ +.auth-theme .select-dark.form-select { + background: var(--input); + border: 1px solid rgba(255, 255, 255, 0.06); + color: var(--text); + border-radius: 12px; + height: 44px; +} +.auth-theme .select-dark.form-select:focus { + background: var(--input); + color: var(--text); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: 0 0 0 3px rgba(255, 107, 74, 0.15); +} diff --git a/src/front/main.jsx b/src/front/main.jsx index a5a3c781dc..74f4dbc99b 100644 --- a/src/front/main.jsx +++ b/src/front/main.jsx @@ -1,29 +1,29 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import './index.css' // Global styles for your application -import { RouterProvider } from "react-router-dom"; // Import RouterProvider to use the router -import { router } from "./routes"; // Import the router configuration -import { StoreProvider } from './hooks/useGlobalReducer'; // Import the StoreProvider for global state management -import { BackendURL } from './components/BackendURL'; +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import { RouterProvider } from "react-router-dom"; +import { router } from "./routes"; +import { StoreProvider } from "./hooks/useGlobalReducer"; +import { BackendURL } from "./components/BackendURL"; const Main = () => { - - if(! import.meta.env.VITE_BACKEND_URL || import.meta.env.VITE_BACKEND_URL == "") return ( - - - + const backendURL = import.meta.env.VITE_BACKEND_URL; + + if ( ! backendURL || backendURL === "") { + return ( + + + ); + } + return ( - - {/* Provide global state to all components */} - - {/* Set up routing for the application */} - - + + + ); -} +}; -// Render the Main component into the root DOM element. -ReactDOM.createRoot(document.getElementById('root')).render(
) +ReactDOM.createRoot(document.getElementById("root")).render(
); diff --git a/src/front/pages/Forgot.jsx b/src/front/pages/Forgot.jsx new file mode 100644 index 0000000000..d00b1fa2e0 --- /dev/null +++ b/src/front/pages/Forgot.jsx @@ -0,0 +1,36 @@ + +// Pide email y muestra confirmación (solo UI). + +import { useState } from "react"; +import AuthShell from "../components/AuthShell"; +import TextInput from "../components/TextInput"; + +export function Forgot() { + const [email, setEmail] = useState(""); + const [sent, setSent] = useState(false); + const [err, setErr] = useState(""); + + const submit = (e) => { + e.preventDefault(); + setErr(""); + if (!email) return setErr("Introduce tu email."); + console.log("Forgot (solo UI):", email); + setSent(true); + }; + + return ( + + {!sent ? ( +
+ setEmail(e.target.value)} placeholder="email@example.com" /> + {err &&
{err}
} + + + ) : ( +
+ Revisa tu correo — te hemos enviado instrucciones de recuperación. +
+ )} +
+ ); +} diff --git a/src/front/pages/Home.jsx b/src/front/pages/Home.jsx index 341ed21768..be0b6084fb 100644 --- a/src/front/pages/Home.jsx +++ b/src/front/pages/Home.jsx @@ -1,52 +1,45 @@ -import React, { useEffect } from "react" -import rigoImageUrl from "../assets/img/rigo-baby.jpg"; -import useGlobalReducer from "../hooks/useGlobalReducer.jsx"; +import React from "react"; +import { useNavigate } from "react-router-dom"; +import { Container, Button } from "react-bootstrap"; export const Home = () => { - - const { store, dispatch } = useGlobalReducer() - - const loadMessage = async () => { - try { - const backendUrl = import.meta.env.VITE_BACKEND_URL - - if (!backendUrl) throw new Error("VITE_BACKEND_URL is not defined in .env file") - - const response = await fetch(backendUrl + "/api/hello") - const data = await response.json() - - if (response.ok) dispatch({ type: "set_hello", payload: data.message }) - - return data - - } catch (error) { - if (error.message) throw new Error( - `Could not fetch the message from the backend. - Please check if the backend is running and the backend port is public.` - ); - } - - } - - useEffect(() => { - loadMessage() - }, []) - - return ( -
-

Hello Rigo!!

-

- Rigo Baby -

-
- {store.message ? ( - {store.message} - ) : ( - - Loading message from the backend (make sure your python 🐍 backend is running)... - - )} -
-
- ); -}; \ No newline at end of file + const navigate = useNavigate(); + + return ( + + {/* Logo */} +
+ MeetFit Logo +
+ + {/* Título */} +

+ MeetFit +

+ + {/* Botón de Login */} +
+ +
+
+ ); +}; diff --git a/src/front/pages/Login.jsx b/src/front/pages/Login.jsx new file mode 100644 index 0000000000..3e342f4b63 --- /dev/null +++ b/src/front/pages/Login.jsx @@ -0,0 +1,70 @@ + +// Guarda email y password en el estado local y simula login en la consola. +// Muestra el aviso de "Cuenta creada" si viene el query param registered=1. +// Usa AuthShell y TextInput como componentes hijos. + +import { useState, useEffect } from "react"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import AuthShell from "../components/AuthShell"; +import TextInput from "../components/TextInput"; + +export function Login() { + const nav = useNavigate(); + const [sp] = useSearchParams(); + + const [form, setForm] = useState({ email: "", password: "" }); + const [err, setErr] = useState(""); + const [ok, setOk] = useState(""); + + useEffect(() => { + if (sp.get("registered")) setOk("Cuenta creada. Ya puedes iniciar sesión."); + }, [sp]); + + const onChange = (e) => + setForm((f) => ({ ...f, [e.target.name]: e.target.value })); + + const submit = (e) => { + e.preventDefault(); + setErr(""); + if (!form.email || !form.password) { + setErr("Completa el email y la contraseña."); + return; + } + console.log("Login (solo UI):", form); + nav("/login"); + }; + + return ( + + {ok &&
{ok}
} +
+ + +
+ + ¿Has olvidado la contraseña? + +
+ {err &&
{err}
} + + +
+ ¿No tienes cuenta? + + Regístrate + +
+
+ ); +} diff --git a/src/front/pages/MapView.jsx b/src/front/pages/MapView.jsx new file mode 100644 index 0000000000..ef4abeca8f --- /dev/null +++ b/src/front/pages/MapView.jsx @@ -0,0 +1,106 @@ +import React, { useEffect, useState } from "react"; +import { GoogleMap, Marker, InfoWindow, useJsApiLoader } from "@react-google-maps/api"; +import { Spinner, Container } from "react-bootstrap"; + +export const MapView = () => { + const [userLocation, setUserLocation] = useState(null); + const [activities, setActivities] = useState([]); + const [selectedActivity, setSelectedActivity] = useState(null); + + const { isLoaded } = useJsApiLoader({ + googleMapsApiKey: import.meta.env.VITE_GOOGLE_MAPS_API_KEY, + }); + + useEffect(() => { + // Obtener la ubicación actual del usuario + navigator.geolocation.getCurrentPosition( + (pos) => { + const { latitude, longitude } = pos.coords; + setUserLocation({ lat: latitude, lng: longitude }); + }, + (err) => console.error("Error getting location:", err), + { enableHighAccuracy: true } + ); + + // Obtener las actividades del backend + const fetchActivities = async () => { + try { + const resp = await fetch(`${import.meta.env.VITE_BACKEND_URL}/api/activities`); + if (resp.ok) { + const data = await resp.json(); + setActivities(data); + } else { + console.error("Error fetching activities"); + } + } catch (err) { + console.error(err); + } + }; + + fetchActivities(); + }, []); + + if (!isLoaded || !userLocation) + return ( +
+ +
+ ); + + return ( + +
+ setSelectedActivity(null)} // Cierra popup al hacer clic fuera + > + {/* Marcador de ubicación del usuario */} + + + {/* Marcadores de actividades */} + {activities.map((a) => ( + setSelectedActivity(a)} + /> + ))} + + {/* Popup de detalles */} + {selectedActivity && ( + setSelectedActivity(null)} + > +
+
{selectedActivity.title}
+

+ Deporte: {selectedActivity.sport} +

+

+ Fecha:{" "} + {new Date(selectedActivity.date).toLocaleDateString()} +

+ {selectedActivity.description && ( +

{selectedActivity.description}

+ )} +
+
+ )} +
+
+
+ ); +}; diff --git a/src/front/pages/Register.jsx b/src/front/pages/Register.jsx new file mode 100644 index 0000000000..abde4fb86e --- /dev/null +++ b/src/front/pages/Register.jsx @@ -0,0 +1,85 @@ + +// Registro: guarda datos en el estado local y simula registro. +// Tras registrar, redirige a /login con ?registered=1 para mostrar aviso. + +import { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import AuthShell from "../components/AuthShell"; +import TextInput from "../components/TextInput"; + +export function Register() { + const nav = useNavigate(); + const [form, setForm] = useState({ + nombre: "", + apellidos: "", + email: "", + password: "", + telefono: "", + edad: "", + genero: "", // male | female | other + }); + const [err, setErr] = useState(""); + + const onChange = (e) => + setForm((f) => ({ ...f, [e.target.name]: e.target.value })); + + const submit = (e) => { + e.preventDefault(); + setErr(""); + + if (!form.nombre || !form.email || !form.password) + return setErr("Completa nombre, email y contraseña."); + + if (form.password.length < 8) + return setErr("La contraseña debe tener al menos 8 caracteres."); + + if (form.edad && (+form.edad < 18 || +form.edad > 100)) + return setErr("Debes ser mayor de edad (18–100)."); + + if (form.genero === "") + return setErr("Selecciona un género."); + + console.log("Register (solo UI):", form); + nav("/login?registered=1"); + }; + + return ( + +
+ + + + + +
La contraseña debe tener 8 o más caracteres.
+ + + + +
+ +
+ + {err &&
{err}
} + + + + +
+ ¿Ya tienes cuenta? + Inicia sesión +
+
+ ); +} diff --git a/src/front/routes.jsx b/src/front/routes.jsx index 0557df6141..887a5be3ea 100644 --- a/src/front/routes.jsx +++ b/src/front/routes.jsx @@ -1,30 +1,24 @@ -// Import necessary components and functions from react-router-dom. - import { - createBrowserRouter, - createRoutesFromElements, - Route, + createBrowserRouter, + createRoutesFromElements, + Route, } from "react-router-dom"; import { Layout } from "./pages/Layout"; -import { Home } from "./pages/Home"; -import { Single } from "./pages/Single"; -import { Demo } from "./pages/Demo"; - -export const router = createBrowserRouter( - 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. +import { Login } from "./pages/Login"; +import { Register } from "./pages/Register"; +import { Forgot } from "./pages/Forgot"; +import { MapView } from "./pages/MapView"; - // Root Route: All navigation will start from here. - } errorElement={

Not found!

} > - {/* Nested Routes: Defines sub-routes within the BaseHome component. */} - } /> - } /> {/* Dynamic route for single items */} - } /> - - ) -); \ No newline at end of file +export const router = createBrowserRouter( + createRoutesFromElements( + } errorElement={

Not found!

}> + } /> + } /> + } /> + } /> + } /> + } /> + + ) +);