diff --git a/.github/workflows/canopeum_backend.yml b/.github/workflows/canopeum_backend.yml index 2135cdb0e..4d6e937a4 100644 --- a/.github/workflows/canopeum_backend.yml +++ b/.github/workflows/canopeum_backend.yml @@ -54,3 +54,4 @@ jobs: - uses: jakebailey/pyright-action@v2 with: python-version: "3.12" + working-directory: canopeum_backend diff --git a/.vscode/extensions.json b/.vscode/extensions.json index b1348de6a..80cddfce1 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,6 +6,7 @@ "davidanson.vscode-markdownlint", "dbaeumer.vscode-eslint", "dprint.dprint", + "dotenv.dotenv-vscode", "eamodio.gitlens", "github.vscode-github-actions", "meganrogge.template-string-converter", @@ -29,6 +30,8 @@ "coenraads.bracket-pair-colorizer", "coenraads.bracket-pair-colorizer-2", "shardulm94.trailing-spaces", + // Use the official plugin instead + "mikestead.dotenv", // Lots of conflicts "esbenp.prettier-vscode", "rvest.vs-code-prettier-eslint", diff --git a/.vscode/launch.json b/.vscode/launch.json index 7db7358d2..9bc146ef6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,23 +1,23 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python Debugger: runserver", - "type": "debugpy", - "request": "launch", - "program": "canopeum_backend/manage.py", - "args": ["runserver"], - "console": "integratedTerminal" - }, - { - "name": "Python Debugger: Current File", - "type": "debugpy", - "request": "launch", - "program": "${file}", - "console": "integratedTerminal" - } - ] + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: runserver", + "type": "debugpy", + "request": "launch", + "program": "canopeum_backend/manage.py", + "args": ["runserver"], + "console": "integratedTerminal" + }, + { + "name": "Python Debugger: Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + } + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 405aae146..98ba355f3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,9 +8,6 @@ 72 ] }, - "[markdown]": { - "files.trimTrailingWhitespace": false - }, "files.trimTrailingWhitespace": true, "files.insertFinalNewline": true, "files.trimFinalNewlines": true, @@ -52,7 +49,18 @@ * CSS-specific settings */ // TODO: Configure stylelint or dprint-prettier so I can recommend it - "[css][scss][postcss][less]": { + // NOTE: due to a bug in VSCode, we have to specify these individually to ensure it overrides user settings + // Please upvote: https://github.com/microsoft/vscode/issues/168411 + "[css]": { + "editor.defaultFormatter": "vscode.css-language-features" + }, + "[scss]": { + "editor.defaultFormatter": "vscode.css-language-features" + }, + "[postcss]": { + "editor.defaultFormatter": "vscode.css-language-features" + }, + "[less]": { "editor.defaultFormatter": "vscode.css-language-features" }, "less.format.spaceAroundSelectorSeparator": true, @@ -62,9 +70,34 @@ * JavasScript-specific settings */ // Set the default formatter to help avoid Prettier - "[json][jsonc][javascript][javascriptreact][typescript][typescriptreact][markdown][dockerfile]": { + // NOTE: due to a bug in VSCode, we have to specify these individually to ensure it overrides user settings + // Please upvote: https://github.com/microsoft/vscode/issues/168411 + "[json]": { + "editor.defaultFormatter": "dprint.dprint" + }, + "[jsonc]": { + "editor.defaultFormatter": "dprint.dprint" + }, + "[javascript]": { + "editor.defaultFormatter": "dprint.dprint" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "dprint.dprint" + }, + "[typescript]": { + "editor.defaultFormatter": "dprint.dprint" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "dprint.dprint" + }, + "[markdown]": { + "files.trimTrailingWhitespace": false, + "editor.defaultFormatter": "dprint.dprint" + }, + "[dockerfile]": { "editor.defaultFormatter": "dprint.dprint" }, + "dprint.path": "canopeum_frontend/node_modules/dprint/dprint.exe", "typescript.tsdk": "canopeum_frontend/node_modules/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true, "js/ts.implicitProjectConfig.checkJs": true, diff --git a/README.md b/README.md index 2fc53894f..7c66208be 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,15 @@ For backend cd releaf-canopeum ``` -3. Set up a Python 3.12 virtual environment +3. Install recommended Editor Extensions (for VSCode):\ + When you first open the project in VSCode, you'll get a notification like this.\ + ![Recommended Popup](/docs/Recommended_Popup.png) + + If you've already dismissed this notification, you can search for `@recommended` in your Extensions tab. + Install everything under "**WORKSPACE RECOMMENDATIONS**", you can ignore "other recommendations":\ + ![Recommended Extensions](/docs/Recommended_Extensions.png) + +4. Set up a Python 3.12 virtual environment ```shell cd canopeum_backend @@ -64,7 +72,7 @@ For backend `CTRL+Shift+P` (Open Command Palette) > `Python: Select Interpreter` ![VSCode_select_venv](/docs/VSCode_select_venv.png) -4. Set up Django backend and Database: (Skip this section for Frontend only) +5. Set up Django backend and Database: (Skip this section for Frontend only) ```shell docker compose up @@ -74,7 +82,7 @@ For backend python manage.py runserver ``` -5. Set up React frontend: +6. Set up React frontend: ```shell cd canopeum_frontend diff --git a/canopeum_backend/canopeum_backend/serializers.py b/canopeum_backend/canopeum_backend/serializers.py index 303bb58c0..19e098f10 100644 --- a/canopeum_backend/canopeum_backend/serializers.py +++ b/canopeum_backend/canopeum_backend/serializers.py @@ -59,7 +59,7 @@ class SiteTypeSerializer(serializers.ModelSerializer): class Meta: model = Sitetype - fields = ["id", "en", "fr"] + fields = ("id", "en", "fr") def get_en(self, obj): return InternationalizationSerializer(obj.name).data.get("en", None) @@ -74,7 +74,7 @@ class TreeTypeSerializer(serializers.ModelSerializer): class Meta: model = Treetype - fields = ["en", "fr"] + fields = ("en", "fr") def get_en(self, obj): return InternationalizationSerializer(obj.name).data.get("en", None) @@ -149,7 +149,7 @@ class BatchfertilizerSerializer(serializers.ModelSerializer): class Meta: model = Batchfertilizer - fields = ["id", "en", "fr"] + fields = ("id", "en", "fr") def get_en(self, obj): return InternationalizationSerializer(obj.fertilizer_type).data.get("en", None) @@ -164,7 +164,7 @@ class BatchMulchLayerSerializer(serializers.ModelSerializer): class Meta: model = Mulchlayertype - fields = ["id", "en", "fr"] + fields = ("id", "en", "fr") def get_en(self, obj): return InternationalizationSerializer(obj.mulch_layer_type).data.get("en", None) @@ -179,7 +179,7 @@ class BatchSupportedSpeciesSerializer(serializers.ModelSerializer): class Meta: model = BatchSupportedSpecies - fields = ["en", "fr"] + fields = ("en", "fr") def get_en(self, obj): return InternationalizationSerializer(obj.tree_type).data.get("en", None) @@ -194,7 +194,7 @@ class BatchSeedSerializer(serializers.ModelSerializer): class Meta: model = BatchSeed - fields = ["quantity", "en", "fr"] + fields = ("quantity", "en", "fr") def get_en(self, obj): return InternationalizationSerializer(obj.tree_type).data.get("en", None) @@ -209,7 +209,7 @@ class BatchSpeciesSerializer(serializers.ModelSerializer): class Meta: model = BatchSpecies - fields = ["quantity", "en", "fr"] + fields = ("quantity", "en", "fr") def get_en(self, obj): return InternationalizationSerializer(obj.tree_type).data.get("en", None) @@ -318,12 +318,31 @@ def get_sponsor(self, obj): return BatchSerializer(obj).data.get("sponsor", None) +class CoordinatesMapSerializer(serializers.ModelSerializer): + latitude = serializers.SerializerMethodField() + longitude = serializers.SerializerMethodField() + + class Meta: + model = Coordinate + fields = ("latitude", "longitude", "address") + + def get_latitude(self, obj): + return obj.dd_latitude + + def get_longitude(self, obj): + return obj.dd_longitude + + class SiteMapSerializer(serializers.ModelSerializer): site_type = SiteTypeSerializer() + coordinates = serializers.SerializerMethodField() class Meta: model = Site - fields = ("id", "name", "site_type", "coordinate", "image") + fields = ("id", "name", "site_type", "coordinates", "image") + + def get_coordinates(self, obj): + return CoordinatesMapSerializer(obj.coordinate).data class SiteOverviewSerializer(serializers.ModelSerializer): diff --git a/canopeum_backend/canopeum_backend/settings.py b/canopeum_backend/canopeum_backend/settings.py index ad5c3766c..4e0f2dadf 100644 --- a/canopeum_backend/canopeum_backend/settings.py +++ b/canopeum_backend/canopeum_backend/settings.py @@ -109,6 +109,11 @@ "COMPONENT_NO_READ_ONLY_REQUIRED": False, # Create separate components for PATCH endpoints (without required list) "COMPONENT_SPLIT_PATCH": True, + "CAMELIZE_NAMES": True, + "POSTPROCESSING_HOOKS": [ + "drf_spectacular.contrib.djangorestframework_camel_case.camelize_serializer_fields", + "drf_spectacular.hooks.postprocess_schema_enums", + ], } diff --git a/canopeum_backend/canopeum_backend/urls.py b/canopeum_backend/canopeum_backend/urls.py index 000fab83a..a20a3c0e5 100644 --- a/canopeum_backend/canopeum_backend/urls.py +++ b/canopeum_backend/canopeum_backend/urls.py @@ -23,7 +23,7 @@ # Like path("social/posts//likes/", views.LikeListAPIView.as_view(), name="like-list"), # Site - path("social/sites/", views.SiteSocialDetailAPIView.as_view(), name="site-list"), + path("social/sites/", views.SiteSocialListAPIView.as_view(), name="site-list"), path("social/sites//", views.SiteSocialDetailAPIView.as_view(), name="site-detail"), # Announcement path( diff --git a/canopeum_backend/canopeum_backend/views.py b/canopeum_backend/canopeum_backend/views.py index 64e162032..ad3522912 100644 --- a/canopeum_backend/canopeum_backend/views.py +++ b/canopeum_backend/canopeum_backend/views.py @@ -1,5 +1,4 @@ -from typing import ClassVar - +from django.contrib.auth import authenticate from django.contrib.auth.models import User from drf_spectacular.utils import extend_schema from rest_framework import status @@ -28,7 +27,7 @@ class LoginAPIView(APIView): - permission_classes: ClassVar[list[type[AllowAny]]] = [AllowAny] + permission_classes = (AllowAny,) @extend_schema(request=AuthUserSerializer, responses=UserSerializer, operation_id="authentication_login") def post(self, request): @@ -43,7 +42,7 @@ def post(self, request): class RegisterAPIView(APIView): - permission_classes: ClassVar[list[type[AllowAny]]] = [AllowAny] + permission_classes = (AllowAny,) @extend_schema(request=UserSerializer, responses=AuthUserSerializer, operation_id="authentication_register") def post(self, request): @@ -90,7 +89,7 @@ def get(self, request, siteId): return Response(serializer.data) @extend_schema(request=SiteSerializer, responses=SiteSerializer, operation_id="site_update") - def put(self, request, siteId): + def patch(self, request, siteId): try: site = Site.objects.get(pk=siteId) except Site.DoesNotExist: @@ -114,7 +113,7 @@ def delete(self, request, siteId): class SiteSummaryListAPIView(APIView): - @extend_schema(responses=SiteSummarySerializer(many=True), operation_id="site_summary") + @extend_schema(responses=SiteSummarySerializer(many=True), operation_id="site_summary_all") def get(self, request): sites = Site.objects.all() plant_count = 0 @@ -135,7 +134,7 @@ def get(self, request): class SiteSummaryDetailAPIView(APIView): - @extend_schema(responses=SiteSummarySerializer, operation_id="site_summarydetail") + @extend_schema(responses=SiteSummarySerializer, operation_id="site_summary") def get(self, request, siteId): try: site = Site.objects.get(pk=siteId) @@ -172,9 +171,19 @@ def get(self, request, siteId): return Response(serializer.data) +class SiteSocialListAPIView(APIView): + @extend_schema( + request=SiteSocialSerializer(many=True), responses=SiteSocialSerializer, operation_id="site_social_all" + ) + def get(self, request): + sites = Site.objects.all() + serializer = SiteSocialSerializer(sites, many=True) + return Response(serializer.data) + + class SiteMapListAPIView(APIView): - @extend_schema(responses=SiteMapSerializer, operation_id="site_map") - def get_site_map(self): + @extend_schema(responses=SiteMapSerializer(many=True), operation_id="site_map") + def get(self, request): sites = Site.objects.all() serializer = SiteMapSerializer(sites, many=True) return Response(serializer.data) @@ -183,12 +192,13 @@ def get_site_map(self): class PostListAPIView(APIView): @extend_schema(responses=PostSerializer(many=True), operation_id="post_all") def get(self, request): - try: - comment_count = Comment.objects.get(post=request.data.get("id")).count() - except Comment.DoesNotExist: - comment_count = 0 + comment_count = Comment.objects.filter(post=request.data.get("id")).count() has_liked = 0 - posts = Post.objects.all() + siteId = request.GET.get("siteId", "") + if siteId != "": + posts = Post.objects.filter(site=siteId) + else: + posts = Post.objects.all() serializer = PostSerializer(posts, many=True, context={"comment_count": comment_count, "has_liked": has_liked}) return Response(serializer.data) @@ -236,7 +246,7 @@ def delete(self, request, pk): class AnnouncementDetailAPIView(APIView): @extend_schema(request=AnnouncementSerializer, responses=AnnouncementSerializer, operation_id="announcement_update") - def put(self, request, siteId): + def patch(self, request, siteId): try: announcement = Announcement.objects.get(site=siteId) except Announcement.DoesNotExist: @@ -251,7 +261,7 @@ def put(self, request, siteId): class ContactDetailAPIView(APIView): @extend_schema(request=ContactSerializer, responses=ContactSerializer, operation_id="contact_update") - def put(self, request, pk): + def patch(self, request, pk): try: contact = Contact.objects.get(pk=pk) except Contact.DoesNotExist: @@ -265,7 +275,7 @@ def put(self, request, pk): class WidgetListAPIView(APIView): - @extend_schema(request=WidgetSerializer, responses=WidgetSerializer, operation_id="widget_all") + @extend_schema(request=WidgetSerializer, responses=WidgetSerializer, operation_id="widget-create") def post(self, request): serializer = WidgetSerializer(data=request.data) if serializer.is_valid(): @@ -275,8 +285,8 @@ def post(self, request): class WidgetDetailAPIView(APIView): - @extend_schema(request=WidgetSerializer, responses=WidgetSerializer, operation_id="widget_detail") - def put(self, request, pk): + @extend_schema(request=WidgetSerializer, responses=WidgetSerializer, operation_id="widget_update") + def patch(self, request, pk): try: widget = Widget.objects.get(pk=pk) except Widget.DoesNotExist: @@ -326,8 +336,8 @@ def post(self, request): class BatchDetailAPIView(APIView): - @extend_schema(request=BatchSerializer, responses=BatchSerializer, operation_id="batch_detail") - def put(self, request, batchId): + @extend_schema(request=BatchSerializer, responses=BatchSerializer, operation_id="batch_update") + def patch(self, request, batchId): try: batch = Batch.objects.get(pk=batchId) except Batch.DoesNotExist: @@ -351,7 +361,13 @@ def delete(self, request, batchId): class UserListAPIView(APIView): - @extend_schema(request=UserSerializer, responses=UserSerializer, operation_id="user_all") + @extend_schema(responses=UserSerializer(many=True), operation_id="user_all") + def get(self, request): + users = User.objects.all() + serializer = UserSerializer(users, many=True) + return Response(serializer.data) + + @extend_schema(request=UserSerializer, responses=UserSerializer, operation_id="user_create") def post(self, request): serializer = UserSerializer(data=request.data) if serializer.is_valid(): @@ -372,7 +388,7 @@ def get(self, request, pk): return Response(serializer.data) @extend_schema(request=UserSerializer, responses=UserSerializer, operation_id="user_update") - def put(self, request, pk): + def patch(self, request, pk): try: user = User.objects.get(pk=pk) except User.DoesNotExist: @@ -384,16 +400,6 @@ def put(self, request, pk): return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @extend_schema(operation_id="user_delete") - def delete(self, request, pk): - try: - user = User.objects.get(pk=pk) - except User.DoesNotExist: - return Response(status=status.HTTP_404_NOT_FOUND) - - user.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - class UserCurrentUserAPIView(APIView): @extend_schema(responses=UserSerializer, operation_id="user_current_user") diff --git a/canopeum_backend/pyproject.toml b/canopeum_backend/pyproject.toml index 83722d4e0..9109015ff 100644 --- a/canopeum_backend/pyproject.toml +++ b/canopeum_backend/pyproject.toml @@ -71,10 +71,9 @@ ignore = [ ### https://github.com/astral-sh/ruff/issues/1256 & https://github.com/astral-sh/ruff/issues/1774): "DJ001", # Avoid using `null=True` on string-based fields "DJ008", # Model does not define `__str__` method: https://docs.astral.sh/ruff/rules/django-model-without-dunder-str/ + "N803", # Different naming convention between js/json and python. A better/configured middleware could take care of that ] -[tool.ruff.format] - # https://docs.astral.sh/ruff/settings/#flake8-implicit-str-concat [tool.ruff.lint.flake8-implicit-str-concat] allow-multiline = false @@ -95,12 +94,6 @@ max-args = 7 # At least same as max-complexity max-branches = 15 -# https://github.com/microsoft/pyright/blob/main/docs/configuration.md#sample-pyprojecttoml-file -[tool.pyright] -pythonVersion = "3.12" -exclude = [".venv/"] -typeCheckingMode = "standard" - # https://mypy.readthedocs.io/en/stable/config_file.html [tool.mypy] show_column_numbers = true diff --git a/canopeum_frontend/.eslintrc.cjs b/canopeum_frontend/.eslintrc.cjs index e8f4e3062..22d231f2e 100644 --- a/canopeum_frontend/.eslintrc.cjs +++ b/canopeum_frontend/.eslintrc.cjs @@ -7,6 +7,7 @@ module.exports = { 'beslogic/dprint', ], ignorePatterns: [ + '.eslintrc.cjs', // Auto-generated 'src/services/api.ts', ], diff --git a/canopeum_frontend/canopeum-mockoon.json b/canopeum_frontend/canopeum-mockoon.json index b5eb15aea..6db79df9a 100644 --- a/canopeum_frontend/canopeum-mockoon.json +++ b/canopeum_frontend/canopeum-mockoon.json @@ -193,13 +193,6 @@ "value": "Mega", "invert": false, "operator": "equals" - }, - { - "target": "query", - "modifier": "", - "value": "", - "invert": false, - "operator": "equals" } ], "rulesOperator": "OR", @@ -629,7 +622,7 @@ "responses": [ { "uuid": "28554ad8-04f5-4513-9d7b-af7f9eb68a9e", - "body": "{\n \"name\": \"{{lorem (int 1 3)}}\",\n \"type\": {\n \"id\": {{int 0 6}},\n \"en\": \"{{lorem (int 1 2)}}\",\n \"fr\": \"{{lorem (int 1 2)}}\"\n },\n \"coordinates\": {\n \"dmsLatitude\": \"{{int 50 60}} {{int 15 30}} {{int 30 60}} {{int 1000 9999}} N\",\n \"dmsLongitude\": \"{{int 50 80}} {{int 20 45}} {{int 10 55}} {{int 1000 9999}} W\",\n },\n \"description\": \"{{lorem (int 1 150)}}\",\n \"size\": {{int 1 200}},\n \"species\": [\n {{#repeat 1 (int 1 4) comma=true}}\n {\n \"id\": {{int 1 9999}},\n \"en\": \"{{lorem (int 1 2)}}\",\n \"fr\": \"{{lorem (int 1 2)}}\",\n \"quantity\": {{int 1 10}}\n }\n {{/repeat}}\n ],\n \"researchPartnership\": {{boolean}},\n \"visibleOnMap\": {{boolean}}\n}", + "body": "{\n \"name\": \"{{lorem (int 1 3)}}\",\n \"type\": {\n \"id\": {{int 0 6}},\n \"en\": \"{{lorem (int 1 2)}}\",\n \"fr\": \"{{lorem (int 1 2)}}\"\n },\n \"coordinates\": {\n \"dmsLatitude\": \"{{int 50 60}} {{int 15 30}} {{int 30 60}} {{int 1000 9999}} N\",\n \"dmsLongitude\": \"{{int 50 80}} {{int 20 45}} {{int 10 55}} {{int 1000 9999}} W\"\n },\n \"description\": \"{{lorem (int 1 150)}}\",\n \"size\": {{int 1 200}},\n \"species\": [\n {{#repeat 1 (int 1 4) comma=true}}\n {\n \"id\": {{int 1 9999}},\n \"en\": \"{{lorem (int 1 2)}}\",\n \"fr\": \"{{lorem (int 1 2)}}\",\n \"quantity\": {{int 1 10}}\n }\n {{/repeat}}\n ],\n \"researchPartnership\": {{boolean}},\n \"visibleOnMap\": {{boolean}}\n}", "latency": 0, "statusCode": 200, "label": "", @@ -663,7 +656,7 @@ "responses": [ { "uuid": "ac5b893d-b30e-43c9-8988-4c13a7e0c819", - "body": "{\n \"name\": \"{{lorem (int 1 3)}}\",\n \"type\": {\n \"id\": {{int 0 6}},\n \"en\": \"{{lorem (int 1 2)}}\",\n \"fr\": \"{{lorem (int 1 2)}}\"\n },\n \"plantCount\": {{int 0 5000}},\n \"survivedCount\": {{int 0 5000}},\n \"propagationCount\": {{int 0 500}},\n \"visitorCount\": {{int 0 500}},\n \"sponsors\": {{{someOf (array (lorem 2) (lorem 2) (lorem 2) (lorem 2) (lorem 2)) 0 5 true}}},\n \"progress\": {{float 0 100}}\n}", + "body": "[\n {{#repeat 1 (int 1 14) comma=true}}\n {\n \"name\": \"{{lorem (int 1 3)}}\",\n \"type\": {\n \"id\": {{int 0 6}},\n \"en\": \"{{lorem (int 1 2)}}\",\n \"fr\": \"{{lorem (int 1 2)}}\"\n },\n \"plantCount\": {{int 0 5000}},\n \"survivedCount\": {{int 0 5000}},\n \"propagationCount\": {{int 0 500}},\n \"visitorCount\": {{int 0 500}},\n \"sponsors\": {{{someOf (array (lorem 2) (lorem 2) (lorem 2) (lorem 2) (lorem 2)) 0 5 true}}},\n \"progress\": {{float 0 100}}\n }\n {{/repeat}}\n]", "latency": 0, "statusCode": 200, "label": "", @@ -1173,7 +1166,7 @@ "responses": [ { "uuid": "cd930225-c517-4732-bb2d-51836439145a", - "body": "{\n \"firstname\": \"{{firstName}}\",\n \"lastname\": \"{{lastName}}\",\n \"email\": \"{{email}}\",\n \"role\": \"MegaAdmin\"\n \"image\": \"\"\n}", + "body": "{\n \"firstname\": \"{{firstName}}\",\n \"lastname\": \"{{lastName}}\",\n \"email\": \"{{email}}\",\n \"role\": \"MegaAdmin\",\n \"image\": \"\"\n}", "latency": 0, "statusCode": 200, "label": "Mega Admin", diff --git a/canopeum_frontend/index.html b/canopeum_frontend/index.html index 261aa7ed2..2ecaf5625 100644 --- a/canopeum_frontend/index.html +++ b/canopeum_frontend/index.html @@ -17,6 +17,10 @@ rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" /> + diff --git a/canopeum_frontend/package-lock.json b/canopeum_frontend/package-lock.json index cfde7c3ca..57a7b6ff4 100644 --- a/canopeum_frontend/package-lock.json +++ b/canopeum_frontend/package-lock.json @@ -14,10 +14,13 @@ "@mui/x-charts": "^7.0.0", "@types/node": "^20.11.30", "bootstrap": "^5.3.3", + "i18next": "^23.10.1", + "i18next-http-backend": "^2.5.0", "maplibre-gl": "^4.1.1", "material-icons": "^1.13.12", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^14.1.0", "react-map-gl": "^7.1.7", "react-router-dom": "^6.22.3" }, @@ -3322,6 +3325,14 @@ "node": ">= 6" } }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -6409,6 +6420,44 @@ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "23.10.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.10.1.tgz", + "integrity": "sha512-NDiIzFbcs3O9PXpfhkjyf7WdqFn5Vq6mhzhtkXzj51aOcNuPNcTwuYNuXCpHsanZGHlHKL35G7huoFeVic1hng==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.5.0.tgz", + "integrity": "sha512-Z/aQsGZk1gSxt2/DztXk92DuDD20J+rNudT7ZCdTrNOiK8uQppfvdjq9+DFQfpAnFPn3VZS+KQIr1S/W1KxhpQ==", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -7530,6 +7579,25 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -8146,6 +8214,27 @@ "react": "^18.2.0" } }, + "node_modules/react-i18next": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.0.tgz", + "integrity": "sha512-3KwX6LHpbvGQ+sBEntjV4sYW3Zovjjl3fpoHbUwSgFHf0uRBcbeCBLR5al6ikncI5+W0EFb71QXZmfop+J6NrQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -9271,6 +9360,11 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -9632,6 +9726,14 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/vt-pbf": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", @@ -9642,6 +9744,20 @@ "pbf": "^3.2.1" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/canopeum_frontend/package.json b/canopeum_frontend/package.json index ee6a67755..4d7726218 100644 --- a/canopeum_frontend/package.json +++ b/canopeum_frontend/package.json @@ -18,10 +18,13 @@ "@mui/x-charts": "^7.0.0", "@types/node": "^20.11.30", "bootstrap": "^5.3.3", + "i18next": "^23.10.1", + "i18next-http-backend": "^2.5.0", "maplibre-gl": "^4.1.1", "material-icons": "^1.13.12", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^14.1.0", "react-map-gl": "^7.1.7", "react-router-dom": "^6.22.3" }, diff --git a/canopeum_frontend/src/App.scss b/canopeum_frontend/src/App.scss index f23dc6227..aba961d2e 100644 --- a/canopeum_frontend/src/App.scss +++ b/canopeum_frontend/src/App.scss @@ -15,27 +15,27 @@ body { } .navbar { - position: sticky; - top: 0; - z-index: 1000; - box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); - padding-bottom: 0; + position: sticky; + top: 0; + z-index: 1000; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); + padding-bottom: 0; } .navbar-logo { - padding-bottom: 0.3rem; + padding-bottom: 0.3rem; } .navbar li { - border-bottom: 0.3rem solid transparent; + border-bottom: 0.3rem solid transparent; } .nav-item.active { - border-bottom-color: white; + border-bottom-color: white; } .nav-item.active .material-symbols-outlined { - font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48; + font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48; } .fill-icon { diff --git a/canopeum_frontend/src/App.tsx b/canopeum_frontend/src/App.tsx index 430935efe..23c7a05c1 100644 --- a/canopeum_frontend/src/App.tsx +++ b/canopeum_frontend/src/App.tsx @@ -1,10 +1,10 @@ -import './App.scss'; -import 'bootstrap/js/index.umd.js'; +import './App.scss' +import 'bootstrap/js/index.umd.js' -import { BrowserRouter } from 'react-router-dom'; +import { BrowserRouter } from 'react-router-dom' -import AuthenticationContextProvider from './components/context/AuthenticationContext'; -import MainLayout from './components/MainLayout'; +import AuthenticationContextProvider from './components/context/AuthenticationContext' +import MainLayout from './components/MainLayout' const App = () => ( @@ -12,6 +12,6 @@ const App = () => ( -); +) -export default App; +export default App diff --git a/canopeum_frontend/src/components/Navbar.tsx b/canopeum_frontend/src/components/Navbar.tsx index 163e844c6..0939b070f 100644 --- a/canopeum_frontend/src/components/Navbar.tsx +++ b/canopeum_frontend/src/components/Navbar.tsx @@ -1,90 +1,126 @@ -import { useCallback, useContext } from 'react'; -import { Link, useLocation, useNavigate } from 'react-router-dom'; -import { AuthenticationContext } from './context/AuthenticationContext'; +import { useCallback, useContext, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Link, useLocation, useNavigate } from 'react-router-dom' + +import { AuthenticationContext } from './context/AuthenticationContext' const Navbar = () => { - const location = useLocation(); + const { i18n: { changeLanguage, language } } = useTranslation() + const [currentLanguage, setCurrentLanguage] = useState(language) + + const location = useLocation() + + const handleChangeLanguage = () => { + const newLanguage = currentLanguage === 'en' + ? 'fr' + : 'en' + setCurrentLanguage(newLanguage) + void changeLanguage(newLanguage) + } + const { isAuthenticated, logout } = useContext(AuthenticationContext) const navigate = useNavigate() const onLoginLogoutbuttonClick = useCallback(() => { - if (!isAuthenticated) { - navigate('/login') - } - else { + if (isAuthenticated) { logout() + } else { + navigate('/login') } }, [isAuthenticated, navigate, logout]) return ( -