From 3b935a3116642fc36d349ed4e4cb735102bfc570 Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Mon, 5 Jun 2023 00:25:22 +0600 Subject: [PATCH 01/45] Configured REST Framework --- UTILS.md | 15 + project/config/api_router.py | 17 + project/config/settings.py | 60 +- project/config/urls.py | 82 ++- project/config/views.py | 32 +- project/poetry.lock | 548 +++++++++++++++++- .../staticfiles/icons/user/avatar-default.png | Bin 0 -> 28790 bytes .../staticfiles/icons/user/avatar-female.png | Bin 0 -> 23261 bytes .../staticfiles/icons/user/avatar-male.png | Bin 0 -> 22048 bytes .../icons/user/female-user-circle.png | Bin 0 -> 4013 bytes .../staticfiles/icons/user/female-user.png | Bin 0 -> 6653 bytes .../icons/user/male-user-circle.png | Bin 0 -> 4119 bytes .../staticfiles/icons/user/male-user.png | Bin 0 -> 2876 bytes .../public/staticfiles/icons/user/user.png | Bin 0 -> 5525 bytes project/pyproject.toml | 4 + project/users/__init__.py | 0 project/users/admin.py | 38 ++ project/users/api/views.py | 19 + project/users/apps.py | 6 + project/users/migrations/0001_initial.py | 109 ++++ project/users/migrations/__init__.py | 0 project/users/models.py | 144 +++++ project/users/tests.py | 3 + project/utils/__init__.py | 0 project/utils/helpers.py | 53 ++ project/utils/image_upload_helpers.py | 25 + project/utils/snippets.py | 284 +++++++++ 27 files changed, 1429 insertions(+), 10 deletions(-) create mode 100644 project/config/api_router.py create mode 100644 project/public/staticfiles/icons/user/avatar-default.png create mode 100644 project/public/staticfiles/icons/user/avatar-female.png create mode 100644 project/public/staticfiles/icons/user/avatar-male.png create mode 100644 project/public/staticfiles/icons/user/female-user-circle.png create mode 100644 project/public/staticfiles/icons/user/female-user.png create mode 100644 project/public/staticfiles/icons/user/male-user-circle.png create mode 100644 project/public/staticfiles/icons/user/male-user.png create mode 100644 project/public/staticfiles/icons/user/user.png create mode 100644 project/users/__init__.py create mode 100644 project/users/admin.py create mode 100644 project/users/api/views.py create mode 100644 project/users/apps.py create mode 100644 project/users/migrations/0001_initial.py create mode 100644 project/users/migrations/__init__.py create mode 100644 project/users/models.py create mode 100644 project/users/tests.py create mode 100644 project/utils/__init__.py create mode 100644 project/utils/helpers.py create mode 100644 project/utils/image_upload_helpers.py create mode 100644 project/utils/snippets.py diff --git a/UTILS.md b/UTILS.md index 82fc78e..456c480 100644 --- a/UTILS.md +++ b/UTILS.md @@ -11,3 +11,18 @@ Generate without hashes $ poetry export -f requirements.txt --output requirements.txt --without-hashes + +## Demo User Authentication Token for Development + +```json +{ + "expiry": "2023-07-05T03:53:01.757821Z", + "token": "c012a83914869d906fc34e514d1c101e9175c652975f48372e731d72091c9bd3", + "user": { + "email": "admin@admin.com" + } +} +``` + +Usage: +Token c012a83914869d906fc34e514d1c101e9175c652975f48372e731d72091c9bd3 diff --git a/project/config/api_router.py b/project/config/api_router.py new file mode 100644 index 0000000..b8d5eec --- /dev/null +++ b/project/config/api_router.py @@ -0,0 +1,17 @@ +from django.conf import settings +from rest_framework.routers import DefaultRouter, SimpleRouter +from users.api.views import LoginView +from knox import views as knox_views +from .views import ExampleView + + +if settings.DEBUG: + router = DefaultRouter() +else: + router = SimpleRouter() + +router.register("example", ExampleView, basename="example") + + +app_name = "api" +urlpatterns = router.urls diff --git a/project/config/settings.py b/project/config/settings.py index 9680244..60390de 100644 --- a/project/config/settings.py +++ b/project/config/settings.py @@ -4,6 +4,8 @@ from pathlib import Path import os from conf import config +from datetime import timedelta +from rest_framework.settings import api_settings # ---------------------------------------------------- # *** Project's BASE DIRECTORY *** @@ -28,8 +30,17 @@ # ---------------------------------------------------- # *** Application Definition *** # ---------------------------------------------------- -THIRD_PARTY_APPS = [] -LOCAL_APPS = [] +THIRD_PARTY_APPS = [ + # Django REST Framework + "rest_framework", + # Knox Authentication + "knox", + # Django REST Framework Yet Another Swagger + "drf_yasg", +] +LOCAL_APPS = [ + "users", +] INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", @@ -78,6 +89,13 @@ }, ] +# ---------------------------------------------------- +# *** Authentication Definition *** +# ---------------------------------------------------- + +# https://docs.djangoproject.com/en/dev/topics/auth/customizing/#substituting-a-custom-user-model +AUTH_USER_MODEL = 'users.User' + # ---------------------------------------------------- # *** WSGI Application *** # ---------------------------------------------------- @@ -189,3 +207,41 @@ # ---------------------------------------------------- DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" SITE_ID = 1 + +# REST Framework Configuration +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": ( + "knox.auth.TokenAuthentication", + ), + "DEFAULT_PERMISSION_CLASSES": ( + "rest_framework.permissions.IsAuthenticated", + ), +} + +# KNOX Configuration +KNOX_TOKEN_MODEL = "knox.AuthToken" + +REST_KNOX = { + # "SECURE_HASH_ALGORITHM": "hashlib.sha512", + "AUTH_TOKEN_CHARACTER_LENGTH": 64, + "TOKEN_TTL": timedelta(hours=730), + "USER_SERIALIZER": "knox.serializers.UserSerializer", + "TOKEN_LIMIT_PER_USER": None, + "AUTO_REFRESH": False, + "MIN_REFRESH_INTERVAL": 60, + "AUTH_HEADER_PREFIX": "Token", + "EXPIRY_DATETIME_FORMAT": api_settings.DATETIME_FORMAT, + "TOKEN_MODEL": "knox.AuthToken", +} + +# Swagger Configuration +SWAGGER_SETTINGS = { + 'SECURITY_DEFINITIONS': { + 'Bearer': { + 'type': 'apiKey', + 'name': 'Authorization', + 'in': 'header' + } + }, + 'JSON_EDITOR': True, +} diff --git a/project/config/urls.py b/project/config/urls.py index dcf4fcb..b4043a6 100644 --- a/project/config/urls.py +++ b/project/config/urls.py @@ -14,8 +14,54 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path, include -from .views import index +from django.urls import path, include, re_path +from .views import IndexView +from users.api.views import LoginView +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi +from knox import views as knox_views +from django.conf import settings +from django.views import defaults as default_views + + +# Yet Another Swagger Schema View +schema_view = get_schema_view( + openapi.Info( + title="Numan Ibn Mazid's Portfolio API", + default_version='v1', + description="API Documentation for Numan Ibn Mazid's Portfolio Project's Backend", + terms_of_service="https://www.google.com/policies/terms/", + contact=openapi.Contact(email="numanibnmazid@gmail.com"), + license=openapi.License(name="BSD License"), + ), + public=True, + permission_classes=[permissions.AllowAny], +) + + +THIRD_PARTY_URLS = [ + # ---------------------------------------------------- + # *** REST FRMAEWORK API URLs *** + # ---------------------------------------------------- + path("api/", include("config.api_router")), + # ---------------------------------------------------- + # *** Knox URLs *** + # ---------------------------------------------------- + # path(r'api/auth/', include('knox.urls')), + path(r'api/auth/login/', LoginView.as_view(), name='knox_login'), + path(r'api/auth/logout/', knox_views.LogoutView.as_view(), name='knox_logout'), + path(r'api/auth/logoutall/', knox_views.LogoutAllView.as_view(), name='knox_logoutall'), + # ---------------------------------------------------- + # *** Swagger URLs *** + # ---------------------------------------------------- + re_path(r'^swagger(?P\.json|\.yaml)$', + schema_view.without_ui(cache_timeout=0), name='schema-json'), + re_path(r'^swagger/$', schema_view.with_ui('swagger', + cache_timeout=0), name='schema-swagger-ui'), + re_path(r'^redoc/$', schema_view.with_ui('redoc', + cache_timeout=0), name='schema-redoc'), +] urlpatterns = [ # ---------------------------------------------------- @@ -25,5 +71,33 @@ # ---------------------------------------------------- # *** Project URLs *** # ---------------------------------------------------- - path("", index, name="index") -] + path("", IndexView.as_view(), name="index"), +] + THIRD_PARTY_URLS + + +if settings.DEBUG: + # This allows the error pages to be debugged during development, just visit + # these url in browser to see how these error pages look like. + urlpatterns += [ + path( + "400/", + default_views.bad_request, + kwargs={"exception": Exception("Bad Request!")}, + ), + path( + "403/", + default_views.permission_denied, + kwargs={"exception": Exception("Permission Denied")}, + ), + path( + "404/", + default_views.page_not_found, + kwargs={"exception": Exception("Page not Found")}, + ), + path("500/", default_views.server_error), + ] + # if "debug_toolbar" in settings.INSTALLED_APPS: + # import debug_toolbar + + # urlpatterns = [ + # path("__debug__/", include(debug_toolbar.urls))] + urlpatterns diff --git a/project/config/views.py b/project/config/views.py index 9000f74..54491ba 100644 --- a/project/config/views.py +++ b/project/config/views.py @@ -1,7 +1,33 @@ from django.shortcuts import render +from knox.auth import TokenAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework.viewsets import ViewSet +from utils.helpers import ResponseWrapper +from django.views.generic import TemplateView -def index(request): - context = {"hello": {"world": {"name": "Universe"}}} - return render(request, "index.html", context) +class IndexView(TemplateView): + template_name = "index.html" + +class ExampleView(ViewSet): + authentication_classes = (TokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def list(self, request): + return ResponseWrapper({"Demo": "Hello, world! This is LIST action"}) + + def create(self, request): + return ResponseWrapper({"Demo": "Hello, world! This is CREATE action"}) + + def retrieve(self, request, pk=None): + return ResponseWrapper({"Demo": "Hello, world! This is RETRIEVE action"}) + + def update(self, request, pk=None): + return ResponseWrapper({"Demo": "Hello, world! This is UPDATE action"}) + + def partial_update(self, request, pk=None): + return ResponseWrapper({"Demo": "Hello, world! This is PARTIAL UPDATE action"}) + + def destroy(self, request, pk=None): + return ResponseWrapper({"Demo": "Hello, world! This is DESTROY action"}) diff --git a/project/poetry.lock b/project/poetry.lock index 583d070..6b32269 100644 --- a/project/poetry.lock +++ b/project/poetry.lock @@ -85,6 +85,17 @@ files = [ pycodestyle = ">=2.9.1" toml = "*" +[[package]] +name = "certifi" +version = "2023.5.7" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, + {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, +] + [[package]] name = "cffi" version = "1.15.1" @@ -161,6 +172,162 @@ files = [ [package.dependencies] pycparser = "*" +[[package]] +name = "charset-normalizer" +version = "3.1.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, + {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, +] + +[[package]] +name = "coreapi" +version = "2.3.3" +description = "Python client library for Core API." +optional = false +python-versions = "*" +files = [ + {file = "coreapi-2.3.3-py2.py3-none-any.whl", hash = "sha256:bf39d118d6d3e171f10df9ede5666f63ad80bba9a29a8ec17726a66cf52ee6f3"}, + {file = "coreapi-2.3.3.tar.gz", hash = "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb"}, +] + +[package.dependencies] +coreschema = "*" +itypes = "*" +requests = "*" +uritemplate = "*" + +[[package]] +name = "coreschema" +version = "0.0.4" +description = "Core Schema." +optional = false +python-versions = "*" +files = [ + {file = "coreschema-0.0.4-py2-none-any.whl", hash = "sha256:5e6ef7bf38c1525d5e55a895934ab4273548629f16aed5c0a6caa74ebf45551f"}, + {file = "coreschema-0.0.4.tar.gz", hash = "sha256:9503506007d482ab0867ba14724b93c18a33b22b6d19fb419ef2d239dd4a1607"}, +] + +[package.dependencies] +jinja2 = "*" + +[[package]] +name = "cryptography" +version = "41.0.1" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-41.0.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:f73bff05db2a3e5974a6fd248af2566134d8981fd7ab012e5dd4ddb1d9a70699"}, + {file = "cryptography-41.0.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1a5472d40c8f8e91ff7a3d8ac6dfa363d8e3138b961529c996f3e2df0c7a411a"}, + {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fa01527046ca5facdf973eef2535a27fec4cb651e4daec4d043ef63f6ecd4ca"}, + {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b46e37db3cc267b4dea1f56da7346c9727e1209aa98487179ee8ebed09d21e43"}, + {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d198820aba55660b4d74f7b5fd1f17db3aa5eb3e6893b0a41b75e84e4f9e0e4b"}, + {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:948224d76c4b6457349d47c0c98657557f429b4e93057cf5a2f71d603e2fc3a3"}, + {file = "cryptography-41.0.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:059e348f9a3c1950937e1b5d7ba1f8e968508ab181e75fc32b879452f08356db"}, + {file = "cryptography-41.0.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b4ceb5324b998ce2003bc17d519080b4ec8d5b7b70794cbd2836101406a9be31"}, + {file = "cryptography-41.0.1-cp37-abi3-win32.whl", hash = "sha256:8f4ab7021127a9b4323537300a2acfb450124b2def3756f64dc3a3d2160ee4b5"}, + {file = "cryptography-41.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:1fee5aacc7367487b4e22484d3c7e547992ed726d14864ee33c0176ae43b0d7c"}, + {file = "cryptography-41.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9a6c7a3c87d595608a39980ebaa04d5a37f94024c9f24eb7d10262b92f739ddb"}, + {file = "cryptography-41.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5d092fdfedaec4cbbffbf98cddc915ba145313a6fdaab83c6e67f4e6c218e6f3"}, + {file = "cryptography-41.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a8e6c2de6fbbcc5e14fd27fb24414507cb3333198ea9ab1258d916f00bc3039"}, + {file = "cryptography-41.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb33ccf15e89f7ed89b235cff9d49e2e62c6c981a6061c9c8bb47ed7951190bc"}, + {file = "cryptography-41.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f0ff6e18d13a3de56f609dd1fd11470918f770c6bd5d00d632076c727d35485"}, + {file = "cryptography-41.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7bfc55a5eae8b86a287747053140ba221afc65eb06207bedf6e019b8934b477c"}, + {file = "cryptography-41.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eb8163f5e549a22888c18b0d53d6bb62a20510060a22fd5a995ec8a05268df8a"}, + {file = "cryptography-41.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8dde71c4169ec5ccc1087bb7521d54251c016f126f922ab2dfe6649170a3b8c5"}, + {file = "cryptography-41.0.1.tar.gz", hash = "sha256:d34579085401d3f49762d2f7d6634d6b6c2ae1242202e860f4d26b046e3a1006"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +nox = ["nox"] +pep8test = ["black", "check-sdist", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "django" version = "4.2.1" @@ -181,6 +348,261 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] +[[package]] +name = "django-rest-knox" +version = "4.2.0" +description = "Authentication for django rest framework" +optional = false +python-versions = ">=3.6" +files = [ + {file = "django-rest-knox-4.2.0.tar.gz", hash = "sha256:4595f1dc23d6e41af7939e5f2d8fdaf6ade0a74a656218e7b56683db5566fcc9"}, + {file = "django_rest_knox-4.2.0-py3-none-any.whl", hash = "sha256:62b8e374a44cd4e9617eaefe27c915b301bf224fa6550633d3013d3f9f415113"}, +] + +[package.dependencies] +cryptography = "*" +django = ">=3.2" +djangorestframework = "*" + +[[package]] +name = "djangorestframework" +version = "3.14.0" +description = "Web APIs for Django, made easy." +optional = false +python-versions = ">=3.6" +files = [ + {file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"}, + {file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"}, +] + +[package.dependencies] +django = ">=3.0" +pytz = "*" + +[[package]] +name = "drf-yasg" +version = "1.21.5" +description = "Automated generation of real Swagger/OpenAPI 2.0 schemas from Django Rest Framework code." +optional = false +python-versions = ">=3.6" +files = [ + {file = "drf-yasg-1.21.5.tar.gz", hash = "sha256:ceef0c3b5dc4389781afd786e6dc3697af2a2fe0d8724ee1f637c23d75bbc5b2"}, + {file = "drf_yasg-1.21.5-py3-none-any.whl", hash = "sha256:ba9cf4bf79f259290daee9b400fa4fcdb0e78d2f043fa5e9f6589c939fd06d05"}, +] + +[package.dependencies] +coreapi = ">=2.3.3" +coreschema = ">=0.0.4" +django = ">=2.2.16" +djangorestframework = ">=3.10.3" +inflection = ">=0.3.1" +packaging = ">=21.0" +pytz = ">=2021.1" +"ruamel.yaml" = ">=0.16.13" +uritemplate = ">=3.0.0" + +[package.extras] +validation = ["swagger-spec-validator (>=2.1.0)"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "inflection" +version = "0.5.1" +description = "A port of Ruby on Rails inflector to Python" +optional = false +python-versions = ">=3.5" +files = [ + {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, + {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, +] + +[[package]] +name = "itypes" +version = "1.2.0" +description = "Simple immutable types for python." +optional = false +python-versions = "*" +files = [ + {file = "itypes-1.2.0-py2.py3-none-any.whl", hash = "sha256:03da6872ca89d29aef62773672b2d408f490f80db48b23079a4b194c86dd04c6"}, + {file = "itypes-1.2.0.tar.gz", hash = "sha256:af886f129dea4a2a1e3d36595a2d139589e4dd287f5cab0b40e799ee81570ff1"}, +] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "pillow" +version = "9.5.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, + {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, + {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, + {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, + {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, + {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, + {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, + {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, + {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, + {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, + {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, + {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, + {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, + {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, + {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, + {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, + {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + [[package]] name = "psycopg2-binary" version = "2.9.6" @@ -340,6 +762,102 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "pytz" +version = "2023.3" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, + {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "ruamel-yaml" +version = "0.17.31" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +optional = false +python-versions = ">=3" +files = [ + {file = "ruamel.yaml-0.17.31-py3-none-any.whl", hash = "sha256:3cf153f0047ced526e723097ac615d3009371779432e304dbd5596b6f3a4c777"}, + {file = "ruamel.yaml-0.17.31.tar.gz", hash = "sha256:098ed1eb6d338a684891a72380277c1e6fc4d4ae0e120de9a447275056dda335"}, +] + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.12\""} + +[package.extras] +docs = ["ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.7" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +optional = false +python-versions = ">=3.5" +files = [ + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5859983f26d8cd7bb5c287ef452e8aacc86501487634573d260968f753e1d71"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:debc87a9516b237d0466a711b18b6ebeb17ba9f391eb7f91c649c5c4ec5006c7"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:df5828871e6648db72d1c19b4bd24819b80a755c4541d3409f0f7acd0f335c80"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:efa08d63ef03d079dcae1dfe334f6c8847ba8b645d08df286358b1f5293d24ab"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win32.whl", hash = "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:1a6391a7cabb7641c32517539ca42cf84b87b667bad38b78d4d42dd23e957c81"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:9c7617df90c1365638916b98cdd9be833d31d337dbcd722485597b43c4a215bf"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win32.whl", hash = "sha256:f6d3d39611ac2e4f62c3128a9eed45f19a6608670c5a2f4f07f24e8de3441d38"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:da538167284de58a52109a9b89b8f6a53ff8437dd6dc26d33b57bf6699153122"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_12_0_arm64.whl", hash = "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:370445fd795706fd291ab00c9df38a0caed0f17a6fb46b0f607668ecb16ce763"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win32.whl", hash = "sha256:ecdf1a604009bd35c674b9225a8fa609e0282d9b896c03dd441a91e5f53b534e"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win_amd64.whl", hash = "sha256:f34019dced51047d6f70cb9383b2ae2853b7fc4dce65129a5acd49f4f9256646"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2aa261c29a5545adfef9296b7e33941f46aa5bbd21164228e833412af4c9c75f"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f01da5790e95815eb5a8a138508c01c758e5f5bc0ce4286c4f7028b8dd7ac3d0"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:40d030e2329ce5286d6b231b8726959ebbe0404c92f0a578c0e2482182e38282"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c3ca1fbba4ae962521e5eb66d72998b51f0f4d0f608d3c0347a48e1af262efa7"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win32.whl", hash = "sha256:7bdb4c06b063f6fd55e472e201317a3bb6cdeeee5d5a38512ea5c01e1acbdd93"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:be2a7ad8fd8f7442b24323d24ba0b56c51219513cfa45b9ada3b87b76c374d4b"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91a789b4aa0097b78c93e3dc4b40040ba55bef518f84a40d4442f713b4094acb"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:99e77daab5d13a48a4054803d052ff40780278240a902b880dd37a51ba01a307"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3243f48ecd450eddadc2d11b5feb08aca941b5cd98c9b1db14b2fd128be8c697"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8831a2cedcd0f0927f788c5bdf6567d9dc9cc235646a434986a852af1cb54b4b"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win32.whl", hash = "sha256:3110a99e0f94a4a3470ff67fc20d3f96c25b13d24c6980ff841e82bafe827cac"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:92460ce908546ab69770b2e576e4f99fbb4ce6ab4b245345a3869a0a0410488f"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5bc0667c1eb8f83a3752b71b9c4ba55ef7c7058ae57022dd9b29065186a113d9"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:4a4d8d417868d68b979076a9be6a38c676eca060785abaa6709c7b31593c35d1"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bf9a6bc4a0221538b1a7de3ed7bca4c93c02346853f44e1cd764be0023cd3640"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7b301ff08055d73223058b5c46c55638917f04d21577c95e00e0c4d79201a6b"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win32.whl", hash = "sha256:d5e51e2901ec2366b79f16c2299a03e74ba4531ddcfacc1416639c557aef0ad8"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:184faeaec61dbaa3cace407cffc5819f7b977e75360e8d5ca19461cd851a5fc5"}, + {file = "ruamel.yaml.clib-0.2.7.tar.gz", hash = "sha256:1f08fd5a2bea9c4180db71678e850b995d2a5f4537be0e94557668cf0f5f9497"}, +] + [[package]] name = "sqlparse" version = "0.4.4" @@ -389,7 +907,35 @@ files = [ {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, ] +[[package]] +name = "uritemplate" +version = "4.1.1" +description = "Implementation of RFC 6570 URI Templates" +optional = false +python-versions = ">=3.6" +files = [ + {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, + {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, +] + +[[package]] +name = "urllib3" +version = "2.0.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.7" +files = [ + {file = "urllib3-2.0.2-py3-none-any.whl", hash = "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e"}, + {file = "urllib3-2.0.2.tar.gz", hash = "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [metadata] lock-version = "2.0" python-versions = "3.9.13" -content-hash = "404a2eddd8f718cd34411f5d5265c0433e30bdbb0fe3fc0fa75eac77120f88b7" +content-hash = "8a84a749e9214717dd8ad819f958d5dd55750052959672720d6206fd664f0a57" diff --git a/project/public/staticfiles/icons/user/avatar-default.png b/project/public/staticfiles/icons/user/avatar-default.png new file mode 100644 index 0000000000000000000000000000000000000000..5911d304818d059f097c9a70e53dbcc7e6437fa0 GIT binary patch literal 28790 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 ZKK(2Qttr(S1>UCynHX9itMpxB{tr7Ev4Q{q literal 0 HcmV?d00001 diff --git a/project/public/staticfiles/icons/user/avatar-male.png b/project/public/staticfiles/icons/user/avatar-male.png new file mode 100644 index 0000000000000000000000000000000000000000..f1e2bed7da654329530cd183b0d7a0a475f0b72e GIT binary patch literal 22048 zcmXtg1yq#J_xH1ONrQBXfPjFYNH4HTDJjy8Akrlb3rLBANQlxXEh*h4DM(2-NH@~3 z`_6uU|JQRkyE`+_og1II_s-0HjMPw5yg|f31VPXZWhI0b1Yv=%SP%go`1t2FdI3Ig z+~k#?5rBVw1XdB?H=(nVz8eIQc3}Qs34ElC5X1&4BjlcWrEJf7`aK&@6+`_#o%cWQ`;iqAmx|05MpMxoSob8rj;AE&Rkv}HB{v;Z0BspN$E>^e@u_FFW8~( zntayPXl`G6BUy9Z&LN42R)`t?LAH{tw-X4k|@g$dcc_Rd~t_mrkvmPNS@u>NW z9tsL^e!MvvzLVY`r|2bkIm!`XMK;8Yj2I~LI!w28*!-P^Ne5m#?ZA{&_!32p5b3%Z%`eX?ov zzqq(MDvEPLVkq`T&i4jhhldQ$d0S~z%-<8V)rp8lYQ3asmCo8P^s$<)KSiO(izTUI z$IBq{?7@bFKU7JkH(l;NpY@Jtc!P`NQC{V5bbLp#-k`}guJEByGA5UY zNk>Dr!lHFg8ojSBGsqm?O{@n+;N`pjy?+F4?XkZK2_*YB`N%`QXBO$QReTv(r442k zvxKoRWPGGyT$6uS^0r%LR{u(g*F!e`_MFe1R&h z(RyB>ZdYL6P+|XP>gnu3&hA5mV;dGcK4bXr_`47a^he6t$4KO^)IVX|4n%%>>pisp zc!;d8x+e>Vm>G<1v%P^!H%5cq&jcn$aLd?}MM4bs&+Qh6;h>UTzm;gqx+8LNdSYnE zJKm_mHV%Gm3O{tW$wX-=Qm?S4)$g55dvLS6-T_?w;4v<9Vd$v2!EwLt#~>TeFN2=Cmv-&*e@omW}-x*Op^QkeL1M(|4{LqRofZT6*!!d1;AA_H|SE`l<`i96= z?b`0SdDN~DLgJu3+RtIGp#Migb^u{3$*g|2+NKA^^4ru?)D`Zma)i6E2R_>{Lgl&R z1ye@AjabdP_7AhSZfcB2-%a{+xH$JIZ7elR3BMTnu*Y9>b{JbWu<+Z9*mN_Cg1cK9IPaiWoXq*?5$ESBprNrd0{dqDg9~k$$!1?>X~Tk|5T&1{WT{&UMq+{|7vP zCzu^fvTQ}P@>UpE1eo%dAh-MiRT9sm*y zytOY!1USx!*!=uQ*dlp9+y_Hm1lFAX#P`T$SGny7`(X1-R~P?F`*I*j?9;j*AMklS ze66?Y(`V{D?4Ed1XITL>spl2XrOYusZw||N?n!$n6e??7)%(}58rAR=jA#X`YP?X- zcXlv3dO%kG+hZ^Umd!#O8eI9m#r&201|Bg~JB5K{seFw(+mX8sJ6c0S@;#|h#Zjze z^u~a^s0ILm!HYsM<3Za+IT#UG6==gc~Gr{E_80WIX34VIB&l({|jiaapR z?@_~p0kc0xWat%afPss?L1tuLv&xuJEOEPEQk?7t_f3Y@2?dNd`;+=6uUQZ3)=^0` zN#{CPz&1MJ&Lz(vKUfI)@k-2QYe0UK6>z{H^2sYN1 zZ`ERikoP!V{bvS8@dOji>PHMbMjpO%zDO@96w3u9;xQ~XeEhf-(r@lj4NqJjkasuX zxZh8|8Av2%Pp}a!KYA04XlkfpRVaPzNs@(A)caBe{dt!h;lJ3f)jg#0-2Nh%?#y>q zeBibSC$qx6HiFz-b^&U6U&ZiUr6#ioHoFP(HzOnDYUatjx(CqGEc|8^A^vdz4;a)>bqZ4b( zuC&wF{8}Attv&j*`_!S@+!nnZWl2&2B*P;zCg!>-i~B=94M!-shT1*phKtnzy~N?9 z6HXQViL)8(YP5`CI5_pE8vT5E>R$(6U4HxbLq_VdR?Y2M~yQ&azn zdwKF6WWW2;JcmBYt;A!OAO@;S`FLM9Mv>%qL^n^WIEeyzsw|`G*K7h@Y;2T-OJCbu zHJRBOamnHzv6FVK=(CN{*kBmd4^C}O2{(z`Mnd-%mC^QJ45#)#DONAnewtIVXhUfe zh4XV(Eu8xXo=1E%Nx0Esk9x{~Oz!U5?Mu19Yg1!{mvRZDJqp+x?x<1BxD#_S8?4 zaYmG8(epby%r#rTW0wAio#xuV!=^rcc1rlk*7-+dL~Bi7wQt9C!3TZOxR|vQC9Zl6~M1i1BTEA!UM*LiQ=(>%wlz3V7 zUZl-7k4K<0VJp#GZRnZRt#jvjvz1jV^EO=0gWj!6do=nvA`EW6i_f^V+c8$O>*Td! zzz==XQSN+`{K)xzbQvIUHeI+SbWcpNYu~?@B=>8n83%15Eho1x-4_FcA+K_M&+kW+ zkoN89pS_QWoqGx%CB0ts`WBkvZFrTkZJs3bn%ZQdu?v+z+xl&2FZ~1AY13IvBQ}-) zJ45K((~Y-`kWRj8}}Bv%c$y>{Z@iCYm={e0JFf%Foe8;cmSEnisbQ|y#b5PWO)ZPqf1 z;DK>D7VrU+((YP+#8NsKFXw(HJ2i#_M*;U$%yqD|2^-NA@@C3;$`YO**JZ9`9p+$} z+g4>`(jgsVUk&Ix^N?>R9sf!X^zS}t?K#B@@D<6Di})E_y^(YUTUdHQY%F_@ZG74# zn5_M#T>Mc)mjIJ^09JT3OV=Xj@k93t7R=YO7M7d1I|+$?G7@+KJeEi_n3mDuB2g!*EAKHS>+ z+f^EY8bwr>%k)ljR}fjugltWDF^IjX%~gSBha!OuKYE0%@7CXuwPGldH@m#=3D|NT z@koETqwI>1W(KQl?`oy`WIjiK@ulL-9iNR%NwR54p>zGs-9}}1+DxRMde;goLDED` zfH-%cL;SP(K6XL;`=`)AZ(iPVn+ofKQJ`iAj_^w}R z@6lw%FVjNVMm>*o1*l@tmZvpA$Vhcl7vb{o8)d#P50TD342N#v%Rgs*9e^cbhj4#R zV;GV##J@eiNCag+^YHM1gdG0}pDewleAnwaAdTwrCnCOWp{5xW7W>|?eohjR5AsOf zKh$Z?aZr_0%Nj8I=HcXPGF6re>kawoYAhTcdm{Cko-w%|PmqA|invYcoWro2LnC%# zg6c0m($EP@_3fJXgve*I-4huu^Zh+t70stX?8@DLwUOyBD1J$(v0`8Z%ZMk$eODsa zaf09SJNZ6#TxU#~uijzl@aGX>+nJZX_%uDFpnp=52ebFcMr`!jt;*%w0riM*zSD{7 z+h$3WQbUvm=-huYvKsRwyI8>3yI;x|pR3snmhp^S=}MBk?E2W_V3^42jl;YP8*4YK zFw_0NZad7+I*=XB*mG5>T1Z^v$71)tIq#7ZD9f|>NK0YKdw&4yim&dha)@>98Vm4l z`KVKH-6V#%bU>=%4w+W^&LC|qW_$0Tk`TN!1W9^;y z0$ALV@1_7UqH*hK){5zxK{v@j+tU_m(qnpic5#>HD2gF34#XtUVUvy+n4!3ZvXzEv& z@o+{dG?)>Qjk>`QaA8_r`bO-UU6B9}EAQ?P|GTu=qs^1jJl@2_(2a?!1V-XV`4T|f z1X-++yA{_a3WzmPrxTJ(!AClqA3y$0_9h6!dxwj};@JF9%MYW_{=x}SV2 z8;Azel6euZkU~h*8OX|JJkHVg_jQbX8)<2t9>< z@g$j{TU7izQF1-~0G#FkDQ^ihvMsmUf+HouxEMarYu}!d&C9Sx^}atb34<4RV!$^e^5=n_b9FPDG3X2Y9P(Mw518=DaG;6(miO(fw0n3m{D4uwDBlujRGmY_c zfu&xSoT@spd1vT}=?%8vL-D?nC2WyAb)YD*_8KAvQZzjW!*0Rjc&Q&z6%7GDg^s^Z z#MYNW8m0)j-;2$`%9g_RN;%P)ksmEC;t7@1i6>=CwG31wYXSID$AKBMus-KmZrA=C zaVJrY7Y#z@|0OSsc#>9cVVS*y9ajio?9HoctFOZJk>c@Cg)(seDtC<4OR0rxZ@||8 zn%15fH;OX{-vEl?=35r<@@*C| zav@C_G2ki?yzI~&714&YN9DDEUg6w4c%M0( zqH^v;uIIiUwEi8l`GM&H%dSf?|JLSKN!8%V+v0C2(B6BIB?3t3zj$E7KUQ{SswYGN z{0wlkJb&+~a0-#sm;8$RUVM6Q=re{)eEH@!i#DRvVh9TuzWiszqJ9!%8G86R^WFfUyz!v#bn}_+7ukCy2+B z;?^yqWl{xyU9b^V^um60D)vpS2@&t^%$`60Tom$=53oibf6w0Z9>3swHc8dKk+4Yn zO0Pwuq^c6E33S@+HA`5A2=9xn@Sh)k3DN5Qs&rH0aBwXT&;0(adrrvy*!#cH-#O&D*VbT8f8KRia84(2tG(&~@h!vsxbj3c9?A3`7peBu;^4*WXx)b% z9M%Ihg9Gf56#!0)OWDK@%p<#}55!pk4jz+Kjuw{vdCg4>Qn}rC}8vQRz+&dm-&7PP>;m#_bg1Zy~*Ej<-nLse5Tyv`4P+%}7DK3#AQ_ zy?BC-MB;;}9JRKqNs(>3x7}r~ZsjgR7QV%UR7z@?0TcJ=YJu(8g|3( z6`RGWgJxukjk&F`HT9DU8=>U6)OdrK8h>52O*Q#~O%YHjatXoX)fz zAx1EUg&)F!c=X-;@o;GJS#n?NbKRv&NZvG$FyazPiROFtDLX*tiH2tRs?TMy>@7ub zdIlliR>#9W4zu>D+NgD1iHGJh1$S58e$V>;ug;N{)Z1?!jpLC(YHCfbwB8cg7fX8I zng5pN|`#{W$;xgrwkn zOQYQHsN$IsIz5fH&Er$iYJWh)1;$I;2^yzs?(!j4#B0U%YfgrBlvGeNiZEYqWVH9C>AS z1vVo+ea|@ssGppCT9t!az}Vo*hcYDctx$2jy^ac*FvWS#o3gLYvw!W#RV)aVR(;cE zqHtT*hzi9>C_=x@#LH5XEKK!1w;Vnb?m2TsxP$#Z82zGM(nrG^4a8{|nwzLR&}1P8SS3R7@`e)_AMm=n%@aqaI*tpPXM zg`EFf(4<^NYS|y23D>;3CZg+Hkgbb5nB(@ujKZb=7v2)~=RVl^oLP0SXURONcb0na zy_o*rxN@g&`pkR4E47eYM&Otz$!Jl8g`>+WCu0>vZnB{ijR#+r5Plh@P|~T4w*uvZ zxbE3!2XIHC#N@b8fEx4d^#*Dyj;yBmPY5B$UrJY-F=UJD2VpQj+@Ca1`tlB&zx~Mk z)iiGXytH$MY~X6JrgxH9&&w9B60=7)qBVNm)Xy&+S!=<<{Q-%f^5@c6@%hrfY=dQ<0sYfMosRX-OKYsok z8=DpeZ+T-*C-jsmhn)a7q?3D11DQ>Ww@6vUqdB`ahbHM2X0#%@ zmP`4G%k-Fv9CjW~&dijr?VOSkTge}c66*>^CT^>d;dkNnSG=pJ7(_8g^`83ccMM;9 zW{v`-);LU8-Eh)fy@6Fr5j+yTAtoxy?6kA(_nd#L^{RowltxZZSB;}QTqIAH6x8%2qhS?fH1o;I5L=3|HrB=xQTo=jIydW4q3Z4dv>54z5{ zEu6daP14+ST{7@;aI(lzqIDzn_U)4Ic@K*v32(6ILQ))bpSHx$sWe|HKWDp}K9(B% zF$6pn-*&%FgglmP|3{~Y9wfDp$(_!BvsDzs^Cd#)da-DFZEHX-qzh{SWyYWd6JwGjG5Sp2!`RDHC`+AVC>x@-}7IN zW2vEd(R|R|o4qgjR#(&!uPI~Agk~6qQbX{H7Mdrk?`^6|FvM2dt>btqM_0K82NK4;^M;+Km7$&6lGXUc%F*G1OjXF}N$Taj-7g z#B7y8H7QzcV(b)?(f`+p)kGNC9dNbn0;a0gHVWh9xB>X{;fEZ%Xw?uB`#-vl^&lfP zw!Qx}(Egj{6vM6RsN*bsM0d<+VI9@rLMc>Z1kujFL3|5p{mp_Ar+#2y9M<8{1x%s= z|9-u4Fz&II(P|EJuKn`m%LDk%NNPxgAHsaEK&NoZqYKlskF%)7L@}vYgWkjChtgWa zIKhuIg>ddt$SVlAQ4YxikuGqZBh~V8LF7Nv>P5&F0c)@jBk{q8W(S9s(TxGQo>r^{uD5GY1WzdSDZ23uZi_;E zYoj=a3iFFYkxo_c*f&%NS(;7@1qPk+<7f>@CbGryAe#8bHo?gu~a#X3ySLskX5Oo){mDvR&4b^eY3 zJ+3SfA-Q`}F=+CPK&JDubYLyWr6!(>%uz=~e3XNgsr6me?lQ&ufpt((?t}8xI7`Kl zfDttt>Pw@gfqds+N|pFy2aFM~&-9m!rEu2GW2DNK^$Avil!&|8rEbbP=YC-5{Ti%= z7g>&fn1dpW-lemrbz&`;cgaXO-^?(0jm+ZxY>`sr@QbG2oTwMd2U5U>HuCNg(1y53r^7M$Su8%Lz_uSXm{A+L* z+*EkI|MleVVU@~x(sGpd&qnd}-Cf=ZxPH$E$#8Sr;bWS~i?0I#fI~$(d$du1apAEV z3ZyeU0s=dt%R8TV@)Q9xS)-a7F<%Z&R8>?}CBg4&J%|9((AFNY?wqpA995|ML9R)} zbNqqcuB0eG-*$vKG$qIeS{JQcTvCZ?QVxLT(bl~QDL&*L4oe1=bG*-mSQ{L>Q$5&e^R~z=2-9lpGCswT% zQ;#*atRSk`n~A#512QD>#laO{YFxzCBj7q29%fU|xBqXm)4S9Niu>9}x0Z?EZ-Yp@#>$D=rwlWS}g zH4s(?kIEE78uB=2pix99Aw(i98l*^>_-Jh}q%9ptcgPhgD}aduU^uY+jkotedP5&^6|pDHz;A-56mqgpHSPIUDIddL zX8crTMS(@yF4Kx5ICzw!f(25tms8BU#gkx;xbznXXNfkEn2{M7q*kE{p1^T{SPBn$ zZeTD)*zE~4c!+=QIPM$$(ivEacbNQ=J>v_e^}(N+V-rpWDS(i-FGUSX;{6oCY8KR1 zX`*`ncngeh0-FD(95*3G(?Z6bMXdq%|I*QvFNyDs56%>yiZT@4%@`PfDxQb-&_fiev zHUqmgfCCn-r9O_t%oSolsP;4XG^H&AqnwsTAl_`20EwOKdQLGt0*0jvbo^IwpsUzyF+EX0&-sZb$)OAj)~#u&28XU? ztY^SFE6<1~>O>8i0FVwbWdsi2A37qnm;vYFAGMgDGl_viSK2SN93dD&Sc^ zDE!>cvku4IyV%g+7TqxPshlNe1P4BA_Y36YC_9 z&+n@N#1f>1hVMu+T{URE`UJXOYpLw?lH$t(=XTBrZi{pPG;Q@=v$Y=POA^O2$G0i> zedJU9pPvr|yY!tz!8`L`5eID4=fRX7BZrUv`T|2N{eZfKfi1fP5zg6wYJL`VKy5{V z-&gP*(uKO^B;;MOGfn{K@eHp_$L&(O>A^L<@biR(`)@z;C1}ksa;a;oc!2M^pb2v+ z58Nix3m2Vp5%P0I;4nU_tgznHO_2R6iKnS9g$vl z74F>Z5{%&HxgxwuT2h}p&;Tg~vU|#GBqx#eWcj+E$shS>kej$xycjyaq%LKF*YENp z2w&;8_^6>)+(Ai!7iM=O-;BTW@vp7Vd@FVEvZEIbayMH-na>r66poPn$x zvBnu(B9MQl9t7wt#6AgB8>XegGF!E*OJ4f~JS@WFF~0QjoQ9(<;f^aV0HIw(L~k#F zaZv^;Pm)+uooF*gpFXot{5&ohz@soeG4VAEtIW0*bP5s!97yBr=wPw^?%!qq9tWUf zkGt;?5>x79fmkZDxst@^$y%Fpmf{MsF1ELZGXc54GUx+ekwIm_Y(#a5u8K66;(Y9y z`6>69+aY)L5 z8NKpZy}>n(_b})5Yd=li;};Z*b}4w5K1KPwrO>{pRp)0#I@CC|<%ka`YfSVC4lctv8wU!LMgQ z$2TdSfSG6DJCR*4j{n0wH@gZKD8C8qVwp~0By&f}AwxlJjT`R*uL$k;!hBw7AgX4; z!9Dn4=$9|UU#-78bc-bx#|%FcbQE`-BjwvMP`2L|K)nuV9^ne zz3(feodXt%O$Ao`v>Uix#w#as{fYe7NNT94*2E;eQ@!hpr~M|gni1RJ%)F`6ZoGJ* zNkLK4HH;M7>E?o<3NMrFWJC|An?ge^**VQ`=z~)N1}ow&O!(1E2!57v=5TvTZ~?l| zEyn~J0W-hBRHt92r%KW7r$eK?h7G>P+IGYzfoC#brq8HNvyi(GLXEt7>7nxqc^DaU zSi|Gq?{q20ouTFxgAOr0}EyF$hql(|SU>%v2*R#mm&d@bzW zX;Jr;T%3p)(iBWBqnnd;CHTBy{*FloE7MHF^A-a#dO(e-KsIc(>N${%2{^KKrZ3Y5 zW$1;=Zu&2w6z%0^bThQh*nQ>B-^*ZK>JKuu%mWba5BMR(T4sD(Str#?dq;WHJ`+iQ z_b7AZz_OL92n$c2A1sM{(9l1 zB{oBHGQg^<0H~&u!*F2fMipjlWzkV;|1Dg;j?>$~P*F1a{(}W8)gPdyt{7wi@i^Ru zUtG$w=@r`O>4fdGZAOKgKF?=6!oE8v<{f)cA9R8%k{21(fLx;3oG=!20x)rK4v2Xtzp z_cJp$ZV9e&s;~!eP-JTW9ellU{BTMmhJBI5>IJEs^+RktU0gLw-q6)CT2WE4gUlz0 z2S8or%BLqtcDyhvD>J>vx9^R#H_dXan=Com+dkG-CFXyz5sg^k1ImB!BmQ{k4|a~Z znbJe-&wyr5ZoBA{*#oUREHPh!cKaQQFha~MeN+DiqG9Y~mp*v~H!21--Dgs>`8Y4uOp+k>x1!Vk-06q9+2D6Yz{06I2)^jAWwEiM#i&obX9z4%?oyx7|-jGx1221 zgY+?(B4d%!dL*=`yOBmQxJbXP*%18dZQoPrhB}*=U7iqYeOfSa0A+3mH4XK`4jHNA z=ZDs#pMbL+Zk^?R7lF4P(ymzjM!q!}wvZq;{|`Z!(LqB)&34b@4wUir>(Q$4c`ZL| zMH2Wb$?zD~c?-JLe(AvXTrIL7I?Y-e`~ z$uIJgkKD@jvAV^;>K7XjJNL9C+%GEKjeRH;|u?5GX`zSJxQ2nYt&u;*Y*^& ziT9-Z*QV&o{ofqYpp7^Gzi|&uU;S^K0BEDn|8Lww-BS3jHrS&v3 zszTn_po%XoH7-yr5n3ztJlyM~gkv)A;v7rN_l)%a4XXtiPi^7=<`JZf&;O{IgaIdq z?Q&Lw>S2XAv~#oaGT@#h^Ddn$WRR=dxLs_wKPIe_vPd`m6#A!*nRWYbN>6i?d3Qg4 z42umR+c;P@y56S?ivmGN?;pENjb%6!?Y41&)$o>v^On|jpuRhRWd`4QnNluJ=^blO z2pG!6R+%Dk?y0J-Uhh30M5c$;egDZUEaIYkQ5!AfI~KgTS7Q2O#mU2FLlqT40U6>a#=Tqvq^Bh{q0xIyIDU<%fW}-&A$DCzR#(x&Eq`GJEvqDf7dMG4j~e z#OGGlZ#=p-wNDX03$Z`83&hDnqSX}^d((U41}a6VKepfchFGuj{l=8zd8~0rFoOJJ z`IoVe?PMNtRG?W>kP}^wxG@3;x=SaHrG5MTII|$T5up;P*i#g=KT%~yIM|24N z%BLOWR#dz5HabP`Y%cS7}8cf|pT z!E3iflPVkzQ=wlASw~NI1$XB{I>R1Y+1N}bIXc%c3kd;Qu#B=y3c+2O%Ts-X8J+@7 znAYhpeb-51l^$JFuYcocZ1`mI$8}3k=hdIW^{M2|Leyh6z1VK`W5wvymdtiVV;u-e zM50`m0R+NHS%1b_at~bkB5PdA>9+4ROa7W>baZQOq)3GMy$ob!%-gtT8SR@ZbD`P{f&e&g$H#ef%h3V}9wSeAI@ zpVGnoNU-v z5^r_=k30g@39;F^ZruMmSJ=$=ArZwZLf);~`DS)_D(vMXzg1YflDfLOyw+tIPRc+( z2ArazOZ!8XHEWAIAt-~~x9~a&g6BUe>V*T_{xw_?j+jbc4Q#zvj!^5TF#~;aj%p8F!)&;Zx27VwGNPur10gSTh=LdD?>N;Q zTD>EmgZv&=>peJ1clRtf(vpbCRI+C8eqgg14`z?dt1|Yzr<+;ws8f{hD%p%@$906n zD)&{Sj2JiZvLIdL;-r4DB*{eFyejhMZ8WIe@^juNPYd)=Z$3p+xonnZ*y72%71j9N zcDjmh8={IjW%02Qa}s$HvO1@(?)t`-#x6X%R_IW?V`PsgY*dFcwL z(|TUUc&V6ESO$#O#Q(3e?e5uk^Dh7_3hg^mA3Rqk2tm}#E0GNv`g&;WO;^g@Jj>m} z7uT#sbbM@F^0TQJtxiiJ$3{mfLT*G8<57I4z@)lAenHpN=Sqxhf)Xhe$_EZ~e&{-O z_c2N?6Nhm%ljLNV2w?}6MiY9un~)y5hI2{_^jBi|?o&bP-VV&z+Bdz9#A~>?FjSt! z=(`ApgOy2$th?iB)LNja{g#wv3O$uvg~zT7F5KVz()T$}x;hwXCZVYjNlI**Ai)`S zN3Em+nDbI~aj;Yy`2(&?K0$&{QSkA_=IDWe_#HHNRy{#d&>*fx%{|YV_$ke}sAwji z%gXUe91dM@q;d2``$vHM?%9Uv&5rg^w>Lvm(ljN&< zGBix-92LmnLme~(-DMOmHuFb)Yg<-)V0yt&<@omqZ~c42LB6uGnD53#Fok9AwJBj( z0JRFSpin=&g^((+I&tj5vroT+Z0X1qgxn+Q$*@mWDk&E#K};cK-I2H&r;pal(zD|e0V@2S;RI(IQRsWtlpXb65kBf) zcKur7ced!32wn^Uhg0Bc-lW0N5p_mCf4_gBTEUiH1n}*A@n*`Ov_-2|1end$mvDb+ zh0M?Ij0`!3Az5*soaAprMpe6ggU?FG#!l@~l&M6yucwix%rH21UgOYV>^-`zb}h&D z0o8A2P*ZA`JwFLFu?t?klJWq?s!oJT)KD#5;b_Qa83;Sg;D9`HyDvjQH{8Y~WOBvp|9bT-I#LHhF#O$4o zA62fs#_9=K*lI;KFy|Ls4u9V?*xSh+q6haPwubMrdQEL3J@KmMYg~H9GBl zDHgll(8KC0=FT_+6nC|h9{dpd`&8=8j^gS)!Q-^8)4C?;-$Q)J>lDCUjSm)psoK3J zBg_2y)h>DF{UzGQ%^J4O!LfmS7HR7b9M-?@LpOhUZ43sDCp$zd@`9@ycJ1fIPZ&k& z*0lu)=5b@~aPDQ5$S$8?K`8;G8DHD$F(y(W-0xeNpPLIJlacy2)Lie4Zq9Y1L9z@Z zG;BZDxU9U{s1;wfX>k8HJD$AV{WyPZM_oCchgUx?R$JrjGhA`uE=8>taxoI6FG!8t zy@a_R9h$eKxqpJ1pP*Vc0EX5~(dl_O;6*?pB1OiGzg7 zeQ@uY{JiY-dXn;p>^S|sUCM4Q4!W`VlEqN0!6POJa(DOI8$hmQ72=%P{uj=$_u9*4 zWXv{0uLIc*u=Xipk?_^1h0cZpkp7RxJ`rYyFuvu6h`YoR>neOBLAIOXE z!7E;YUnHA3yXw+d|TO z(Dm1zTiP3hvp-X3<)fiGNde&|)D%_ok2PE+<#rnA=4aztcBhVxU_tmVQ!G7~&5qp+ zFLPW70+nhl-3=)*s97?g>vB7;7Njhj=4HvI+rwVK8o4tT9r^6fwQjEa?#=FItZUQF(>gNJRMh8@(b0D& zuLEa)-}-Y(xWD|L8FO+*$eK?BiYVAPa^Me$M)@8MP1SQG-26S78hO!41K}rY{s?Hx zbJ1>gWH{80Ks+K>IiWMrhrM)gQ1Dhhux(!b)Q*(Se#O-i16W-Jte%a8Ob^a1p{@hX z_W2+iN{U#Hh=Xcxs+`{}_X9f9+V`4J<{$AhZ!HobvHSB3Of{&BE+Z*o_JX=^kRR-( zI(%H&A1Zc(r2mZjUIHtA+)jX+)le$DqvPpKk?Reg>uYGRitE=0%Ro%UlgG?&bJssE`2S84v3m z9d8+2%I4TM+zRG9m8BQ{-J{&uby&Yt%vpDrCsGxIjBb>%kssHxbChiY*TQ^e8({1S_5i1`MF>2(D`f4ij!%;giD=BEe&xCM6FxGCwc!s_C{>NLdPD@ngl*{ z2G*VQ?8Wu8OV$B67E9|Wnr}AtWKw)MZWnTi9=HV$i|~7n;~SObA*4YKKdIdUdJyHH^a1t+safc*)M)k5&g8}U0CH9p z-FWWLx8PSoxQniD&$r3*laHLDi5#2LWimF*mW&%IgHjYVE2=mEIH89J^#kW8_Sb%N zrM?@@VzN%SDSJR2*MT=FBfdYguz&yTj|_cev0;(q2W{}&Bh9}Tp+GhGk5+i$iCY5- z*PcyoP=GYagzpCW9eQ`92zkVeT1GWb_sB=oaf2D;fil4>$q)92Pg(RJpR<=sV&AzR zS$*D>5_Mja1e)9vTBuD#`5rAlZ#g$1ot>d|{x>x?D1Z^?z~wlczn8UK)Ye;>o?L6M zpR=nZab5=u`R8#|!6wTC>Z(Xk*d!Hidh9&=*?s>7Y(4FaW`z(2-hWiccLCxfWl6EqUcO`+RR33f zX2zFM(jExJfXUrOpp5BTJvnqL=aclC?wWeuk~}Z?4^4b;$D}(V=UFc#@E5|{k=1_a z6WBE~qhBeFb5naDbc*)3=3ztAk>&XV=VU6#KkdBQj*#lUiEH-w&y;vyOD(yr+{jWtx(%bk8MRWb0kc)Uv5T}Xu1#E2T? zU`2$>;_xwK_4aXU(}{EAJ(qtsA&8~`^Zx?)&^c!S0C{`-CkBQS$D{~*+r9OPn~`JJ zg}P+KKIPCaOiX&F z;wG4Mo~i)6MXDD6B<&6)@@h;y0tDc0jWNd{dnpe)rNgqO0T?tVN%C+a8$H)hMKd_Q zD2u#AyGsPEV4sbV7n(b1>g?DwxU$03sgYpU*o7!~)oj=KT@3Qm!hy|x?iRn8>N_4b zZ@IW7aDDo^@6<`LQ4!cu;tJmxoj#z3bCY1jw=*JI46LAW9bMgepJEpeSQ1DxN_Y$| zi{S7RELTn(+d7fhqg&;lwi19v*XTL><+|eZD~^V<_jl z(P!FxgVV_qk|O~MKkQ&5AW-V-=Gv0q^UGuH$b6HFz5XIYSR@k)o6@(4{5X@m#TaC8 zH>0vDqZ_GbV{tRStz4M7=*%8(2yFO5#|{yo%rbIjXMc!_(EqG{M<~M>0Ib#pqg4lf zS^yKS_XKbM3w={-oC%d4bw~eJ^RP zYG%9&I{>w62i#k(XJqR>?`zzsB2u1Ow|i}2{#<(0Wh{2Rs>~JM{_%$ku9KPC*=_=9SYMRgMaLE4*dnhiXFf^; zUboy4rHv5c&Q<~E(3As*t1M!5Qd;I*H){=oY*_I|F+7g|D_)xbo-5Yt>UO$7zVuau zZ!2d?yutwh(#Kd|vtpN>&Wnj7;q`mfQSB_>1~qAt^>{F6O5J~Rf`#?%b4DlSv4y72 z?3$`dG1-)`RWC8z++$}O$eW&^HT|em^v^OVF%LZPO7)@}e_t_)XXL%!@9l;Tb|n2(v#mXB@dKabes zjUXpq;EOG|xtk1}sVt4{pZ-ufH8;&Td%JTVb@^M05`(q^o~+$rjqMJ!_L2N)X1#fy8DSisT@3No#UJ; z*{Bq3=pPH7d*5)NQ(ZOI#gX(fO14(+G!YK$F<@oPCA5x3p8vg`DC~k6X|CL@5bt^| zjt{K^+av;poqhMFtyJDx@`6( z^om)>`ppQ|8d(PV)!efSwPr|J+-)#5hh+UzE#%{Zl!>qT5Sx7eJ#6K(`=+pkgSd>= z*?phrlv0;R*ZoGUa+(XC- zp*VKmxtd8)OBnaH@IAgE4Lt!teo>rwB(NGe| z5=k_dA)+p8c*^s}>sf){7iIrJ(Mqzdkau*3Q0JfNqYH6+1d;zjKz7eA)3QA#oYv_( zU%IqKBMVW*CyFT%L)}f9q)j6he_;6B=|bAhJ0MB=yy6TNbpc=95&1ocF+K2SV{T>T z%m&LwGjBkLHCT49K&Vp6w4a zb$A3K_aSnS+kD8S3)&`mu)5QGA3G`?sp=UAk$nKVmCP2=+s?>aD|_ZNGYKFpit~hq zBeU0RK6F8@aR~3~%G_X|msB=VQ@gXm3^zKl`Vsa+RwFRIvLgP$YxQA(P5BylO}g4% zq*=eB^o8KDkV0OGV{Rr1&_BF-m-jxzsHy#sjTR)^2nv2*J>q^oiY&F4Z0#;{o7ekv z*T1zs#^$v}|Be!GOFa0g+FyF7G{AEQetKYLdirBu`QN@9W9duqF0~3=Z;FX$#cx9f zoPtZWS-BA`V^-qyU7j-CxS_OcbigA(RwwZ%<+TfY%+ISp=-57%3^7O{g#JDjS16HD z6MeRFq5g5jEVKVLPZBj+fa0{r;Eg`}$>e$IKJ7F1d?L?Rn9Uwi31j64me=t^zh)F2 z1gsyJt~`kKUG0fX^O-}=a+j{%OC^pZhb0qf0hJ2|=8w?5?{!@-<->ORnUG#B1={F@ zizPCQqn?%Vt=C!_T*Cr3^KD^vrYiE;@%UtY;jg+PvR9_)wm&h-GXd77Y$~rX*qk;V z{+M}nV?7utM}Ypo)rNeD@`FrU(lI{2;rq{1cz_~No;yA3zDc`;j^SALf&XdU7wLSy zI2>G7q6zS>l0OM&T8k{!E1Bc?6M|)RmXSD=kO)tQ@Kw(LiQgj7yJvN)UkgF;1_Nv8 zs6MAJMUD0syH-=5x8B}RB)Ct}EA>c(o+B2WYm_Zm-bO9CpSk`nQ}(@qz%n?WiBCM$ ztF2MqV_Eo2GbCB&`|UR1ZMc#y4J07PD>?>PR3qja1m8T4fd=KkI5l7P)o4c z2=e5(_dy&ly`8ckEW#c|Y1j(_^TfMe zDtE7VS{`{f6e`EF%ggQ~IZU&(zc`5qp*tpZkZ;?j8ah2bP$iaN9~=4HG{ezQxsf;@ zV2AElOf>E%xVWdYMdhds3D@ev+ggM?AYf76Z{MxmO^3@)U06E8|Rui(kA^ z07RU#cF@ub>n0!N1ptNb1!UbMzm-Xvr{b$vbX%iWG}8UE+P4(scf5*|k8}eGaVV#* z=D5!L1$CQP)Vl5|#rn-;eG0Tj*;mSb+4Ll0lK95{2oFT664Kx2E)6zy53s;7xV39N z=gAfKKEg;?r!cL$3Nwh64**&nc$bqK#uzLZTgtwSu6rUmp5t6_i;q7GBEG7k84MJK zQn9FmE$&PF(o~Zj*SptqI;3=cY^ue8v^tDycg39IJ2E0+*W$IrzZfFc5|vMJI;pKE zfv{u?VuK(1aZ5&8ojreFohqXKW-8Ns34B-ARLs5#i`(UZE5jbTo;eg}^q}LM?LL0?xqg@kKz! zS6TE3RpqV&uT~ zRQ=JynSuOs+rXV2>ZSb^@7^Lk7;wz2SEHbZf(L{!2hGnJj9E#({kV32TiF`V&=~B! zc^Z$HI8QSJiLIdC5>4PL!iuLH4>!+)gmA&VLKuIs49ApMz&;f(;lXD7#6L&Ot#7>~ zHK=}C%UyENAHl+PP)`J4w_Cdy$Dz)8G*8u=`L&I`er80b`}Bk*6(d-*Iaq$^T0irv z6i}yr%LHT)c41<4im!cba?jaYd`*lr;$b4B$q_5kf$jt)?!=*DQD<90plbS)p~9I1 zLnDQ42THG^iZ}YCu(VMQdTG8IG2r=DC#gZzGxstDa4$TBDSdgt7Oe*-P`48~emL$$ zWmp);pw3=sxrp0rI2!~SHzA?hEiYnPvpREfO#mZ=2^Gu8%YhxfJ3|SJlu~vRU)DF3 z&e#lp;a&f~&6?XL%@axY#hXP6B5msKS{oC1y9+YPw^`JDN>P)5b-G5}w?QE_PFs5S znb|hVbXQGu&AA#`V@~Y4FDOp~35qJFAn^-mO1hoaHj3YD2)6vqL<^{J9QWP_&HGnf zh@T@7Bm;m?&)Dw7KroEK$@QVDOS>yjQK-c)PqQ#Jmf}UQ9WEdpJ3B2}4UE?O#wZsU0xni?;l~fa;oqD9rX%S;eCQr( z@hQ*z%Cb_+>;F}VMilCQW2%D3rV$^LkIGbaa(rw?4!krc6_Bcqw4@ZzAwk>W4D>SU zlwGH&F$jjm(aTa-yM8GUFgfs(RxN^5hhJAMM^7EO7u=k7p9HlULe`e>-c)BmX$gt- zOAO!qTf8|>Lj$JD?9X(|et6^Jlo2Ydh2yfST;zOC{NisnDc7AhIDSP!mT)23+!$n&ND1j= zp4j|tVzk*%6by=0&RTJTWB8UqK(NOQJ4(&Z4h4#qd_y9Hb%shPe{UfLME$AyA34d7 zW1%Wi($q}oR>AX@3=<@6MSn2@rCf9ImdYU z7UBnnjcPGf8=YMfvIU);!ZSEQd9LRSxN!M|MxP5`iQLWHAF!|Yer~h*8yVA~gD=#}IO;P42Nr$@MToa@91NOkpfZ=H2+SQKi#DKliqLQ6*pnaNuIqbRyx99jZKjH9!l;BGxDcp`8TmAIcXx)e_3dgJE% zM(|F)4#Kc((PRG45Pm&_n_89bHCiw|tl#?*Kqrdy5`^^K+}&h^{T`#I}aK z{0F(Qx@MrCne*@jGe+@rajs?%*qqE^wH9XiB&qT;IdoB`@>7{7D3O}lbC}}#cZ5eN zUo<{uvJzs8RxMa)?3$!`lL*g9jE{|NbW!03E7!5QIwT=nCPWNdvrI3p#@Gkr4bw)% z^CtdEdU(RiDiBkP{WqaJaLN3!v3e)Zd&`KEsvo2itWXK$xztNZ&oQpoM|Qrkf7m#b zr8L++(bi?y+D6yJ<#_tFCdr#~M;(zg?qwa)cWLIk{hveqo%n}m^(Po7!)}ZmDt`O3 zJ*b5Cp5SVx>|}dnn`pkVjtJ4jcIt)d1NBM1E&FYz_;O#LXwS_!7h(Dex!*wKv5kDW z{kysi1ASFR*=w8Zw(8thWHVFs5Eh(p!=xj?+Hv1Vs9bayc^aj_+K_+qcqqwSYXCA} z8va6lZqM3&B?li^DYE$J)vten4e(=oLVera7t}uLpRa6jtMay}Fq*0afP9V9uC zwFSJ?scKk z$(;AuKN1u|GUq*K-kHSR`Tm!Dp7(j4bDs0wp7%09o;-Q-3{+jDK(31sfvws~S5(z#aGu z1OosVFdHiSu+<;AH-|Q}OG^tCDqXSVV3?O}TUlm}5>MPyGP0ySn?|x4khZgt8dBJ5 z0xr*{og7l9DEzjP3irNj8OT~d0{+-^M)W5Dh1s$(Ktw?NpS@M?4cSnhl|CfskIiR9 z>+ydt7ywWJrmPM6qgiQ!r3WO~7$`?7@JI2rGjPk#EaZEVaoB`lIuRuo5{O zy$vu#VBaXMbiN=mn*fyBY#_GZ~4Gkxb;N+40Xg+%!LH}7KVqwH%5pYerNPgnfIk@Sc?#`Aw zy$LA_zh1`ZE8~DPHm7-RVQLrXAptXgpCTRj&$KN<@a=k>P-}HAQF3 zTHq*PJ22z(ghewr#t-kV8CI1rua?6uxERnL zXu^>X-j(MR!&idApztRlUPBVmuK{qH@u@HNY<*qYmuOW9Q^ybzY7Vv6QLjxSxoVNWyZ zo-l$i;S#a_r0H}$0*w;)XA=qM`h)_~+)VDCy|z*~bM!+*Lz#`NE-fcKI9lsupiS_o)G$=g#yxZBtRM`ju>CZIoDx-_lp|FRGCjaRultsYV`Y) zK;vo26Gblc4n_NrsTufk0iD4XbOu|5<*h2=#?idr4%-Y9N75&wuymZR`!I83Jun72 z0qM!djLF!e9?pDxP)P3l4tvIUO50(ZL8LEfIB`U#$#EjNE#1d#Ym5&EgIkVIz?u4w zbY-fB0AHD;UC>8xA~XdPD0Eric8WzO97`b-O~L22gPF*MHlF<0aG5l?ThGL*0Gv%H zr2Ax~vTAU7i$yI0!^ts2FYK^2PWbQjCve;!fkkClFl{(S|3gt)9fwYZ@Y1GJNNI#i zD+_SX%n?u&V?k;4p9T?uF-eJSE31Z1Ky;fd#N@Y&{S_O6&fRjx|O;DQ|>SR}zuVHpAumL*aasu8f`lK`E2t4iN4LA`y!kF6VZt zIR7Ip6lk)}mthRT&0KHVT1>6P_6dhlswX2bW+7?)N2~v|sd5!14$Qu`3Z=z1j2h;~ ztZRL`WoBLD!>D0ylos1C=ekN%l$blZ*e)?3V5Ga(S^u+PtPqkm9CWssAJ^B8^PqN| zM_7J{+lsqqj4*BQ9PO=58SD5IrW+7}CQDuoiArpbbA{B!Gl3eC(@oJ43Hz*aP29k4 zNg3#mvcST!jAfco90(FIi5ET)(~Yiv4Iz^Di9|z#=E4MFqy_}~?^2-2l2=2LiMT9P zIU|{f$x|kyhA?pPOrQ>0Dk&rv)v|xPMobes#=N}5fLDZ1H8{ThNlyYJ1F5@$LQP#X4K`7-ClD%Y< z#M?hC`tMRVAf77c830oH5z&AEz#q1Rv8z5I!|Vm@IiAFdtzwT;T~mvh^B2l89V#t# z=}!CgYe@I4IQltr0Y;3e5tbz)EM3+BOWcu@%y(r@WR%R z&>^7enp)g^=VIw6tVmVm1*A(U*akps%qoDlVi*=Y{1nDcxJX4ha3mud5}_0lp%ju4t-DzpT)_aqUKEuS@3pM1)c~?GIY`%pxkdGm*;jeJGrfvK3cxpK;9~|mm=pF_dN$3nG z(vGqKfGG?`g$}TK0Ke~FHdQkYkssU-ZB*vVy?yYWx6)%; z*AOCNt^U|<28icD`ZFGipzxt3u=*_W=`8iarOD5yXz@t6(uwIkypm>3InYM^X4MPeA?H#3`k}o%fO%U@x@5U1o#3%m%yL@@buq6Yw|V>Me)s zQMm94uxKVLh4g~iTd_?KF*L@%2L|K27Kq?}^cPSMA2VmT1Hj7ya;pKD{Ln22kD}nm z{{?dyP1eCJ_<=dg83d%F!rcVWBSy@fVB+0tsZx;aFyY$?TEro5v)=W+2l*F9ri2Q7kz98W!w>l@5mFe#+ zNk_)h)ber`Ruukq=q&RpryF70DD`A(PO!hR8A?O5judq=g?}~EU;vV~l9p7s_Yq^a zq#I;9URe*B2Y+&g#$kUo?R_jskb0}f=hm5miXknLQ|fbWQW(Ag;Ns~n5p3(0v z69CkDb0?<;6Kee@H6#q;b3H!Ss%+TF+G0Sd&$&ra=mr3F>ZyionaLT-sYU=e%owkg zbi*phmQYiHV{y;!Qt|OZ2yjs?1sy}k}<6O~y|J~E}SKj=N(%Sx+ z_(8C9@RA}*%%1)m*V2XG$%Va~zMRnG&s|~PfAmk*_x4;q=;tH~LvqTbQKn_c+$ zH>GT)6=6WW*2qtuyWjTq&i}L=K9;qgx5$Lln9IVc>#tty{Mo&x?{GKEfHoiTMZbPf ztvmjNWzRw3>|HiVRgVm%t{n5UhXVqv{?fZ@-gd`*xV+0s*o&|9TiSumC z;wOxeuuTZ|d!J9r|aOr|O(KRaY>?A(3MBqnl<4Ro*BB^KqOB z2r(9@0#Oc90&N2M0f<)(!}!U!zW+$))uy|@5LcUk-mv5fP)cR4stpReo@QvUT5>mK^~fGSZ$TvdiVv*HIgNR*!fK~V;Wq_fQyzuz3?{F!vdNFjeJz1l;6wfg?>pb(OQL1)M_ zk8Svp6k-EC%}&8t^zyri?|D?%QKqQq(u9j2z?gB1q@aQkGXTh6o`(ow%;3QmAh%Y=$i*YxY-BtP(nqE@Gh7kXWT%G?EB#!t~v2T22tnV{njD0D|009|-J%YY4~D61q@iVu8ZNg_RRbYTo> zIcKwDYbyR00B!SQj}udq%{JxhXV^ zs-S61C9UVf?B05YL%S|unJLd~XS>a=?KZo%$I~80!ovQBX$6uVtT8`vhu{=={h6pJa$(}A+h;(#^B|Eo9lj%8&M$j;-BAFiE z5M=4ylUZ`dc=qo+&!JrxxOnbzQfd+~49WSkSDpYU3e1?DpKat1C{$D_bali#i)PEk zaE&lTA{HgC8ygEKLZmcJRgIq;ZyTHQF|SW!?&6WmT|AO-$l&CmOPoA%i3{gKTyBpd zWqz7O_(mXt4RM<72*f_>iMz{iPeYTSR)hl669h;j2qVP@^xLMeq=fIBg+eZ zHx#YrUw?Q6+k#_9EdJAkEw;&7_R zzzc^QqED5M!-lJPy~+9r+tLbY0WkXZ8 z0G>HzC)ISI%tLuiV@b&94~EqT@%rL4+YVWwLP9`&Q%XuyM=mZl4MlAUi4+bb8Pb?z z{J7YXs^o}qRYPiRL{S7yV=I&Ci!Iq>PovBGD@F0gk)kr>xvy=QoLD!9bP2H?IIzdy zqa&#xg5eFpVisQ%)HanRuRq*wwC~y;!*cPZu_>*S&0oKKeNoqbl8-cwuITppb;eB_PTh#Yr>3Pw{fKfV&#I%W(#zOM zshQuAgJv?4-mQv-%ZXYH6isEVUHPL5Q<^gp>G~0IUbnTTwgh-QDu4XY9Dn9icql(V$Ptsgf%y<*ciiiDyu#E~igRc*bNbMZ*ihK9XS z2;4L$W?N+yBF%%XU=>Ym6|IE?d@6D69?&Z)Qxeg2DVpV(9nz4{c`4<6c84%ExuirQ zZYx%iMPbO(Pi_iIm$Pm3xD>HfH7ZI^DTZOw(H1F2k%NNvwg{#XpB?&qB5hmUsBx)I zC*byCg$EUdA!^KC3V0ztez>h!Re@U9*0e)=E)=84LBZkODOwvwrq6#yk4>`&o1R^{ zX?`)vEDA#$Uo}ON*V7Z{KA=S|w z>3pde6NfMH>ZU!ko=lb?6kt?IhT0@Twgob`IWbMigwMe-tk)Ae7 zr>wm1&-wg zSRz(^_p2Kl`b$T@8DgURZ=f1L*MvQPNq(+$MG-9ggcqMT9%N?Q4oCN1>@OKb!_mDL zvF*6kT3)Fzb58zSCl-Cmhn`|wg{rWozjXAAAy2Q^_$j1(IFVlXNuL@l%l}iFE|_{< zDx_og*0WdFf|s!8{j;4=KXUbsv^sIPjn?z=gBuh@F#Wp17tZD`^rr197VyG%manhu zuOw>`TsI&YkXx2U8C+jvr{{ zd~+8NmyQKD^?zBpwn1gyjcM_I`-cd@_yb7dG_lYtAI}= z(o663X_-E&{@})&y~&Wx%Nr5bBJ(M?9YR>~Ek@6}b7+sZe`?xbfSM z{Dt+LCgQOq!+_ZBuIXu(Y-j zy3$?I+q0R-n}W`&lg&W#45@eDj1r9wV6H0R#8H!f`%#GfyHi0Bjw5;P<^6=aa_^uj z20~piUi+8*iBKK@_U$tGw;zQ#anu~p6uh(D=8a!E(t)FgO?GUF(R!ix@uFc=1%LeDY&5;N%f6QB@Usmc z($ajn*Jo;Kk+9^*ev`uojEp0mKp>ud;)MsE$d|r?7!r>xF*fHq!qwvu zOqi@Qc9O=(F`7Fl8Hh(N9@=NHb4!el_UwIu4MB~1#N}OKGZ`B+dSW%7`HLCod2CC_ z2c~KB%YWX-siW=5dozQc&Wn~)*%Hfk-&9s9%w6bZ+AKXQIHSGI;q(cMkB*p}Ic+7k z;H5$G`!dtG{gLlKlJ_!c!4vMMpWGA_-Ojg!kjsI(T+b+qpuSOM)HszXAVY^UM8=@WscGw^}>T>5~>GkDG+L`qXQuBw}4CB-gHb@xf?c z4`!m@>%gm4tBE*|N&Y7@6=>Vw?% z=hLaq5mlHEE?o@s+RF!MyO26YaiU*6e`j5FLuLz{vrKjv5l&nzcr58c@-tLr)8%2u z-&(b5Rll|e7iD37{qpsJu&&=O5zD0DZg;?ST44ev1-#6w@>8St(c>lj=3?iigAp?s zI;iU^^KTi$oW&#aj_tB-$p^o0X4`L1rY##!3Fz&&*H%{dRr^-cV!ILHSgX<3F1HpW zuL#L2ntJ)Sk3awTfM+5_i&}g3>pyICZ0kuRj{s?5?W#mht?-HKEB#bz`Hu%5w1*?_ zc1C=-hSsu5FLM@+Wb*7ff@QgVXyK5-;awN_a7%O2&HxY+dr_6&xV}1=w={QHHaj}H zITVUXCp)sF3&c9dQvUw0{?GkIiQX76L!SHErqPyTKZPW@l$(GmaHeREGQ(prtgI5< zU-@_uxn2NNs+btsCA5CGc{^s-lt+~+eyU}lke0SxG6I3bxJMcvyt*hz+ zXPRy+({&s7_Ha+298aLE;9^ZFdA~hm?7D17N6LcCX(5C$r`*GW>N1bs*V123NQ_7r zqq{3Pos zkj@N`qfFIodh3UZrsEA(pz4LYDY|XRp00?!|8g`Ia%7()e3>F5)60F{Srq}*-;a1= z+l&zlci~vsvo3&`6dW`a_L+)f$V_WRSVG93Sc3oc)Y=CN+RL6drQcb;zEacmzZOEQ zBts$s0%xYjQl@AQ`PN@^gpJBu+GQp)E&isx@0 zfBp+yIX{qrylTH@m4f58sImoUl}O8H&^8WHIKXz`6H{7arbzrSDv^+G|$4 zxD4dWxa^E_ft>5L#5m0vaJyY_1iy@=$8wGE>3ninkQz^Ej$*lF*t~Tq#F=QiI{J*? zqD~#y=|T>0+*0`|Id>51DFz+8{b43AyZ z1Ah;i3fl|~+to1@1hXo9EUXIPDbfM`hLpVD9%1+8D2{6urUCQ3Hq-Rtt;N_9cAF}@ z4b?4gF(JgVC!T-c_1@m=wWh0AZkR5l*aiduyh3t|&!RysO*BHp5$rHkj+yDw9#9n) zR|lCI^bSako#-~$+!i8arAxplO|sBqQb3GGK{#g%Hb*sLDHA3v;f5z)c<^AZ=W|wZ z?aH-YQU^@MjNzWbWV6Gvd8;kN>F#v( zR3Rh_y*4AX(jS1<<|w@0t-6>Z_6j|);E8AN>&|>OXTLCI`BU^TLjbpUtwCW1K%*+T zJ7BWFYoWP#ayDl2)3Y7C-4-Hth5r~qkL}VC;U~@QY0OXs%<u}dQXB;;0A_h?=6SBp_JRJYv8$z?u7qtdjeV)QbV|dR_~iE= zX>~u7MVcwaSLtDfPr&RV4s?Ygph#}=THN6`sdPzb#CG^i>t%j+zLQoXMPG+y^UC=y ze$mpE#*7Lfx!q@RtMBSD1LBhD@?2CZUi~YX&!mypvn?xm`0*@{ou)Rf78+H_vVh6; zUOUa+NvN@RE`@pLQkZ{k?w~oES`OBLd4sH;`hqSb)7{!eiu*IS!ljLn&#hWRxa{$XP>iR4B0DOg4MX z*7+a2`s-~!Plny6)2fi%1Yf5*_=eU$7lGc@SOP;uJ@p2^kkq+M2kB~a$T-PCz=Fhe z-5|}X`iq1b+t*`!2_)C)4%3Tlw3Y(3ibIQ?pi(Sw=VkgPl0N~i9O|_qu5Yi?ZD#24 z|E&%|fyjv}RYGDWE*a&{%VbD$>D4m?)m3k}2Kr7KysptsW-NE7cPg1$dL4Ryi2B1p z+CrHm?MS4n@HfZeA|?gfV{sXo@3nee7`s+*%`@bXrEtWG3wpVta@|$(DijOX$~f37 z+~LIqMWhW;h`Qw#78UIp#nPu*XpOz)-RB zoFnC>vi8JOcK1}sr^KygH{LU}WQD5z4#P6mOy2C)2}?L^Dtx}oOe?fo3Vz$I;Q%d; z!ZLrloBW)m@Lo*oc@}Q+7Pf_YTTJ6vk4mdh92zrddpoMp?g$QOvM)M;K45^?p+aE_kh?j4~Vy*QC? zBeHsVw?k6uLb<-}zV3%R%1v81CvEY24sq|Y8&L#yk2^W%yRPSRe>?ZHec{RyA|=vs zNbt_K91JJDCpeBq!VhklHinhi6H-AcX3WBH&BHCJkeFmkDYFjFw_K4%(#g)Z9YMkp z$wixVQa#i%9SkcYU+O8^4I4wGxNahF8vxm~sU(`vxdLs{apHeoxI$D-pA0dg7Dmj( z68@wt*``g*G18usT54UwG%R9HMl6$_3ZXKhCYmdD-w-NjJpqprV%_QAwiG~(Gt_Y; zF(W$3{dv@gnZ#tOGZ{jO=l39!HdE0-jmMYCT(l_) zsBwl`mP4p3ipQsc)_Y7^uPV!kn#8&dq$3Hq8ZeM!SWnB(u~EX`^NtF5GD2*8q#{=W zBFDQ|`1X~`cGDZk5#E!?7d^DN;gJrB5Cwm(db#cebF_<}^mdbuBo;9-qPc_bpbrUg zM~hE(q98y+GAgr&0dt%PW%#I)7F9BP;96vbIVC(aN)s(3=GH4Ei>weAgw8mb9Xwtq zEv~mCgix96Wap1}3~T{N!Gs+7v#cmyMH0lJHdAte1$Knv!b2Sv-VE9_V6h$M96DoU zcD<{m%?);hPN6c<$)0qNaSUQ8*a9ZoseH?{du{o=W?33A-R^ysACQvk?5y?rG{UQ8 z@A-|=qEYrfdm5Lc6C|J_nQHgG3&4xRET{LfE2PcrK4`0REb8c;wlRIsCV(k97}Xcj zRhCHER>n~JZNVQHV(x)mr1yqQ3E2Zo1=y?lkxsVG{}=;dEgt{5$u}oCC!( zD>vTcAXf;e;=*BJgk0n)OX9nADU00000NkvXX Hu0mjfZ>?6S%4Hw+cvwicV0C0n3F7zr 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 Vtzu)of7<{6002ovPDHLkV1oR5>$v~` literal 0 HcmV?d00001 diff --git a/project/public/staticfiles/icons/user/male-user.png b/project/public/staticfiles/icons/user/male-user.png new file mode 100644 index 0000000000000000000000000000000000000000..9ddc3dde9a6cbd0ce5909ce2ac167d8361f5b6b7 GIT binary patch literal 2876 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 X#8kwl$S*uP00000NkvXXu0mjfwH|`y literal 0 HcmV?d00001 diff --git a/project/pyproject.toml b/project/pyproject.toml index 387b6ad..58c90a2 100644 --- a/project/pyproject.toml +++ b/project/pyproject.toml @@ -13,6 +13,10 @@ pydantic = "^1.10.2" argon2-cffi = "^21.3.0" autopep8 = "^1.7.0" python-dotenv = "^0.21.0" +djangorestframework = "^3.14.0" +drf-yasg = "^1.21.5" +django-rest-knox = "^4.2.0" +pillow = "^9.5.0" [tool.poetry.dev-dependencies] diff --git a/project/users/__init__.py b/project/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/users/admin.py b/project/users/admin.py new file mode 100644 index 0000000..e39204e --- /dev/null +++ b/project/users/admin.py @@ -0,0 +1,38 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.contrib.auth import get_user_model + + +class UserAdmin(BaseUserAdmin): + fieldsets = ( + (None, { + 'fields': ( + 'email', 'username', 'password', 'slug', 'name', 'nick_name', 'gender', + 'image', 'dob', 'website', 'contact', 'contact_email', 'address', 'about', + 'last_login', 'updated_at', 'date_joined' + )}), + ('Permissions', { + 'fields': ( + 'is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions', + )}), + ) + add_fieldsets = ( + ( + None, + { + 'classes': ('wide',), + 'fields': ('email', 'password1', 'password2') + } + ), + ) + + list_display = ('id', 'email', 'username', 'slug', 'get_dynamic_username', 'name', + 'is_staff', 'is_superuser', 'date_joined') + list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups') + search_fields = ('email', 'username') + readonly_fields = ('updated_at', 'date_joined',) + ordering = ('-date_joined',) + filter_horizontal = ('groups', 'user_permissions',) + + +admin.site.register(get_user_model(), UserAdmin) diff --git a/project/users/api/views.py b/project/users/api/views.py new file mode 100644 index 0000000..b7a001e --- /dev/null +++ b/project/users/api/views.py @@ -0,0 +1,19 @@ +from rest_framework import permissions +from rest_framework.authtoken.serializers import AuthTokenSerializer +from knox.views import LoginView as KnoxLoginView +from django.contrib.auth import login +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema + + + +class LoginView(KnoxLoginView): + permission_classes = (permissions.AllowAny,) + + @swagger_auto_schema(request_body=AuthTokenSerializer) + def post(self, request, format=None): + serializer = AuthTokenSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = serializer.validated_data['user'] + login(request, user) + return super(LoginView, self).post(request, format=None) diff --git a/project/users/apps.py b/project/users/apps.py new file mode 100644 index 0000000..88f7b17 --- /dev/null +++ b/project/users/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "users" diff --git a/project/users/migrations/0001_initial.py b/project/users/migrations/0001_initial.py new file mode 100644 index 0000000..b86c936 --- /dev/null +++ b/project/users/migrations/0001_initial.py @@ -0,0 +1,109 @@ +# Generated by Django 4.2.1 on 2023-06-04 16:21 + +from django.db import migrations, models +import users.models +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ("email", models.EmailField(max_length=254, unique=True)), + ("username", models.CharField(max_length=254, unique=True)), + ("name", models.CharField(blank=True, max_length=100, null=True)), + ("slug", models.SlugField(max_length=254, unique=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("nick_name", models.CharField(blank=True, max_length=100, null=True)), + ( + "gender", + models.CharField( + blank=True, + choices=[ + ("Male", "Male"), + ("Female", "Female"), + ("Other", "Other"), + ("Do not mention", "Do not mention"), + ], + max_length=20, + null=True, + ), + ), + ( + "image", + models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.upload_user_image_path, + ), + ), + ( + "dob", + models.DateField( + blank=True, null=True, verbose_name="date of birth" + ), + ), + ("website", models.URLField(blank=True, null=True)), + ("contact", models.CharField(blank=True, max_length=30, null=True)), + ( + "contact_email", + models.EmailField(blank=True, max_length=254, null=True), + ), + ("address", models.CharField(blank=True, max_length=254, null=True)), + ("about", models.TextField(blank=True, null=True)), + ("is_staff", models.BooleanField(default=False)), + ("is_superuser", models.BooleanField(default=False)), + ("is_active", models.BooleanField(default=True)), + ("last_login", models.DateTimeField(blank=True, null=True)), + ("date_joined", models.DateTimeField(auto_now_add=True)), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "User", + "verbose_name_plural": "Users", + "ordering": ["-date_joined"], + }, + managers=[ + ("objects", users.models.UserManager()), + ], + ), + ] diff --git a/project/users/migrations/__init__.py b/project/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/users/models.py b/project/users/models.py new file mode 100644 index 0000000..b994475 --- /dev/null +++ b/project/users/models.py @@ -0,0 +1,144 @@ +from django.db import models +from django.db.models.signals import pre_save +from django.dispatch import receiver +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin +from django.utils import timezone +from django.http import Http404 +from utils.snippets import autoslugFromUUID, generate_unique_username_from_email +from utils.image_upload_helpers import upload_user_image_path +from django.utils.translation import gettext_lazy as _ +from django.templatetags.static import static + + +class UserManager(BaseUserManager): + use_in_migrations = True + + def _create_user(self, email, password, **extra_fields): + if not email: + raise ValueError(_('Users must have an email address')) + now = timezone.now() + email = self.normalize_email(email) + user = self.model( + email=email, + last_login=now, + **extra_fields + ) + user.set_password(password) + user.save(using=self._db) + return user + + def create_user(self, email, password, **extra_fields): + extra_fields.setdefault('is_superuser', False) + extra_fields.setdefault('is_staff', False) + return self._create_user(email, password, **extra_fields) + + def create_superuser(self, email, password, **extra_fields): + extra_fields.setdefault('is_superuser', True) + extra_fields.setdefault('is_staff', True) + + if extra_fields.get('is_superuser') is not True: + raise ValueError(_('Superuser must have is_superuser=True.')) + + return self._create_user(email, password, **extra_fields) + + def all(self): + return self.get_queryset() + + def get_by_id(self, id): + try: + instance = self.get_queryset().get(id=id) + except User.DoesNotExist: + raise Http404(_("User Not Found!")) + except User.MultipleObjectsReturned: + qs = self.get_queryset().filter(id=id) + instance = qs.first() + except: + raise Http404(_("Something went wrong!")) + return instance + + def get_by_slug(self, slug): + try: + instance = self.get_queryset().get(slug=slug) + except User.DoesNotExist: + raise Http404(_("User Not Found!")) + except User.MultipleObjectsReturned: + qs = self.get_queryset().filter(slug=slug) + instance = qs.first() + except: + raise Http404(_("Something went wrong!")) + return instance + + +@autoslugFromUUID() +class User(AbstractBaseUser, PermissionsMixin): + class Gender(models.TextChoices): + MALE = "Male", _("Male") + FEMALE = "Female", _("Female") + OTHER = "Other", _("Other") + UNDEFINED = "Do not mention", _("Do not mention") + + email = models.EmailField(max_length=254, unique=True) + username = models.CharField(max_length=254, unique=True) + """ Additional Fields Starts """ + name = models.CharField(max_length=100, null=True, blank=True) + slug = models.SlugField(unique=True, max_length=254) + updated_at = models.DateTimeField(auto_now=True) + # Fields for Portfolio + nick_name = models.CharField(max_length=100, null=True, blank=True) + gender = models.CharField(max_length=20, choices=Gender.choices, blank=True, null=True) + image = models.ImageField(upload_to=upload_user_image_path, null=True, blank=True) + dob = models.DateField(null=True, blank=True, verbose_name=_("date of birth")) + website = models.URLField(null=True, blank=True) + contact = models.CharField(max_length=30, null=True, blank=True) + contact_email = models.EmailField(null=True, blank=True) + address = models.CharField(max_length=254, null=True, blank=True) + about = models.TextField(null=True, blank=True) + """ Additional Fields Ends """ + is_staff = models.BooleanField(default=False) + is_superuser = models.BooleanField(default=False) + is_active = models.BooleanField(default=True) + last_login = models.DateTimeField(null=True, blank=True) + date_joined = models.DateTimeField(auto_now_add=True) + + USERNAME_FIELD = 'email' + EMAIL_FIELD = 'email' + REQUIRED_FIELDS = [] + + objects = UserManager() + + class Meta: + verbose_name = ("User") + verbose_name_plural = ("Users") + ordering = ["-date_joined"] + + + def __str__(self): + return self.get_dynamic_username() + + def get_dynamic_username(self): + """ Get a dynamic username for a specific user instance. if the user has a name then returns the name, + if the user does not have a name but has a username then return username, otherwise returns email as username """ + if self.name: + return self.name + elif self.username: + return self.username + return self.email + + def get_user_image(self): + if self.image: + return self.image.url + else: + if self.gender and self.gender == "Male": + return static("icons/user/avatar-male.png") + if self.gender and self.gender == "Female": + return static("icons/user/avatar-female.png") + return static("icons/user/avatar-default.png") + + +@receiver(pre_save, sender=User) +def update_username_from_email(sender, instance, **kwargs): + """ Generates and updates username from user email on User pre_save hook """ + if not instance.pk: + instance.username = generate_unique_username_from_email( + instance=instance + ) diff --git a/project/users/tests.py b/project/users/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/project/users/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/project/utils/__init__.py b/project/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/utils/helpers.py b/project/utils/helpers.py new file mode 100644 index 0000000..0cb679f --- /dev/null +++ b/project/utils/helpers.py @@ -0,0 +1,53 @@ +from rest_framework.response import Response + + +class ResponseWrapper(Response): + + def __init__(self, data=None, error_code=None, template_name=None, headers=None, exception=False, content_type=None, + error_message=None, message=None, response_success=True, status=None, data_type=None): + """ + Alters the init arguments slightly. + For example, drop 'template_name', and instead use 'data'. + + Setting 'renderer' and 'media_type' will typically be deferred, + For example being set automatically by the `APIView`. + """ + status_by_default_for_gz = 200 + if error_code is None and status is not None: + if status > 299 or status < 200: + error_code = status + response_success = False + else: + status_by_default_for_gz = status + if error_code is not None: + status_by_default_for_gz = error_code + response_success = False + + # manipulate dynamic message + if message is not None and not message == "": + if message.lower() == "list": + message = "List retrieved successfully!" if response_success else "Failed to retrieve the list!" + elif message.lower() == "create": + message = "Created successfully!" if response_success else "Failed to create!" + elif message.lower() == "update": + message = "Updated successfully!" if response_success else "Failed to update!" + elif message.lower() == "delete": + message = "Deleted successfully!" if response_success else "Failed to delete!" + elif message.lower() == "retrieve": + message = "Object retrieved successfully!" if response_success else "Failed to retrieve the object!" + else: + pass + + output_data = { + "success": response_success, + "status_code": error_code if not error_code == "" and not error_code == None else status_by_default_for_gz, + "data": data, + "message": message if message else str(error_message) if error_message else "Success" if response_success else "Failed", + "error": {"code": error_code, "error_details": error_message}, + } + if data_type is not None: + output_data["type"] = data_type + + super().__init__(data=output_data, status=status_by_default_for_gz, + template_name=template_name, headers=headers, + exception=exception, content_type=content_type) diff --git a/project/utils/image_upload_helpers.py b/project/utils/image_upload_helpers.py new file mode 100644 index 0000000..2d9f16c --- /dev/null +++ b/project/utils/image_upload_helpers.py @@ -0,0 +1,25 @@ +import os +import time +from django.utils.text import slugify + + +def get_filename(filepath): + base_name = os.path.basename(filepath) + name, ext = os.path.splitext(base_name) + + new_filename = "{datetime}".format( + datetime=time.strftime("%Y%m%d-%H%M%S") + ) + final_filename = '{new_filename}{ext}'.format( + new_filename=new_filename, ext=ext + ) + return final_filename + + +# User Image Path +def upload_user_image_path(instance, filename): + new_filename = get_filename(filename) + return "Users/{username}/Images/{final_filename}".format( + username=slugify(instance.username[:50]), + final_filename=new_filename + ) diff --git a/project/utils/snippets.py b/project/utils/snippets.py new file mode 100644 index 0000000..eb7a858 --- /dev/null +++ b/project/utils/snippets.py @@ -0,0 +1,284 @@ +import random +import string +import time +from django.utils.text import slugify +from urllib.parse import urlparse +from django.db import models +from django.dispatch import receiver +import uuid +# PDF imports +from io import BytesIO +from django.http import HttpResponse +from django.template.loader import get_template + + +def random_string_generator(size=4, chars=string.ascii_lowercase + string.digits): + """[Generates random string] + + Args: + size (int, optional): [size of string to generate]. Defaults to 4. + chars ([str], optional): [characters to use]. Defaults to string.ascii_lowercase+string.digits. + + Returns: + [str]: [Generated random string] + """ + return ''.join(random.choice(chars) for _ in range(size)) + + +def random_number_generator(size=4, chars='1234567890'): + """[Generates random number] + + Args: + size (int, optional): [size of number to generate]. Defaults to 4. + chars (str, optional): [numbers to use]. Defaults to '1234567890'. + + Returns: + [str]: [Generated random number] + """ + return ''.join(random.choice(chars) for _ in range(size)) + + +def simple_random_string(): + """[Generates simple random string] + + Returns: + [str]: [Generated random string] + """ + timestamp_m = time.strftime("%Y") + timestamp_d = time.strftime("%m") + timestamp_y = time.strftime("%d") + timestamp_now = time.strftime("%H%M%S") + random_str = random_string_generator() + random_num = random_number_generator() + bindings = ( + random_str + timestamp_d + random_num + timestamp_now + + timestamp_y + random_num + timestamp_m + ) + return bindings + + +def simple_random_string_with_timestamp(size=None): + """[Generates random string with timestamp] + + Args: + size ([int], optional): [Size of string]. Defaults to None. + + Returns: + [str]: [Generated random string] + """ + timestamp_m = time.strftime("%Y") + timestamp_d = time.strftime("%m") + timestamp_y = time.strftime("%d") + random_str = random_string_generator() + random_num = random_number_generator() + bindings = ( + random_str + timestamp_d + timestamp_m + timestamp_y + random_num + ) + if not size == None: + return bindings[0:size] + return bindings + + +# def unique_slug_generator(instance, field=None, new_slug=None): +# """[Generates unique slug] + +# Args: +# instance ([Model Class instance]): [Django Model class object instance]. +# field ([Django Model Field], optional): [Django Model Class Field]. Defaults to None. +# new_slug ([str], optional): [passed new slug]. Defaults to None. + +# Returns: +# [str]: [Generated unique slug] +# """ +# if field == None: +# field = instance.title +# if new_slug is not None: +# slug = new_slug +# else: +# slug = slugify(field[:50]) + +# Klass = instance.__class__ +# qs_exists = Klass.objects.filter(slug=slug).exists() +# if qs_exists: +# new_slug = "{slug}-{randstr}".format( +# slug=slug, +# randstr=random_string_generator(size=4) +# ) +# return unique_slug_generator(instance, new_slug=new_slug) +# return slug + + +# def is_url(url): +# """[Checks if a provided string is URL or Not] + +# Args: +# url ([str]): [URL String] + +# Returns: +# [bool]: [returns True if provided string is URL, otherwise returns False] +# """ + +# min_attr = ('scheme', 'netloc') + +# try: +# result = urlparse(url) +# if all([result.scheme, result.netloc]): +# return True +# else: +# return False +# except: +# return False + + +# def autoUniqueIdWithField(fieldname): +# """[Generates auto slug integrating model's field value and UUID] + +# Args: +# fieldname ([str]): [Model field name to use to generate slug] +# """ + +# def decorator(model): +# # some sanity checks first +# assert hasattr(model, fieldname), f"Model has no field {fieldname}" +# assert hasattr(model, "slug"), "Model is missing a slug field" + +# @receiver(models.signals.pre_save, sender=model, weak=False) +# def generate_unique_id(sender, instance, *args, raw=False, **kwargs): +# if not raw and not getattr(instance, fieldname): +# source = getattr(instance, fieldname) + +# def generate(): +# uuid = random_number_generator(size=12) +# Klass = instance.__class__ +# qs_exists = Klass.objects.filter(uuid=uuid).exists() +# if qs_exists: +# generate() +# else: +# instance.uuid = uuid +# pass + +# # generate uuid +# generate() + +# return model +# return decorator + + +# def autoslugWithFieldAndUUID(fieldname): +# """[Generates auto slug integrating model's field value and UUID] + +# Args: +# fieldname ([str]): [Model field name to use to generate slug] +# """ + +# def decorator(model): +# # some sanity checks first +# assert hasattr(model, fieldname), f"Model has no field {fieldname}" +# assert hasattr(model, "slug"), "Model is missing a slug field" + +# @receiver(models.signals.pre_save, sender=model, weak=False) +# def generate_slug(sender, instance, *args, raw=False, **kwargs): +# if not raw and not instance.slug: +# source = getattr(instance, fieldname) +# try: +# slug = slugify(source)[:123] + "-" + str(uuid.uuid4()) +# Klass = instance.__class__ +# qs_exists = Klass.objects.filter(slug=slug).exists() +# if qs_exists: +# new_slug = "{slug}-{randstr}".format( +# slug=slug, +# randstr=random_string_generator(size=4) +# ) +# instance.slug = new_slug +# else: +# instance.slug = slug +# except Exception as e: +# instance.slug = simple_random_string() +# return model +# return decorator + + +# def autoslugFromField(fieldname): +# """[Generates auto slug from model's field value] + +# Args: +# fieldname ([str]): [Model field name to use to generate slug] +# """ + +# def decorator(model): +# # some sanity checks first +# assert hasattr(model, fieldname), f"Model has no field {fieldname!r}" +# assert hasattr(model, "slug"), "Model is missing a slug field" + +# @receiver(models.signals.pre_save, sender=model, weak=False) +# def generate_slug(sender, instance, *args, raw=False, **kwargs): +# if not raw and not instance.slug: +# source = getattr(instance, fieldname) +# try: +# slug = slugify(source) +# Klass = instance.__class__ +# qs_exists = Klass.objects.filter(slug=slug).exists() +# if qs_exists: +# new_slug = "{slug}-{randstr}".format( +# slug=slug, +# randstr=random_string_generator(size=4) +# ) +# instance.slug = new_slug +# else: +# instance.slug = slug +# except Exception as e: +# instance.slug = simple_random_string() +# return model +# return decorator + + +def autoslugFromUUID(): + """[Generates auto slug using UUID] + """ + + def decorator(model): + assert hasattr(model, "slug"), "Model is missing a slug field" + + @receiver(models.signals.pre_save, sender=model, weak=False) + def generate_slug(sender, instance, *args, raw=False, **kwargs): + if not raw and not instance.slug: + try: + instance.slug = str(uuid.uuid4()) + except Exception as e: + instance.slug = simple_random_string() + return model + return decorator + + +def generate_unique_username_from_email(instance): + """[Generates unique username from email] + + Args: + instance ([model class object instance]): [model class object instance] + + Raises: + ValueError: [If found invalid email] + + Returns: + [str]: [unique username] + """ + + # get email from instance + email = instance.email + + if not email: + raise ValueError("Invalid email!") + + def generate_username(email): + return email.split("@")[0][:15] + "__" + simple_random_string_with_timestamp(size=5) + + generated_username = generate_username(email=email) + + Klass = instance.__class__ + qs_exists = Klass.objects.filter(username=generated_username).exists() + + if qs_exists: + # recursive call + generate_unique_username_from_email(instance=instance) + + return generated_username From 19b5291e17d611cfb5e27872d1a0a8cb0c931d1c Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Mon, 5 Jun 2023 00:27:45 +0600 Subject: [PATCH 02/45] Updated Requirements File #35 --- project/requirements.txt | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/project/requirements.txt b/project/requirements.txt index c9db375..ee3e363 100644 --- a/project/requirements.txt +++ b/project/requirements.txt @@ -2,14 +2,35 @@ argon2-cffi-bindings==21.2.0 ; python_full_version == "3.9.13" argon2-cffi==21.3.0 ; python_full_version == "3.9.13" asgiref==3.6.0 ; python_full_version == "3.9.13" autopep8==1.7.0 ; python_full_version == "3.9.13" +certifi==2023.5.7 ; python_full_version == "3.9.13" cffi==1.15.1 ; python_full_version == "3.9.13" +charset-normalizer==3.1.0 ; python_full_version == "3.9.13" +coreapi==2.3.3 ; python_full_version == "3.9.13" +coreschema==0.0.4 ; python_full_version == "3.9.13" +cryptography==41.0.1 ; python_full_version == "3.9.13" +django-rest-knox==4.2.0 ; python_full_version == "3.9.13" django==4.2.1 ; python_full_version == "3.9.13" +djangorestframework==3.14.0 ; python_full_version == "3.9.13" +drf-yasg==1.21.5 ; python_full_version == "3.9.13" +idna==3.4 ; python_full_version == "3.9.13" +inflection==0.5.1 ; python_full_version == "3.9.13" +itypes==1.2.0 ; python_full_version == "3.9.13" +jinja2==3.1.2 ; python_full_version == "3.9.13" +markupsafe==2.1.3 ; python_full_version == "3.9.13" +packaging==23.1 ; python_full_version == "3.9.13" +pillow==9.5.0 ; python_full_version == "3.9.13" psycopg2-binary==2.9.6 ; python_full_version == "3.9.13" pycodestyle==2.10.0 ; python_full_version == "3.9.13" pycparser==2.21 ; python_full_version == "3.9.13" pydantic==1.10.7 ; python_full_version == "3.9.13" python-dotenv==0.21.1 ; python_full_version == "3.9.13" +pytz==2023.3 ; python_full_version == "3.9.13" +requests==2.31.0 ; python_full_version == "3.9.13" +ruamel-yaml-clib==0.2.7 ; platform_python_implementation == "CPython" and python_full_version == "3.9.13" +ruamel-yaml==0.17.31 ; python_full_version == "3.9.13" sqlparse==0.4.4 ; python_full_version == "3.9.13" toml==0.10.2 ; python_full_version == "3.9.13" typing-extensions==4.5.0 ; python_full_version == "3.9.13" tzdata==2023.3 ; sys_platform == "win32" and python_full_version == "3.9.13" +uritemplate==4.1.1 ; python_full_version == "3.9.13" +urllib3==2.0.2 ; python_full_version == "3.9.13" From ef5acf8adcd13aee7394d3d6426c505718a34145 Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Mon, 5 Jun 2023 03:00:34 +0600 Subject: [PATCH 03/45] User Model and Serializer added #35 --- project/config/api_router.py | 10 +- project/config/router.py | 9 ++ project/config/settings.py | 77 +++++++-------- project/config/urls.py | 31 +++++-- project/config/views.py | 18 ++-- project/users/admin.py | 81 +++++++++++----- project/users/api/routers.py | 5 + project/users/api/serializers.py | 46 +++++++++ project/users/api/views.py | 29 +++++- .../migrations/0002_user_is_portfolio_user.py | 18 ++++ project/users/models.py | 69 ++++++++------ project/utils/helpers.py | 93 +++++++++++++++---- project/utils/image_upload_helpers.py | 11 +-- project/utils/snippets.py | 78 +++++++++++++--- 14 files changed, 415 insertions(+), 160 deletions(-) create mode 100644 project/config/router.py create mode 100644 project/users/api/routers.py create mode 100644 project/users/api/serializers.py create mode 100644 project/users/migrations/0002_user_is_portfolio_user.py diff --git a/project/config/api_router.py b/project/config/api_router.py index b8d5eec..0f111f6 100644 --- a/project/config/api_router.py +++ b/project/config/api_router.py @@ -1,17 +1,11 @@ from django.conf import settings from rest_framework.routers import DefaultRouter, SimpleRouter -from users.api.views import LoginView -from knox import views as knox_views from .views import ExampleView +from .router import router +from users.api.routers import * -if settings.DEBUG: - router = DefaultRouter() -else: - router = SimpleRouter() - router.register("example", ExampleView, basename="example") - app_name = "api" urlpatterns = router.urls diff --git a/project/config/router.py b/project/config/router.py new file mode 100644 index 0000000..12b81d7 --- /dev/null +++ b/project/config/router.py @@ -0,0 +1,9 @@ +from django.conf import settings +from rest_framework.routers import DefaultRouter, SimpleRouter + +router = None + +if settings.DEBUG: + router = DefaultRouter() +else: + router = SimpleRouter() diff --git a/project/config/settings.py b/project/config/settings.py index 60390de..263ab1d 100644 --- a/project/config/settings.py +++ b/project/config/settings.py @@ -41,14 +41,18 @@ LOCAL_APPS = [ "users", ] -INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", -] + THIRD_PARTY_APPS + LOCAL_APPS +INSTALLED_APPS = ( + [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + ] + + THIRD_PARTY_APPS + + LOCAL_APPS +) # ---------------------------------------------------- # *** Middleware Definition *** @@ -74,9 +78,7 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [ - os.path.join(BASE_DIR, "config", "templates") - ], + "DIRS": [os.path.join(BASE_DIR, "config", "templates")], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -94,7 +96,7 @@ # ---------------------------------------------------- # https://docs.djangoproject.com/en/dev/topics/auth/customizing/#substituting-a-custom-user-model -AUTH_USER_MODEL = 'users.User' +AUTH_USER_MODEL = "users.User" # ---------------------------------------------------- # *** WSGI Application *** @@ -105,13 +107,13 @@ # *** Database Configuration *** # ---------------------------------------------------- DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': config.DATABASE.NAME, - 'USER': config.DATABASE.USER, - 'PASSWORD': config.DATABASE.PASSWORD, - 'HOST': config.DATABASE.HOST, - 'PORT': config.DATABASE.PORT + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": config.DATABASE.NAME, + "USER": config.DATABASE.USER, + "PASSWORD": config.DATABASE.PASSWORD, + "HOST": config.DATABASE.HOST, + "PORT": config.DATABASE.PORT, } } @@ -153,16 +155,16 @@ # ---------------------------------------------------- # *** Static and Media Files Configuration *** # ---------------------------------------------------- -PUBLIC_ROOT = os.path.join(BASE_DIR, 'public/') +PUBLIC_ROOT = os.path.join(BASE_DIR, "public/") # STATIC & MEDIA URL -STATIC_URL = '/static/' -MEDIA_URL = '/media/' +STATIC_URL = "/static/" +MEDIA_URL = "/media/" # STATIC & MEDIA ROOT -MEDIA_ROOT = os.path.join(PUBLIC_ROOT, 'media/') -STATIC_ROOT = os.path.join(PUBLIC_ROOT, 'static/') +MEDIA_ROOT = os.path.join(PUBLIC_ROOT, "media/") +STATIC_ROOT = os.path.join(PUBLIC_ROOT, "static/") # Static Files Directories STATICFILES_DIRS = [ - os.path.join(PUBLIC_ROOT, 'staticfiles'), + os.path.join(PUBLIC_ROOT, "staticfiles"), ] # https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders @@ -186,7 +188,7 @@ "console": { "level": os.getenv("DJANGO_LOG_LEVEL", "INFO"), "class": "logging.StreamHandler", - "formatter": "verbose" + "formatter": "verbose", } }, "loggers": { @@ -196,10 +198,7 @@ "propagate": True, }, }, - "root": { - "level": os.getenv("DJANGO_LOG_LEVEL", "INFO"), - "handlers": ["console"] - } + "root": {"level": os.getenv("DJANGO_LOG_LEVEL", "INFO"), "handlers": ["console"]}, } # ---------------------------------------------------- @@ -210,12 +209,8 @@ # REST Framework Configuration REST_FRAMEWORK = { - "DEFAULT_AUTHENTICATION_CLASSES": ( - "knox.auth.TokenAuthentication", - ), - "DEFAULT_PERMISSION_CLASSES": ( - "rest_framework.permissions.IsAuthenticated", - ), + "DEFAULT_AUTHENTICATION_CLASSES": ("knox.auth.TokenAuthentication",), + "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), } # KNOX Configuration @@ -236,12 +231,8 @@ # Swagger Configuration SWAGGER_SETTINGS = { - 'SECURITY_DEFINITIONS': { - 'Bearer': { - 'type': 'apiKey', - 'name': 'Authorization', - 'in': 'header' - } + "SECURITY_DEFINITIONS": { + "Bearer": {"type": "apiKey", "name": "Authorization", "in": "header"} }, - 'JSON_EDITOR': True, + "JSON_EDITOR": True, } diff --git a/project/config/urls.py b/project/config/urls.py index b4043a6..67d5aa7 100644 --- a/project/config/urls.py +++ b/project/config/urls.py @@ -29,7 +29,7 @@ schema_view = get_schema_view( openapi.Info( title="Numan Ibn Mazid's Portfolio API", - default_version='v1', + default_version="v1", description="API Documentation for Numan Ibn Mazid's Portfolio Project's Backend", terms_of_service="https://www.google.com/policies/terms/", contact=openapi.Contact(email="numanibnmazid@gmail.com"), @@ -49,18 +49,29 @@ # *** Knox URLs *** # ---------------------------------------------------- # path(r'api/auth/', include('knox.urls')), - path(r'api/auth/login/', LoginView.as_view(), name='knox_login'), - path(r'api/auth/logout/', knox_views.LogoutView.as_view(), name='knox_logout'), - path(r'api/auth/logoutall/', knox_views.LogoutAllView.as_view(), name='knox_logoutall'), + path(r"api/auth/login/", LoginView.as_view(), name="knox_login"), + path(r"api/auth/logout/", knox_views.LogoutView.as_view(), name="knox_logout"), + path( + r"api/auth/logoutall/", + knox_views.LogoutAllView.as_view(), + name="knox_logoutall", + ), # ---------------------------------------------------- # *** Swagger URLs *** # ---------------------------------------------------- - re_path(r'^swagger(?P\.json|\.yaml)$', - schema_view.without_ui(cache_timeout=0), name='schema-json'), - re_path(r'^swagger/$', schema_view.with_ui('swagger', - cache_timeout=0), name='schema-swagger-ui'), - re_path(r'^redoc/$', schema_view.with_ui('redoc', - cache_timeout=0), name='schema-redoc'), + re_path( + r"^swagger(?P\.json|\.yaml)$", + schema_view.without_ui(cache_timeout=0), + name="schema-json", + ), + re_path( + r"^swagger/$", + schema_view.with_ui("swagger", cache_timeout=0), + name="schema-swagger-ui", + ), + re_path( + r"^redoc/$", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc" + ), ] urlpatterns = [ diff --git a/project/config/views.py b/project/config/views.py index 54491ba..e8cb8bf 100644 --- a/project/config/views.py +++ b/project/config/views.py @@ -1,33 +1,35 @@ -from django.shortcuts import render from knox.auth import TokenAuthentication from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import ViewSet -from utils.helpers import ResponseWrapper from django.views.generic import TemplateView +from utils.helpers import custom_response_wrapper +from rest_framework.response import Response class IndexView(TemplateView): template_name = "index.html" +@custom_response_wrapper class ExampleView(ViewSet): authentication_classes = (TokenAuthentication,) permission_classes = (IsAuthenticated,) def list(self, request): - return ResponseWrapper({"Demo": "Hello, world! This is LIST action"}) + # return ResponseWrapper({"Demo": "Hello, world! This is LIST action"}) + return Response({"Demo": "Hello, world! This is LIST action"}) def create(self, request): - return ResponseWrapper({"Demo": "Hello, world! This is CREATE action"}) + return Response({"Demo": "Hello, world! This is CREATE action"}) def retrieve(self, request, pk=None): - return ResponseWrapper({"Demo": "Hello, world! This is RETRIEVE action"}) + return Response({"Demo": "Hello, world! This is RETRIEVE action"}) def update(self, request, pk=None): - return ResponseWrapper({"Demo": "Hello, world! This is UPDATE action"}) + return Response({"Demo": "Hello, world! This is UPDATE action"}) def partial_update(self, request, pk=None): - return ResponseWrapper({"Demo": "Hello, world! This is PARTIAL UPDATE action"}) + return Response({"Demo": "Hello, world! This is PARTIAL UPDATE action"}) def destroy(self, request, pk=None): - return ResponseWrapper({"Demo": "Hello, world! This is DESTROY action"}) + return Response({"Demo": "Hello, world! This is DESTROY action"}) diff --git a/project/users/admin.py b/project/users/admin.py index e39204e..9258559 100644 --- a/project/users/admin.py +++ b/project/users/admin.py @@ -5,34 +5,71 @@ class UserAdmin(BaseUserAdmin): fieldsets = ( - (None, { - 'fields': ( - 'email', 'username', 'password', 'slug', 'name', 'nick_name', 'gender', - 'image', 'dob', 'website', 'contact', 'contact_email', 'address', 'about', - 'last_login', 'updated_at', 'date_joined' - )}), - ('Permissions', { - 'fields': ( - 'is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions', - )}), - ) - add_fieldsets = ( ( None, { - 'classes': ('wide',), - 'fields': ('email', 'password1', 'password2') - } + "fields": ( + "email", + "username", + "password", + "slug", + "is_portfolio_user", + "name", + "nick_name", + "gender", + "image", + "dob", + "website", + "contact", + "contact_email", + "address", + "about", + "last_login", + "updated_at", + "date_joined", + ) + }, + ), + ( + "Permissions", + { + "fields": ( + "is_active", + "is_staff", + "is_superuser", + "groups", + "user_permissions", + ) + }, ), ) + add_fieldsets = ( + (None, {"classes": ("wide",), "fields": ("email", "password1", "password2")}), + ) - list_display = ('id', 'email', 'username', 'slug', 'get_dynamic_username', 'name', - 'is_staff', 'is_superuser', 'date_joined') - list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups') - search_fields = ('email', 'username') - readonly_fields = ('updated_at', 'date_joined',) - ordering = ('-date_joined',) - filter_horizontal = ('groups', 'user_permissions',) + list_display = ( + "id", + "email", + "username", + "is_portfolio_user", + "slug", + "get_dynamic_username", + "name", + "is_staff", + "is_superuser", + "date_joined", + ) + list_filter = ("is_staff", "is_superuser", "is_active", "is_portfolio_user", "groups") + search_fields = ("email", "username") + readonly_fields = ( + "updated_at", + "date_joined", + ) + ordering = ("-date_joined",) + filter_horizontal = ( + "groups", + "user_permissions", + ) admin.site.register(get_user_model(), UserAdmin) diff --git a/project/users/api/routers.py b/project/users/api/routers.py new file mode 100644 index 0000000..331ce31 --- /dev/null +++ b/project/users/api/routers.py @@ -0,0 +1,5 @@ +from .views import UserViewset +from config.router import router + + +router.register("users", UserViewset, basename="users") diff --git a/project/users/api/serializers.py b/project/users/api/serializers.py new file mode 100644 index 0000000..f0b0ae3 --- /dev/null +++ b/project/users/api/serializers.py @@ -0,0 +1,46 @@ +from rest_framework import serializers +from django.contrib.auth import get_user_model + + +class UserSerializer(serializers.ModelSerializer): + image = serializers.SerializerMethodField() + + class Meta: + model = get_user_model() + fields = [ + "id", + "username", + "email", + "name", + "slug", + "nick_name", + "gender", + "image", + "dob", + "website", + "contact", + "contact_email", + "address", + "about", + "is_portfolio_user", + "is_active", + "is_staff", + "is_superuser", + "date_joined", + "last_login", + "updated_at", + ] + read_only_fields = [ + "id", + "username", + "is_active", + "slug", + "updated_at", + "is_staff", + "is_superuser", + "date_joined", + "last_login", + ] + + def get_image(self, obj): + return obj.get_user_image() diff --git a/project/users/api/views.py b/project/users/api/views.py index b7a001e..457ac84 100644 --- a/project/users/api/views.py +++ b/project/users/api/views.py @@ -2,9 +2,13 @@ from rest_framework.authtoken.serializers import AuthTokenSerializer from knox.views import LoginView as KnoxLoginView from django.contrib.auth import login -from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema - +from django.contrib.auth import get_user_model +from rest_framework.viewsets import ModelViewSet +from .serializers import UserSerializer +from utils.helpers import custom_response_wrapper +from rest_framework.decorators import action +from rest_framework.response import Response class LoginView(KnoxLoginView): @@ -14,6 +18,25 @@ class LoginView(KnoxLoginView): def post(self, request, format=None): serializer = AuthTokenSerializer(data=request.data) serializer.is_valid(raise_exception=True) - user = serializer.validated_data['user'] + user = serializer.validated_data["user"] login(request, user) return super(LoginView, self).post(request, format=None) + + +@custom_response_wrapper +class UserViewset(ModelViewSet): + permission_classes = (permissions.IsAuthenticated,) + queryset = get_user_model().objects.all() + serializer_class = UserSerializer + pagination_class = None + lookup_field = "slug" + + + @action(detail=False, methods=['get']) + def get_portfolio_user(self, request): + user_qs = get_user_model().objects.filter(is_portfolio_user=True) + if user_qs.exists(): + user = user_qs.first() + serializer = self.get_serializer(user) + return Response(serializer.data) + return Response({"message": "No portfolio user found!"}, status=404) diff --git a/project/users/migrations/0002_user_is_portfolio_user.py b/project/users/migrations/0002_user_is_portfolio_user.py new file mode 100644 index 0000000..827763b --- /dev/null +++ b/project/users/migrations/0002_user_is_portfolio_user.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2023-06-04 20:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="is_portfolio_user", + field=models.BooleanField(default=False), + ), + ] diff --git a/project/users/models.py b/project/users/models.py index b994475..20bc0dd 100644 --- a/project/users/models.py +++ b/project/users/models.py @@ -1,13 +1,19 @@ from django.db import models from django.db.models.signals import pre_save from django.dispatch import receiver -from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin +from django.contrib.auth.models import ( + AbstractBaseUser, + BaseUserManager, + PermissionsMixin, +) from django.utils import timezone from django.http import Http404 from utils.snippets import autoslugFromUUID, generate_unique_username_from_email from utils.image_upload_helpers import upload_user_image_path from django.utils.translation import gettext_lazy as _ from django.templatetags.static import static +from django.conf import settings +from utils.snippets import image_as_base64, get_static_file_path class UserManager(BaseUserManager): @@ -15,29 +21,25 @@ class UserManager(BaseUserManager): def _create_user(self, email, password, **extra_fields): if not email: - raise ValueError(_('Users must have an email address')) + raise ValueError(_("Users must have an email address")) now = timezone.now() email = self.normalize_email(email) - user = self.model( - email=email, - last_login=now, - **extra_fields - ) + user = self.model(email=email, last_login=now, **extra_fields) user.set_password(password) user.save(using=self._db) return user def create_user(self, email, password, **extra_fields): - extra_fields.setdefault('is_superuser', False) - extra_fields.setdefault('is_staff', False) + extra_fields.setdefault("is_superuser", False) + extra_fields.setdefault("is_staff", False) return self._create_user(email, password, **extra_fields) def create_superuser(self, email, password, **extra_fields): - extra_fields.setdefault('is_superuser', True) - extra_fields.setdefault('is_staff', True) + extra_fields.setdefault("is_superuser", True) + extra_fields.setdefault("is_staff", True) - if extra_fields.get('is_superuser') is not True: - raise ValueError(_('Superuser must have is_superuser=True.')) + if extra_fields.get("is_superuser") is not True: + raise ValueError(_("Superuser must have is_superuser=True.")) return self._create_user(email, password, **extra_fields) @@ -85,7 +87,9 @@ class Gender(models.TextChoices): updated_at = models.DateTimeField(auto_now=True) # Fields for Portfolio nick_name = models.CharField(max_length=100, null=True, blank=True) - gender = models.CharField(max_length=20, choices=Gender.choices, blank=True, null=True) + gender = models.CharField( + max_length=20, choices=Gender.choices, blank=True, null=True + ) image = models.ImageField(upload_to=upload_user_image_path, null=True, blank=True) dob = models.DateField(null=True, blank=True, verbose_name=_("date of birth")) website = models.URLField(null=True, blank=True) @@ -93,6 +97,7 @@ class Gender(models.TextChoices): contact_email = models.EmailField(null=True, blank=True) address = models.CharField(max_length=254, null=True, blank=True) about = models.TextField(null=True, blank=True) + is_portfolio_user = models.BooleanField(default=False) """ Additional Fields Ends """ is_staff = models.BooleanField(default=False) is_superuser = models.BooleanField(default=False) @@ -100,24 +105,24 @@ class Gender(models.TextChoices): last_login = models.DateTimeField(null=True, blank=True) date_joined = models.DateTimeField(auto_now_add=True) - USERNAME_FIELD = 'email' - EMAIL_FIELD = 'email' + USERNAME_FIELD = "email" + EMAIL_FIELD = "email" REQUIRED_FIELDS = [] objects = UserManager() class Meta: - verbose_name = ("User") - verbose_name_plural = ("Users") + verbose_name = "User" + verbose_name_plural = "Users" ordering = ["-date_joined"] - def __str__(self): return self.get_dynamic_username() def get_dynamic_username(self): - """ Get a dynamic username for a specific user instance. if the user has a name then returns the name, - if the user does not have a name but has a username then return username, otherwise returns email as username """ + """Get a dynamic username for a specific user instance. if the user has a name then returns the name, + if the user does not have a name but has a username then return username, otherwise returns email as username + """ if self.name: return self.name elif self.username: @@ -126,19 +131,23 @@ def get_dynamic_username(self): def get_user_image(self): if self.image: - return self.image.url + image_path = settings.MEDIA_ROOT + self.image.url.lstrip("/media/") else: if self.gender and self.gender == "Male": - return static("icons/user/avatar-male.png") - if self.gender and self.gender == "Female": - return static("icons/user/avatar-female.png") - return static("icons/user/avatar-default.png") + image_path = get_static_file_path("icons/user/avatar-male.png") + elif self.gender and self.gender == "Female": + image_path = get_static_file_path("icons/user/avatar-female.png") + else: + image_path = get_static_file_path("icons/user/avatar-default.png") + + if image_path: + return image_as_base64(image_path) + + return None @receiver(pre_save, sender=User) def update_username_from_email(sender, instance, **kwargs): - """ Generates and updates username from user email on User pre_save hook """ + """Generates and updates username from user email on User pre_save hook""" if not instance.pk: - instance.username = generate_unique_username_from_email( - instance=instance - ) + instance.username = generate_unique_username_from_email(instance=instance) diff --git a/project/utils/helpers.py b/project/utils/helpers.py index 0cb679f..2724495 100644 --- a/project/utils/helpers.py +++ b/project/utils/helpers.py @@ -1,10 +1,23 @@ from rest_framework.response import Response +from functools import wraps +from rest_framework.renderers import JSONRenderer -class ResponseWrapper(Response): - - def __init__(self, data=None, error_code=None, template_name=None, headers=None, exception=False, content_type=None, - error_message=None, message=None, response_success=True, status=None, data_type=None): +class ResponseWrapper(Response, JSONRenderer): + def __init__( + self, + data=None, + error_code=None, + template_name=None, + headers=None, + exception=False, + content_type=None, + error_message=None, + message=None, + response_success=True, + status=None, + data_type=None, + ): """ Alters the init arguments slightly. For example, drop 'template_name', and instead use 'data'. @@ -26,28 +39,74 @@ def __init__(self, data=None, error_code=None, template_name=None, headers=None, # manipulate dynamic message if message is not None and not message == "": if message.lower() == "list": - message = "List retrieved successfully!" if response_success else "Failed to retrieve the list!" + message = ( + "List retrieved successfully!" + if response_success + else "Failed to retrieve the list!" + ) elif message.lower() == "create": - message = "Created successfully!" if response_success else "Failed to create!" - elif message.lower() == "update": - message = "Updated successfully!" if response_success else "Failed to update!" - elif message.lower() == "delete": - message = "Deleted successfully!" if response_success else "Failed to delete!" + message = ( + "Created successfully!" if response_success else "Failed to create!" + ) + elif message.lower() in ["update", "partial_update"]: + message = ( + "Updated successfully!" if response_success else "Failed to update!" + ) + elif message.lower() == "destroy": + message = ( + "Deleted successfully!" if response_success else "Failed to delete!" + ) elif message.lower() == "retrieve": - message = "Object retrieved successfully!" if response_success else "Failed to retrieve the object!" + message = ( + "Object retrieved successfully!" + if response_success + else "Failed to retrieve the object!" + ) else: - pass + message = ( + message.capitalize() + " successfully!" + if response_success + else "Failed to " + message + "!" + ) output_data = { "success": response_success, - "status_code": error_code if not error_code == "" and not error_code == None else status_by_default_for_gz, + "status_code": error_code + if not error_code == "" and not error_code == None + else status_by_default_for_gz, "data": data, - "message": message if message else str(error_message) if error_message else "Success" if response_success else "Failed", + "message": message + if message + else str(error_message) + if error_message + else "Success" + if response_success + else "Failed", "error": {"code": error_code, "error_details": error_message}, } if data_type is not None: output_data["type"] = data_type - super().__init__(data=output_data, status=status_by_default_for_gz, - template_name=template_name, headers=headers, - exception=exception, content_type=content_type) + super().__init__( + data=output_data, status=status_by_default_for_gz, content_type=content_type + ) + + +def custom_response_wrapper(viewset_cls): + """ + Custom decorator to wrap the `finalize_response` method of a ViewSet + with the ResponseWrapper functionality. + """ + original_finalize_response = viewset_cls.finalize_response + + @wraps(original_finalize_response) + def wrapped_finalize_response(self, request, response, *args, **kwargs): + if isinstance(response, ResponseWrapper): + return response + response = ResponseWrapper( + data=response.data, message=self.action, status=response.status_code + ) + return original_finalize_response(self, request, response, *args, **kwargs) + + viewset_cls.finalize_response = wrapped_finalize_response + return viewset_cls diff --git a/project/utils/image_upload_helpers.py b/project/utils/image_upload_helpers.py index 2d9f16c..e3e2f6b 100644 --- a/project/utils/image_upload_helpers.py +++ b/project/utils/image_upload_helpers.py @@ -7,12 +7,8 @@ def get_filename(filepath): base_name = os.path.basename(filepath) name, ext = os.path.splitext(base_name) - new_filename = "{datetime}".format( - datetime=time.strftime("%Y%m%d-%H%M%S") - ) - final_filename = '{new_filename}{ext}'.format( - new_filename=new_filename, ext=ext - ) + new_filename = "{datetime}".format(datetime=time.strftime("%Y%m%d-%H%M%S")) + final_filename = "{new_filename}{ext}".format(new_filename=new_filename, ext=ext) return final_filename @@ -20,6 +16,5 @@ def get_filename(filepath): def upload_user_image_path(instance, filename): new_filename = get_filename(filename) return "Users/{username}/Images/{final_filename}".format( - username=slugify(instance.username[:50]), - final_filename=new_filename + username=slugify(instance.username[:50]), final_filename=new_filename ) diff --git a/project/utils/snippets.py b/project/utils/snippets.py index eb7a858..6b220b6 100644 --- a/project/utils/snippets.py +++ b/project/utils/snippets.py @@ -6,10 +6,14 @@ from django.db import models from django.dispatch import receiver import uuid + # PDF imports from io import BytesIO from django.http import HttpResponse from django.template.loader import get_template +import os +import base64 +from django.contrib.staticfiles import finders def random_string_generator(size=4, chars=string.ascii_lowercase + string.digits): @@ -22,10 +26,10 @@ def random_string_generator(size=4, chars=string.ascii_lowercase + string.digits Returns: [str]: [Generated random string] """ - return ''.join(random.choice(chars) for _ in range(size)) + return "".join(random.choice(chars) for _ in range(size)) -def random_number_generator(size=4, chars='1234567890'): +def random_number_generator(size=4, chars="1234567890"): """[Generates random number] Args: @@ -35,7 +39,7 @@ def random_number_generator(size=4, chars='1234567890'): Returns: [str]: [Generated random number] """ - return ''.join(random.choice(chars) for _ in range(size)) + return "".join(random.choice(chars) for _ in range(size)) def simple_random_string(): @@ -51,8 +55,13 @@ def simple_random_string(): random_str = random_string_generator() random_num = random_number_generator() bindings = ( - random_str + timestamp_d + random_num + timestamp_now + - timestamp_y + random_num + timestamp_m + random_str + + timestamp_d + + random_num + + timestamp_now + + timestamp_y + + random_num + + timestamp_m ) return bindings @@ -71,9 +80,7 @@ def simple_random_string_with_timestamp(size=None): timestamp_y = time.strftime("%d") random_str = random_string_generator() random_num = random_number_generator() - bindings = ( - random_str + timestamp_d + timestamp_m + timestamp_y + random_num - ) + bindings = random_str + timestamp_d + timestamp_m + timestamp_y + random_num if not size == None: return bindings[0:size] return bindings @@ -233,8 +240,7 @@ def simple_random_string_with_timestamp(size=None): def autoslugFromUUID(): - """[Generates auto slug using UUID] - """ + """[Generates auto slug using UUID]""" def decorator(model): assert hasattr(model, "slug"), "Model is missing a slug field" @@ -246,7 +252,9 @@ def generate_slug(sender, instance, *args, raw=False, **kwargs): instance.slug = str(uuid.uuid4()) except Exception as e: instance.slug = simple_random_string() + return model + return decorator @@ -270,7 +278,11 @@ def generate_unique_username_from_email(instance): raise ValueError("Invalid email!") def generate_username(email): - return email.split("@")[0][:15] + "__" + simple_random_string_with_timestamp(size=5) + return ( + email.split("@")[0][:15] + + "__" + + simple_random_string_with_timestamp(size=5) + ) generated_username = generate_username(email=email) @@ -282,3 +294,47 @@ def generate_username(email): generate_unique_username_from_email(instance=instance) return generated_username + + +# def image_as_base64(image_file, format='png'): +# """ +# :param `image_file` for the complete path of image. +# :param `format` is format for image, eg: `png` or `jpg`. +# """ +# if not os.path.isfile(image_file): +# return None + +# encoded_string = '' +# with open(image_file, 'rb') as img_f: +# encoded_string = base64.b64encode(img_f.read()) +# return 'data:image/%s;base64,%s' % (format, encoded_string) + + +def get_static_file_path(static_path): + """ + Get the absolute file path for a static file. + :param static_path: The static file path relative to the static root. + :return: The absolute file path or None if the file is not found. + """ + static_file = finders.find(static_path) + if static_file: + return static_file + return None + + +def image_as_base64(image_file): + """ + :param `image_file` for the complete path of the image. + """ + if not os.path.isfile(image_file): + print(f"Image file not found: {image_file}") + return None + + # Get the file extension dynamically + extension = os.path.splitext(image_file)[1][1:] + encoded_string = "" + + with open(image_file, "rb") as img_f: + encoded_string = base64.b64encode(img_f.read()).decode("utf-8") + + return f"data:image/{extension};base64,{encoded_string}" From 2431cf93b936cdf2f7a6ecfea3238b12e1caf3a7 Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Mon, 5 Jun 2023 03:05:36 +0600 Subject: [PATCH 04/45] Added portfolio user get api #35 --- project/users/api/views.py | 2 +- project/utils/helpers.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/project/users/api/views.py b/project/users/api/views.py index 457ac84..f1d155d 100644 --- a/project/users/api/views.py +++ b/project/users/api/views.py @@ -36,7 +36,7 @@ class UserViewset(ModelViewSet): def get_portfolio_user(self, request): user_qs = get_user_model().objects.filter(is_portfolio_user=True) if user_qs.exists(): - user = user_qs.first() + user = user_qs.last() serializer = self.get_serializer(user) return Response(serializer.data) return Response({"message": "No portfolio user found!"}, status=404) diff --git a/project/utils/helpers.py b/project/utils/helpers.py index 2724495..6e5b759 100644 --- a/project/utils/helpers.py +++ b/project/utils/helpers.py @@ -64,9 +64,9 @@ def __init__( ) else: message = ( - message.capitalize() + " successfully!" + "SUCCESS!" if response_success - else "Failed to " + message + "!" + else "FAILED!" ) output_data = { From d29f7911637cbdf3eaedb8c86b547bb95194ce86 Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Thu, 15 Jun 2023 01:12:20 +0600 Subject: [PATCH 05/45] Profile Information Added #35 #36 - Configured Backend API Information in ENV - Added Profile Information In Index Page --- frontend/.env.example | 3 + frontend/content/StaticData.ts | 3 +- frontend/lib/backendAPI.ts | 31 ++++++++++ frontend/lib/types.ts | 28 ++++++++- frontend/next.config.js | 3 +- frontend/pages/index.tsx | 60 +++++++++++++++++-- frontend/styles/globals.css | 3 + project/config/settings.py | 31 ++++++++++ project/poetry.lock | 16 ++++- project/pyproject.toml | 1 + project/requirements.txt | 1 + project/users/admin.py | 2 +- project/users/api/serializers.py | 2 +- .../0003_rename_nick_name_user_nickname.py | 18 ++++++ project/users/models.py | 2 +- 15 files changed, 191 insertions(+), 13 deletions(-) create mode 100644 project/users/migrations/0003_rename_nick_name_user_nickname.py diff --git a/frontend/.env.example b/frontend/.env.example index b064bda..f10fb68 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -40,3 +40,6 @@ REVALIDATE_SECRET= # Backend API URL BACKEND_API_BASE_URL= + +# Backend API TOKEN +BACKEND_API_TOKEN= diff --git a/frontend/content/StaticData.ts b/frontend/content/StaticData.ts index 75883b9..500621b 100644 --- a/frontend/content/StaticData.ts +++ b/frontend/content/StaticData.ts @@ -5,7 +5,8 @@ const staticData: StaticData = { personal: { name: "Numan Ibn Mazid", profession: "Software Engineer", - current_position: "Currently Working as a Software Engineer at SELISE Digital Platforms. (Stack: Python, Django, Javascript)" + current_position: "Currently Working as a Software Engineer at SELISE Digital Platforms. (Stack: Python, Django, Javascript)", + about: "Experienced professional Software Engineer who enjoys developing innovative software solutions that are tailored to customer desirability and usability." } } diff --git a/frontend/lib/backendAPI.ts b/frontend/lib/backendAPI.ts index 965db17..18c9315 100644 --- a/frontend/lib/backendAPI.ts +++ b/frontend/lib/backendAPI.ts @@ -1,4 +1,35 @@ + const BACKEND_API_BASE_URL = process.env.BACKEND_API_BASE_URL +const BACKEND_API_TOKEN = process.env.BACKEND_API_TOKEN + + +// *** PROFILE *** +// Profile URL +const PROFILE_PATH = "users/get_portfolio_user/" +const PROFILE_ENDPOINT = "http://127.0.0.1:8000/api/" + PROFILE_PATH + +/** + * Makes a request to the BACKEND API to retrieve Portfolio User Information. + */ +export const getProfileInfo = async () => { + const portfolioProfile = await fetch( + PROFILE_ENDPOINT, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } + ) + + if (portfolioProfile.ok) { + const responseData = await portfolioProfile.json() + return responseData.data + } else { + const errorMessage = `Error fetching portfolio profile: ${portfolioProfile.status} ${portfolioProfile.statusText}` + // Handle the error or display the error message + console.log(errorMessage) + } +} // *** SKILLS *** diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts index 9e624cb..c400617 100644 --- a/frontend/lib/types.ts +++ b/frontend/lib/types.ts @@ -9,13 +9,39 @@ import { ReadTimeResults } from "reading-time" export type PersonalStaticData = { name: string, profession: string, - current_position: string + current_position: string, + about: string } export type StaticData = { personal: PersonalStaticData } +/* Profile Types */ +export type ProfileType = { + id: number + username: string + email: string + name: string + slug: string + nickname: string + gender: string + image: string + dob: string + website: string + contact: string + contact_email: string + address: string + about: string + is_portfolio_user: string + is_active: string + is_staff: string + is_superuser: string + date_joined: string + last_login: string + updated_at: string +} + /* Custom Animated Components types */ export type AnimatedTAGProps = { variants: Variants diff --git a/frontend/next.config.js b/frontend/next.config.js index 8c1ca8d..7f7f3d5 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -32,6 +32,7 @@ module.exports = withPWA({ ignoreBuildErrors: false, }, env:{ - BACKEND_API_BASE_URL : process.env.BACKEND_API_BASE_URL + BACKEND_API_BASE_URL : process.env.BACKEND_API_BASE_URL, + BACKEND_API_TOKEN : process.env.BACKEND_API_TOKEN } }); diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index ecf0588..5573a62 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -22,16 +22,24 @@ import staticData from "@content/StaticData" import React from "react" import Link from "next/link" import { useEffect, useState } from 'react' -import { getAllBlogs } from "@lib/backendAPI" +import { getProfileInfo, getAllBlogs } from "@lib/backendAPI" +import { ProfileType } from "@lib/types" export default function Home() { + const [profileInfo, setProfileInfo] = useState(null) const [blogs, setBlogs] = useState([]) useEffect(() => { + fetchProfileInfo() fetchBlogs() }, []) + const fetchProfileInfo = async () => { + const profileData: ProfileType = await getProfileInfo() + setProfileInfo(profileData) + } + const fetchBlogs = async () => { const blogsData = await getAllBlogs(3) setBlogs(blogsData) @@ -62,15 +70,15 @@ export default function Home() {

.line::before { margin: 0; } } + + +/* Custom CSS */ diff --git a/project/config/settings.py b/project/config/settings.py index 263ab1d..545d845 100644 --- a/project/config/settings.py +++ b/project/config/settings.py @@ -37,6 +37,8 @@ "knox", # Django REST Framework Yet Another Swagger "drf_yasg", + # Django CORS Headers + "corsheaders", ] LOCAL_APPS = [ "users", @@ -58,6 +60,8 @@ # *** Middleware Definition *** # ---------------------------------------------------- MIDDLEWARE = [ + # Django CORS Headers Middleware + "corsheaders.middleware.CorsMiddleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", @@ -236,3 +240,30 @@ }, "JSON_EDITOR": True, } + +# Django CORS Headers Configuration +CORS_ORIGIN_ALLOW_ALL = False +CORS_ORIGIN_WHITELIST = [ + 'http://localhost:3000', # frontend URL here +] + +CORS_ALLOW_METHODS = [ + 'GET', + 'POST', + 'PUT', + 'PATCH', + 'DELETE', + 'OPTIONS', +] + +CORS_ALLOW_HEADERS = [ + 'accept', + 'accept-encoding', + 'authorization', + 'content-type', + 'dnt', + 'origin', + 'user-agent', + 'x-csrftoken', + 'x-requested-with', +] diff --git a/project/poetry.lock b/project/poetry.lock index 6b32269..135fe9f 100644 --- a/project/poetry.lock +++ b/project/poetry.lock @@ -348,6 +348,20 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] +[[package]] +name = "django-cors-headers" +version = "4.1.0" +description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." +optional = false +python-versions = ">=3.7" +files = [ + {file = "django_cors_headers-4.1.0-py3-none-any.whl", hash = "sha256:88a4bfae24b6404dd0e0640203cb27704a2a57fd546a429e5d821dfa53dd1acf"}, + {file = "django_cors_headers-4.1.0.tar.gz", hash = "sha256:36a8d7a6dee6a85f872fe5916cc878a36d0812043866355438dfeda0b20b6b78"}, +] + +[package.dependencies] +Django = ">=3.2" + [[package]] name = "django-rest-knox" version = "4.2.0" @@ -938,4 +952,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = "3.9.13" -content-hash = "8a84a749e9214717dd8ad819f958d5dd55750052959672720d6206fd664f0a57" +content-hash = "2178e977ab4eeb23e95839bd1b98e0d7606503992da0c517d13ad2e660b74e4c" diff --git a/project/pyproject.toml b/project/pyproject.toml index 58c90a2..799ebec 100644 --- a/project/pyproject.toml +++ b/project/pyproject.toml @@ -17,6 +17,7 @@ djangorestframework = "^3.14.0" drf-yasg = "^1.21.5" django-rest-knox = "^4.2.0" pillow = "^9.5.0" +django-cors-headers = "^4.1.0" [tool.poetry.dev-dependencies] diff --git a/project/requirements.txt b/project/requirements.txt index ee3e363..0d43449 100644 --- a/project/requirements.txt +++ b/project/requirements.txt @@ -8,6 +8,7 @@ charset-normalizer==3.1.0 ; python_full_version == "3.9.13" coreapi==2.3.3 ; python_full_version == "3.9.13" coreschema==0.0.4 ; python_full_version == "3.9.13" cryptography==41.0.1 ; python_full_version == "3.9.13" +django-cors-headers==4.1.0 ; python_full_version == "3.9.13" django-rest-knox==4.2.0 ; python_full_version == "3.9.13" django==4.2.1 ; python_full_version == "3.9.13" djangorestframework==3.14.0 ; python_full_version == "3.9.13" diff --git a/project/users/admin.py b/project/users/admin.py index 9258559..15e002e 100644 --- a/project/users/admin.py +++ b/project/users/admin.py @@ -15,7 +15,7 @@ class UserAdmin(BaseUserAdmin): "slug", "is_portfolio_user", "name", - "nick_name", + "nickname", "gender", "image", "dob", diff --git a/project/users/api/serializers.py b/project/users/api/serializers.py index f0b0ae3..c5040d3 100644 --- a/project/users/api/serializers.py +++ b/project/users/api/serializers.py @@ -13,7 +13,7 @@ class Meta: "email", "name", "slug", - "nick_name", + "nickname", "gender", "image", "dob", diff --git a/project/users/migrations/0003_rename_nick_name_user_nickname.py b/project/users/migrations/0003_rename_nick_name_user_nickname.py new file mode 100644 index 0000000..ae5cc51 --- /dev/null +++ b/project/users/migrations/0003_rename_nick_name_user_nickname.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2023-06-14 19:01 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0002_user_is_portfolio_user"), + ] + + operations = [ + migrations.RenameField( + model_name="user", + old_name="nick_name", + new_name="nickname", + ), + ] diff --git a/project/users/models.py b/project/users/models.py index 20bc0dd..46eda98 100644 --- a/project/users/models.py +++ b/project/users/models.py @@ -86,7 +86,7 @@ class Gender(models.TextChoices): slug = models.SlugField(unique=True, max_length=254) updated_at = models.DateTimeField(auto_now=True) # Fields for Portfolio - nick_name = models.CharField(max_length=100, null=True, blank=True) + nickname = models.CharField(max_length=100, null=True, blank=True) gender = models.CharField( max_length=20, choices=Gender.choices, blank=True, null=True ) From be194a109f6e5da67a579d10cb7eb917f9b5752f Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Fri, 16 Jun 2023 05:10:38 +0600 Subject: [PATCH 06/45] Professional Experiences Added #35 #36 - Configured Professional Experience backend - Configured Professional Experience frontend with backend api --- .../components/Home/ExperienceSection.tsx | 54 +++++---- frontend/components/TimelineItem.tsx | 85 ++++++++++---- frontend/layout/BlogLayout.tsx | 4 +- frontend/lib/backendAPI.ts | 71 +++++++----- frontend/lib/types.ts | 13 ++- frontend/pages/about.tsx | 19 ++- frontend/pages/index.tsx | 51 ++++++-- project/config/api_router.py | 3 +- project/config/settings.py | 3 + project/poetry.lock | 34 +++++- project/portfolios/__init__.py | 0 project/portfolios/admin.py | 22 ++++ project/portfolios/api/__init__.py | 0 .../api/professional_experiences/__init__.py | 0 .../api/professional_experiences/routers.py | 5 + .../professional_experiences/serializers.py | 22 ++++ .../api/professional_experiences/views.py | 13 +++ project/portfolios/apps.py | 6 + project/portfolios/migrations/0001_initial.py | 67 +++++++++++ ...lter_professionalexperience_description.py | 19 +++ project/portfolios/migrations/__init__.py | 0 project/portfolios/models.py | 109 ++++++++++++++++++ project/portfolios/tests.py | 3 + project/portfolios/views.py | 3 + .../staticfiles/icons/office-building.png | Bin 0 -> 17545 bytes project/pyproject.toml | 1 + project/requirements.txt | 2 + project/users/api/__init__.py | 0 project/users/api/serializers.py | 8 +- project/users/migrations/0001_initial.py | 2 +- project/users/models.py | 7 +- project/utils/helpers.py | 53 ++++++++- project/utils/image_upload_helpers.py | 10 +- project/utils/mixins.py | 21 ++++ project/utils/snippets.py | 62 +++++----- 35 files changed, 632 insertions(+), 140 deletions(-) create mode 100644 project/portfolios/__init__.py create mode 100644 project/portfolios/admin.py create mode 100644 project/portfolios/api/__init__.py create mode 100644 project/portfolios/api/professional_experiences/__init__.py create mode 100644 project/portfolios/api/professional_experiences/routers.py create mode 100644 project/portfolios/api/professional_experiences/serializers.py create mode 100644 project/portfolios/api/professional_experiences/views.py create mode 100644 project/portfolios/apps.py create mode 100644 project/portfolios/migrations/0001_initial.py create mode 100644 project/portfolios/migrations/0002_alter_professionalexperience_description.py create mode 100644 project/portfolios/migrations/__init__.py create mode 100644 project/portfolios/models.py create mode 100644 project/portfolios/tests.py create mode 100644 project/portfolios/views.py create mode 100644 project/public/staticfiles/icons/office-building.png create mode 100644 project/users/api/__init__.py create mode 100644 project/utils/mixins.py diff --git a/frontend/components/Home/ExperienceSection.tsx b/frontend/components/Home/ExperienceSection.tsx index c7ecbd7..15a70e6 100644 --- a/frontend/components/Home/ExperienceSection.tsx +++ b/frontend/components/Home/ExperienceSection.tsx @@ -1,25 +1,19 @@ import { FadeContainer, popUp } from "../../content/FramerMotionVariants" -import { HomeHeading } from "../../pages" import { motion } from "framer-motion" import React from "react" -import { useEffect, useState } from 'react' -import { getAllExperiences } from "@lib/backendAPI" import { TimelineItem } from "@components/TimelineItem" import { TimelineList } from "@components/TimelineList" import { ExperienceType } from "@lib/types" +import AnimatedHeading from "@components/FramerMotion/AnimatedHeading" +import { headingFromLeft } from "@content/FramerMotionVariants" +import { useRouter } from 'next/router' -export default function SkillSection() { - const [experiences, setExperiences] = useState([]) - - useEffect(() => { - fetchExperiences() - }, []) - - const fetchExperiences = async () => { - const experiencesData = await getAllExperiences() - setExperiences(experiencesData) - } +// export default function ExperienceSection({ experienceProps = null }) { +export default function ExperienceSection({ experiences }: { experiences: ExperienceType[] }) { + const router = useRouter() + // limit experiences to 1 if on home page otherwise show all + const experiencesToDisplay = router.pathname === '/' ? experiences.slice(0, 1) : experiences // ******* Loader Starts ******* if (experiences.length === 0) { @@ -29,7 +23,17 @@ export default function SkillSection() { return (
- +
+ + Work Experiences + + {experiences.length} + + +
-

Here's a brief rundown of my most recent experiences.

- {experiences ? ( +

Here's a brief rundown of my professional experiences.

+ {experiencesToDisplay ? ( - {experiences.map((experience: ExperienceType, index) => ( + {experiencesToDisplay.map((experience: ExperienceType, index) => ( ))} diff --git a/frontend/components/TimelineItem.tsx b/frontend/components/TimelineItem.tsx index 63fcd3d..986231a 100644 --- a/frontend/components/TimelineItem.tsx +++ b/frontend/components/TimelineItem.tsx @@ -1,17 +1,28 @@ +import Image from "next/image" + + type Props = { - title: string; - meta: string; - link?: string | null; - meta_small?: string | null; - content: any; + designation: string + company: string + company_image: string + company_url?: string | null + address?: string | null + job_type: string + duration: string + duration_in_days: string + description?: any | null } export function TimelineItem({ - title, - meta, - link = null, - meta_small, - content + designation, + company, + company_image, + company_url = null, + address = null, + job_type, + duration, + duration_in_days, + description = null }: Props) { return ( <> @@ -31,21 +42,53 @@ export function TimelineItem({ fill="currentColor" > -

{title}

+

{designation}

+ + {company_image ? ( +
+ {company_url ? ( + + {company} +

{company}

+
+ ) : ( +
+ {company} +

{company}

+
+ )} +
+ ) : null} + +

+ {duration} + ({duration_in_days}) +

+ + {address ?

{address}

: null} - {link ? ( - - {meta} - - ) : ( -

{meta}

- )} -

{meta_small}

+ {job_type ?

[{job_type}]

: null}
diff --git a/frontend/layout/BlogLayout.tsx b/frontend/layout/BlogLayout.tsx index 9a67e3c..3ee5eda 100644 --- a/frontend/layout/BlogLayout.tsx +++ b/frontend/layout/BlogLayout.tsx @@ -60,7 +60,7 @@ export default function BlogLayout({
Jatin Sharma

- Jatin Sharma + Numan Ibn Mazid {getFormattedDate(new Date(post.meta.date))}

diff --git a/frontend/lib/backendAPI.ts b/frontend/lib/backendAPI.ts index 18c9315..b1a7a6e 100644 --- a/frontend/lib/backendAPI.ts +++ b/frontend/lib/backendAPI.ts @@ -31,6 +31,45 @@ export const getProfileInfo = async () => { } } +// *** EXPERIENCE *** + +// Experience URL + +const EXPERIENCE_PATH = "professional-experiences/" +const EXPERIENCE_ENDPOINT = "http://127.0.0.1:8000/api/" + EXPERIENCE_PATH + +/** + * Makes a request to the BACKEND API to retrieve all Experience Data. + */ +export const getAllExperiences = async (length?: number | undefined) => { + let ENDPOINT = null + // Set limit if length is not undefined + if (length !== undefined) { + ENDPOINT = EXPERIENCE_ENDPOINT + `?_limit=${length}` + } + else { + ENDPOINT = EXPERIENCE_ENDPOINT + } + + const allExperiences = await fetch( + ENDPOINT, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } + ) + + if (allExperiences.ok) { + const responseData = await allExperiences.json() + return responseData.data + } else { + const errorMessage = `Error fetching professional experiences: ${allExperiences.status} ${allExperiences.statusText}` + // Handle the error or display the error message + console.log(errorMessage) + } +} + // *** SKILLS *** // Skills URL @@ -108,38 +147,6 @@ export const getAllBlogs = async (length?: number | undefined) => { return fakeBlogsData } -// *** EXPERIENCE *** - -// Experience URL -const EXPERIENCE_PATH = "/posts?_limit=5" -const EXPERIENCE_ENDPOINT = BACKEND_API_BASE_URL + EXPERIENCE_PATH - -/** - * Makes a request to the BACKEND API to retrieve all Experience Data. - */ -export const getAllExperiences = async () => { - - const allExperiences = await fetch( - EXPERIENCE_ENDPOINT - ) - .then((response) => response.json()) - .catch((error) => console.log('Error fetching experiences:', error)) - - // TODO:Integrate with backend API - // ******* Faking data Starts ******* - const fakeExperiencesData = allExperiences.map((experience: { title: any, body: any }) => ({ - title: "Software Engineer", - company: experience.title.split(' ').slice(0, 3).join(' ').toUpperCase(), - company_url: "https://github.com/NumanIbnMazid", - duration: "2018 - 2019", - description: experience.body - })) - // Need to return `allExperiences` - // ******* Faking data Ends ******* - - return fakeExperiencesData -} - // *** PROJECTS *** // Certificate URL diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts index c400617..8bcad32 100644 --- a/frontend/lib/types.ts +++ b/frontend/lib/types.ts @@ -84,11 +84,22 @@ export type ProjectType = { } export type ExperienceType = { - title: string + id: number + slug: string company: string + company_image: string company_url: string + address: string + designation: string + job_type: string + start_date: string + end_date: string duration: string + duration_in_days: string + currently_working: string description: string + created_at: string + updated_at: string } export type SkillType = { diff --git a/frontend/pages/about.tsx b/frontend/pages/about.tsx index b72cdc4..63a68ff 100644 --- a/frontend/pages/about.tsx +++ b/frontend/pages/about.tsx @@ -1,8 +1,8 @@ import MDXContent from "@lib/MDXContent" import pageMeta from "@content/meta" -import { MovieType, PostType } from "@lib/types" +import { MovieType, PostType, ExperienceType } from "@lib/types" import StaticPage from "@components/StaticPage" -import { getAllMovies } from "@lib/backendAPI" +import { getAllExperiences, getAllMovies } from "@lib/backendAPI" import { useEffect, useState } from 'react' import MovieCard from "@components/MovieCard" import { motion } from "framer-motion" @@ -21,17 +21,24 @@ export default function About({ movies: MovieType[] }) { + const [experiences, setExperiences] = useState([]) const [movies, setMovies] = useState([]) - useEffect(() => { - fetchMovies() - }, []) + const fetchExperiences = async () => { + const experiencesData: ExperienceType[] = await getAllExperiences() + setExperiences(experiencesData) + } const fetchMovies = async () => { const moviesData = await getAllMovies() setMovies(moviesData) } + useEffect(() => { + fetchExperiences() + fetchMovies() + }, []) + // ******* Loader Starts ******* if (movies.length === 0) { return
Loading...
@@ -51,7 +58,7 @@ export default function About({ className="grid min-h-screen py-20 place-content-center" >
- + diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index 5573a62..8d664c3 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -22,18 +22,19 @@ import staticData from "@content/StaticData" import React from "react" import Link from "next/link" import { useEffect, useState } from 'react' -import { getProfileInfo, getAllBlogs } from "@lib/backendAPI" -import { ProfileType } from "@lib/types" +import { getProfileInfo, getAllExperiences, getAllBlogs } from "@lib/backendAPI" +import { ProfileType, ExperienceType } from "@lib/types" export default function Home() { const [profileInfo, setProfileInfo] = useState(null) + const [experiences, setExperiences] = useState([]) const [blogs, setBlogs] = useState([]) - useEffect(() => { - fetchProfileInfo() - fetchBlogs() - }, []) + const fetchExperiences = async () => { + const experiencesData: ExperienceType[] = await getAllExperiences() + setExperiences(experiencesData) + } const fetchProfileInfo = async () => { const profileData: ProfileType = await getProfileInfo() @@ -45,6 +46,12 @@ export default function Home() { setBlogs(blogsData) } + useEffect(() => { + fetchProfileInfo() + fetchExperiences() + fetchBlogs() + }, []) + // // ******* Loader Starts ******* // if (blogs.length === 0) { // return
Loading...
@@ -160,9 +167,39 @@ export default function Home() {
- + + {/* Experience Section */} + + + {/* View all experiences link */} + + View all experiences + + + + + + {/* Skills Section */} + + {/* Blogs Section */} + + {/* Contact Section */}
diff --git a/project/config/api_router.py b/project/config/api_router.py index 0f111f6..c83ad81 100644 --- a/project/config/api_router.py +++ b/project/config/api_router.py @@ -1,8 +1,7 @@ -from django.conf import settings -from rest_framework.routers import DefaultRouter, SimpleRouter from .views import ExampleView from .router import router from users.api.routers import * +from portfolios.api.professional_experiences.routers import * router.register("example", ExampleView, basename="example") diff --git a/project/config/settings.py b/project/config/settings.py index 545d845..9af987f 100644 --- a/project/config/settings.py +++ b/project/config/settings.py @@ -39,9 +39,12 @@ "drf_yasg", # Django CORS Headers "corsheaders", + # Django CKEditor + "ckeditor", ] LOCAL_APPS = [ "users", + "portfolios", ] INSTALLED_APPS = ( [ diff --git a/project/poetry.lock b/project/poetry.lock index 135fe9f..e94631f 100644 --- a/project/poetry.lock +++ b/project/poetry.lock @@ -348,6 +348,21 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] +[[package]] +name = "django-ckeditor" +version = "6.5.1" +description = "Django admin CKEditor integration." +optional = false +python-versions = ">=3.8" +files = [ + {file = "django-ckeditor-6.5.1.tar.gz", hash = "sha256:57cd8fb7cd150adca354cd4e5c35a743fadaab7073f957d2b7167f0d9fe1fcaf"}, + {file = "django_ckeditor-6.5.1-py3-none-any.whl", hash = "sha256:1321f24df392f30698513930ce5c9f6d899f9bd0ef734c3b64fe936d809e11b3"}, +] + +[package.dependencies] +Django = ">=3.2" +django-js-asset = ">=2.0" + [[package]] name = "django-cors-headers" version = "4.1.0" @@ -362,6 +377,23 @@ files = [ [package.dependencies] Django = ">=3.2" +[[package]] +name = "django-js-asset" +version = "2.0.0" +description = "script tag with additional attributes for django.forms.Media" +optional = false +python-versions = ">=3.6" +files = [ + {file = "django_js_asset-2.0.0-py3-none-any.whl", hash = "sha256:86f9f300d682537ddaf0487dc2ab356581b8f50c069bdba91d334a46e449f923"}, + {file = "django_js_asset-2.0.0.tar.gz", hash = "sha256:adc1ee1efa853fad42054b540c02205344bb406c9bddf87c9e5377a41b7db90f"}, +] + +[package.dependencies] +Django = ">=2.2" + +[package.extras] +tests = ["coverage"] + [[package]] name = "django-rest-knox" version = "4.2.0" @@ -952,4 +984,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = "3.9.13" -content-hash = "2178e977ab4eeb23e95839bd1b98e0d7606503992da0c517d13ad2e660b74e4c" +content-hash = "e34dcc3aa93890f63ccd00da4cf645202b22a3deba13a4dbba374911bfa6d65d" diff --git a/project/portfolios/__init__.py b/project/portfolios/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/portfolios/admin.py b/project/portfolios/admin.py new file mode 100644 index 0000000..0fce53f --- /dev/null +++ b/project/portfolios/admin.py @@ -0,0 +1,22 @@ +from django.contrib import admin +from django.db import models +from utils.mixins import CustomModelAdminMixin +from portfolios.models import ( + ProfessionalExperience, +) +from ckeditor.widgets import CKEditorWidget + + +# ---------------------------------------------------- +# *** Professional Experience *** +# ---------------------------------------------------- + +class ProfessionalExperienceAdmin(CustomModelAdminMixin, admin.ModelAdmin): + formfield_overrides = { + models.TextField: {'widget': CKEditorWidget}, + } + class Meta: + model = ProfessionalExperience + + +admin.site.register(ProfessionalExperience, ProfessionalExperienceAdmin) diff --git a/project/portfolios/api/__init__.py b/project/portfolios/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/portfolios/api/professional_experiences/__init__.py b/project/portfolios/api/professional_experiences/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/portfolios/api/professional_experiences/routers.py b/project/portfolios/api/professional_experiences/routers.py new file mode 100644 index 0000000..a8e4b8d --- /dev/null +++ b/project/portfolios/api/professional_experiences/routers.py @@ -0,0 +1,5 @@ +from config.router import router +from portfolios.api.professional_experiences.views import ProfessionalExperienceViewset + + +router.register("professional-experiences", ProfessionalExperienceViewset, basename="professional_experiences") diff --git a/project/portfolios/api/professional_experiences/serializers.py b/project/portfolios/api/professional_experiences/serializers.py new file mode 100644 index 0000000..194e3ef --- /dev/null +++ b/project/portfolios/api/professional_experiences/serializers.py @@ -0,0 +1,22 @@ +from rest_framework import serializers +from portfolios.models import ProfessionalExperience + + +class ProfessionalExperienceSerializer(serializers.ModelSerializer): + company_image = serializers.SerializerMethodField() + duration = serializers.SerializerMethodField() + duration_in_days = serializers.SerializerMethodField() + + class Meta: + model = ProfessionalExperience + fields = "__all__" + read_only_fields = ("id", "slug", "created_at", "updated_at") + + def get_company_image(self, obj): + return obj.get_company_image() + + def get_duration(self, obj): + return obj.get_duration() + + def get_duration_in_days(self, obj): + return obj.get_duration_in_days() diff --git a/project/portfolios/api/professional_experiences/views.py b/project/portfolios/api/professional_experiences/views.py new file mode 100644 index 0000000..345e842 --- /dev/null +++ b/project/portfolios/api/professional_experiences/views.py @@ -0,0 +1,13 @@ +from utils.helpers import custom_response_wrapper, ProjectGenericModelViewset +from portfolios.models import ( + ProfessionalExperience, +) +from portfolios.api.professional_experiences.serializers import ( + ProfessionalExperienceSerializer +) + + +@custom_response_wrapper +class ProfessionalExperienceViewset(ProjectGenericModelViewset): + queryset = ProfessionalExperience.objects.all() + serializer_class = ProfessionalExperienceSerializer diff --git a/project/portfolios/apps.py b/project/portfolios/apps.py new file mode 100644 index 0000000..6113f1c --- /dev/null +++ b/project/portfolios/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PortfoliosConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "portfolios" diff --git a/project/portfolios/migrations/0001_initial.py b/project/portfolios/migrations/0001_initial.py new file mode 100644 index 0000000..2f2b00a --- /dev/null +++ b/project/portfolios/migrations/0001_initial.py @@ -0,0 +1,67 @@ +# Generated by Django 4.2.1 on 2023-06-15 17:25 + +from django.db import migrations, models +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="ProfessionalExperience", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("slug", models.SlugField(max_length=255, unique=True)), + ("company", models.CharField(max_length=150)), + ( + "company_image", + models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_professional_experience_company_image_path, + ), + ), + ("company_url", models.URLField(blank=True, null=True)), + ("address", models.CharField(blank=True, max_length=254, null=True)), + ("designation", models.CharField(max_length=150)), + ( + "job_type", + models.CharField( + choices=[ + ("Full Time", "Full Time"), + ("Part Time", "Part Time"), + ("Contractual", "Contractual"), + ("Remote", "Remote"), + ], + default="Full Time", + max_length=20, + ), + ), + ("start_date", models.DateField()), + ("end_date", models.DateField(blank=True, null=True)), + ("currently_working", models.BooleanField(default=False)), + ("description", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Professional Experience", + "verbose_name_plural": "Professional Experiences", + "db_table": "professional_experience", + "ordering": ("-currently_working", "-start_date"), + "get_latest_by": "created_at", + }, + ), + ] diff --git a/project/portfolios/migrations/0002_alter_professionalexperience_description.py b/project/portfolios/migrations/0002_alter_professionalexperience_description.py new file mode 100644 index 0000000..2416203 --- /dev/null +++ b/project/portfolios/migrations/0002_alter_professionalexperience_description.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.1 on 2023-06-15 19:06 + +import ckeditor.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="professionalexperience", + name="description", + field=ckeditor.fields.RichTextField(blank=True, null=True), + ), + ] diff --git a/project/portfolios/migrations/__init__.py b/project/portfolios/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/portfolios/models.py b/project/portfolios/models.py new file mode 100644 index 0000000..35f564a --- /dev/null +++ b/project/portfolios/models.py @@ -0,0 +1,109 @@ +from django.db import models +from django.conf import settings +from django.utils import dateformat +from django.utils.timezone import datetime +from django.utils.translation import gettext_lazy as _ +from utils.helpers import CustomModelManager +from utils.snippets import autoslugWithFieldAndUUID, image_as_base64, get_static_file_path +from utils.image_upload_helpers import get_professional_experience_company_image_path +from ckeditor.fields import RichTextField + + +""" *************** Professional Experience *************** """ + + +@autoslugWithFieldAndUUID(fieldname="company") +class ProfessionalExperience(models.Model): + """ + Professional Experience model. + Details: Includes Job Experiences and other professional experiences. + """ + class JobType(models.TextChoices): + FULL_TIME = _('Full Time'), _('Full Time') + PART_TIME = _('Part Time'), _('Part Time') + CONTRACTUAL = _('Contractual'), _('Contractual') + REMOTE = _('Remote'), _('Remote') + + slug = models.SlugField(max_length=255, unique=True) + company = models.CharField(max_length=150) + company_image = models.ImageField(upload_to=get_professional_experience_company_image_path, blank=True, null=True) + company_url = models.URLField(blank=True, null=True) + address = models.CharField(max_length=254, blank=True, null=True) + designation = models.CharField(max_length=150) + job_type = models.CharField(max_length=20, choices=JobType.choices, default=JobType.FULL_TIME) + start_date = models.DateField() + end_date = models.DateField(blank=True, null=True) + currently_working = models.BooleanField(default=False) + description = RichTextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'professional_experience' + verbose_name = _('Professional Experience') + verbose_name_plural = _('Professional Experiences') + ordering = ('-currently_working', '-start_date') + get_latest_by = "created_at" + + def __str__(self): + return self.company + + def get_duration(self): + if self.end_date is None and not self.currently_working: + raise ValueError("End date is required to calculate duration in days. Please provide end date or mark as currently working.") + if self.currently_working and self.end_date is not None: + raise ValueError("End date is not required when marked as currently working. Please remove end date or mark as not currently working.") + + end_date = None + if self.end_date is not None: + end_date = self.end_date.strftime("%b %Y") + if self.currently_working: + end_date = "Present" + start_date = self.start_date.strftime("%b %Y") + return f"{start_date} - {end_date}" + + def get_duration_in_days(self): + if self.end_date is None and not self.currently_working: + raise ValueError("End date is required to calculate duration in days. Please provide end date or mark as currently working.") + if self.currently_working and self.end_date is not None: + raise ValueError("End date is not required when marked as currently working. Please remove end date or mark as not currently working.") + + end_date = None + if self.end_date is not None: + end_date = self.end_date + if self.currently_working: + end_date = datetime.now().date() + + duration = end_date - self.start_date + + years = duration.days // 365 + months = (duration.days % 365) // 30 + days = (duration.days % 365) % 30 + + duration_str = "" + if years > 0: + duration_str += f"{years} Year{'s' if years > 1 else ''}, " + if months > 0: + duration_str += f"{months} Month{'s' if months > 1 else ''}, " + if days > 0: + duration_str += f"{days} Day{'s' if days > 1 else ''}" + + return duration_str + + def get_end_date(self): + if self.currently_working: + return _('Present') + elif self.end_date: + # return formatted date (supporting translation) + return dateformat.format(self.end_date, "F Y") + return _('Not Specified') + + def get_company_image(self): + if self.company_image: + image_path = settings.MEDIA_ROOT + self.company_image.url.lstrip("/media/") + else: + image_path = get_static_file_path("icons/office-building.png") + return image_as_base64(image_path) diff --git a/project/portfolios/tests.py b/project/portfolios/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/project/portfolios/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/project/portfolios/views.py b/project/portfolios/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/project/portfolios/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/project/public/staticfiles/icons/office-building.png b/project/public/staticfiles/icons/office-building.png new file mode 100644 index 0000000000000000000000000000000000000000..681483c68d1d5873c21a8ae31eb396008d462697 GIT binary patch literal 17545 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 literal 0 HcmV?d00001 diff --git a/project/pyproject.toml b/project/pyproject.toml index 799ebec..154dd18 100644 --- a/project/pyproject.toml +++ b/project/pyproject.toml @@ -18,6 +18,7 @@ drf-yasg = "^1.21.5" django-rest-knox = "^4.2.0" pillow = "^9.5.0" django-cors-headers = "^4.1.0" +django-ckeditor = "^6.5.1" [tool.poetry.dev-dependencies] diff --git a/project/requirements.txt b/project/requirements.txt index 0d43449..e239313 100644 --- a/project/requirements.txt +++ b/project/requirements.txt @@ -8,7 +8,9 @@ charset-normalizer==3.1.0 ; python_full_version == "3.9.13" coreapi==2.3.3 ; python_full_version == "3.9.13" coreschema==0.0.4 ; python_full_version == "3.9.13" cryptography==41.0.1 ; python_full_version == "3.9.13" +django-ckeditor==6.5.1 ; python_full_version == "3.9.13" django-cors-headers==4.1.0 ; python_full_version == "3.9.13" +django-js-asset==2.0.0 ; python_full_version == "3.9.13" django-rest-knox==4.2.0 ; python_full_version == "3.9.13" django==4.2.1 ; python_full_version == "3.9.13" djangorestframework==3.14.0 ; python_full_version == "3.9.13" diff --git a/project/users/api/__init__.py b/project/users/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/users/api/serializers.py b/project/users/api/serializers.py index c5040d3..b995b96 100644 --- a/project/users/api/serializers.py +++ b/project/users/api/serializers.py @@ -7,7 +7,7 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = get_user_model() - fields = [ + fields = ( "id", "username", "email", @@ -29,8 +29,8 @@ class Meta: "date_joined", "last_login", "updated_at", - ] - read_only_fields = [ + ) + read_only_fields = ( "id", "username", "is_active", @@ -40,7 +40,7 @@ class Meta: "is_superuser", "date_joined", "last_login", - ] + ) def get_image(self, obj): return obj.get_user_image() diff --git a/project/users/migrations/0001_initial.py b/project/users/migrations/0001_initial.py index b86c936..eabce50 100644 --- a/project/users/migrations/0001_initial.py +++ b/project/users/migrations/0001_initial.py @@ -52,7 +52,7 @@ class Migration(migrations.Migration): models.ImageField( blank=True, null=True, - upload_to=utils.image_upload_helpers.upload_user_image_path, + upload_to=utils.image_upload_helpers.get_user_image_path, ), ), ( diff --git a/project/users/models.py b/project/users/models.py index 46eda98..fd4e4dc 100644 --- a/project/users/models.py +++ b/project/users/models.py @@ -9,9 +9,8 @@ from django.utils import timezone from django.http import Http404 from utils.snippets import autoslugFromUUID, generate_unique_username_from_email -from utils.image_upload_helpers import upload_user_image_path +from utils.image_upload_helpers import get_user_image_path from django.utils.translation import gettext_lazy as _ -from django.templatetags.static import static from django.conf import settings from utils.snippets import image_as_base64, get_static_file_path @@ -90,7 +89,7 @@ class Gender(models.TextChoices): gender = models.CharField( max_length=20, choices=Gender.choices, blank=True, null=True ) - image = models.ImageField(upload_to=upload_user_image_path, null=True, blank=True) + image = models.ImageField(upload_to=get_user_image_path, null=True, blank=True) dob = models.DateField(null=True, blank=True, verbose_name=_("date of birth")) website = models.URLField(null=True, blank=True) contact = models.CharField(max_length=30, null=True, blank=True) @@ -143,7 +142,7 @@ def get_user_image(self): if image_path: return image_as_base64(image_path) - return None + return @receiver(pre_save, sender=User) diff --git a/project/utils/helpers.py b/project/utils/helpers.py index 6e5b759..94f8833 100644 --- a/project/utils/helpers.py +++ b/project/utils/helpers.py @@ -1,6 +1,11 @@ from rest_framework.response import Response -from functools import wraps +from rest_framework.viewsets import ModelViewSet from rest_framework.renderers import JSONRenderer +from rest_framework import permissions +from django.db import models +from django.http import Http404 +from django.utils.translation import gettext_lazy as _ +from functools import wraps class ResponseWrapper(Response, JSONRenderer): @@ -8,9 +13,6 @@ def __init__( self, data=None, error_code=None, - template_name=None, - headers=None, - exception=False, content_type=None, error_message=None, message=None, @@ -110,3 +112,46 @@ def wrapped_finalize_response(self, request, response, *args, **kwargs): viewset_cls.finalize_response = wrapped_finalize_response return viewset_cls + + +class CustomModelManager(models.Manager): + """ + Custom Model Manager + actions: all(), get_by_id(id), get_by_slug(slug) + """ + def all(self): + return self.get_queryset() + + def get_by_id(self, id): + try: + return self.get(id=id) + except self.model.DoesNotExist: + raise Http404(_("Not Found !!!")) + except self.model.MultipleObjectsReturned: + return self.get_queryset().filter(id=id).first() + except Exception: + raise Http404(_("Something went wrong !!!")) + + def get_by_slug(self, slug): + try: + return self.get(slug=slug) + except self.model.DoesNotExist: + raise Http404(_("Not Found !!!")) + except self.model.MultipleObjectsReturned: + return self.get_queryset().filter(id=id).first() + except Exception: + raise Http404(_("Something went wrong !!!")) + + +class ProjectGenericModelViewset(ModelViewSet): + permission_classes = (permissions.IsAuthenticated,) + pagination_class = None + lookup_field = "slug" + + + def get_queryset(self): + queryset = super().get_queryset() + limit = self.request.GET.get('_limit') + if limit: + queryset = queryset[:int(limit)] + return queryset diff --git a/project/utils/image_upload_helpers.py b/project/utils/image_upload_helpers.py index e3e2f6b..a715042 100644 --- a/project/utils/image_upload_helpers.py +++ b/project/utils/image_upload_helpers.py @@ -13,8 +13,16 @@ def get_filename(filepath): # User Image Path -def upload_user_image_path(instance, filename): +def get_user_image_path(instance, filename): new_filename = get_filename(filename) return "Users/{username}/Images/{final_filename}".format( username=slugify(instance.username[:50]), final_filename=new_filename ) + + +# Professional Experience Company Image Path +def get_professional_experience_company_image_path(instance, filename): + new_filename = get_filename(filename) + return "ProfessionalExperiences/{company}/Images/{final_filename}".format( + company=slugify(instance.company[:50]), final_filename=new_filename + ) diff --git a/project/utils/mixins.py b/project/utils/mixins.py new file mode 100644 index 0000000..2a8ec44 --- /dev/null +++ b/project/utils/mixins.py @@ -0,0 +1,21 @@ + + +""" +----------------------- * Custom Model Admin Mixins * ----------------------- +""" + + +class CustomModelAdminMixin(object): + """ + DOCSTRING for CustomModelAdminMixin: + This model mixing automatically displays all fields of a model in admin panel following the criteria. + code: @ Numan Ibn Mazid + """ + + def __init__(self, model, admin_site): + self.list_display = [ + field.name + for field in model._meta.fields + if field.get_internal_type() != "TextField" + ] + super(CustomModelAdminMixin, self).__init__(model, admin_site) diff --git a/project/utils/snippets.py b/project/utils/snippets.py index 6b220b6..af0974e 100644 --- a/project/utils/snippets.py +++ b/project/utils/snippets.py @@ -171,38 +171,38 @@ def simple_random_string_with_timestamp(size=None): # return decorator -# def autoslugWithFieldAndUUID(fieldname): -# """[Generates auto slug integrating model's field value and UUID] +def autoslugWithFieldAndUUID(fieldname): + """[Generates auto slug integrating model's field value and UUID] -# Args: -# fieldname ([str]): [Model field name to use to generate slug] -# """ + Args: + fieldname ([str]): [Model field name to use to generate slug] + """ -# def decorator(model): -# # some sanity checks first -# assert hasattr(model, fieldname), f"Model has no field {fieldname}" -# assert hasattr(model, "slug"), "Model is missing a slug field" + def decorator(model): + # some sanity checks first + assert hasattr(model, fieldname), f"Model has no field {fieldname}" + assert hasattr(model, "slug"), "Model is missing a slug field" -# @receiver(models.signals.pre_save, sender=model, weak=False) -# def generate_slug(sender, instance, *args, raw=False, **kwargs): -# if not raw and not instance.slug: -# source = getattr(instance, fieldname) -# try: -# slug = slugify(source)[:123] + "-" + str(uuid.uuid4()) -# Klass = instance.__class__ -# qs_exists = Klass.objects.filter(slug=slug).exists() -# if qs_exists: -# new_slug = "{slug}-{randstr}".format( -# slug=slug, -# randstr=random_string_generator(size=4) -# ) -# instance.slug = new_slug -# else: -# instance.slug = slug -# except Exception as e: -# instance.slug = simple_random_string() -# return model -# return decorator + @receiver(models.signals.pre_save, sender=model, weak=False) + def generate_slug(sender, instance, *args, raw=False, **kwargs): + if not raw and not instance.slug: + source = getattr(instance, fieldname) + try: + slug = slugify(source)[:123] + "-" + str(uuid.uuid4()) + Klass = instance.__class__ + qs_exists = Klass.objects.filter(slug=slug).exists() + if qs_exists: + new_slug = "{slug}-{randstr}".format( + slug=slug, + randstr=random_string_generator(size=4) + ) + instance.slug = new_slug + else: + instance.slug = slug + except Exception as e: + instance.slug = simple_random_string() + return model + return decorator # def autoslugFromField(fieldname): @@ -319,7 +319,7 @@ def get_static_file_path(static_path): static_file = finders.find(static_path) if static_file: return static_file - return None + return def image_as_base64(image_file): @@ -328,7 +328,7 @@ def image_as_base64(image_file): """ if not os.path.isfile(image_file): print(f"Image file not found: {image_file}") - return None + return # Get the file extension dynamically extension = os.path.splitext(image_file)[1][1:] From abaa67eaecade3f79de55db9af317ecf1dc0782b Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Sat, 17 Jun 2023 00:39:39 +0600 Subject: [PATCH 07/45] Skills Section Added #35 #36 --- frontend/components/Home/SkillSection.tsx | 57 +++++------ frontend/lib/backendAPI.ts | 36 +++---- frontend/lib/types.ts | 10 +- project/config/api_router.py | 4 +- project/config/views.py | 30 ------ project/portfolios/admin.py | 14 ++- .../api/professional_experiences/views.py | 16 ++-- project/portfolios/api/skills/__init__.py | 0 project/portfolios/api/skills/routers.py | 5 + project/portfolios/api/skills/serializers.py | 14 +++ project/portfolios/api/skills/views.py | 13 +++ ...ll_alter_professionalexperience_company.py | 69 ++++++++++++++ .../migrations/0004_alter_skill_level.py | 30 ++++++ .../migrations/0005_alter_skill_options.py | 22 +++++ .../migrations/0006_alter_skill_level.py | 30 ++++++ .../migrations/0007_alter_skill_level.py | 23 +++++ .../migrations/0008_alter_skill_order.py | 18 ++++ .../migrations/0009_alter_skill_slug.py | 18 ++++ ...sionalexperience_slug_alter_skill_order.py | 23 +++++ project/portfolios/models.py | 90 ++++++++++++++++-- .../{office-building.png => company.png} | Bin project/public/staticfiles/icons/skill.png | Bin 0 -> 42453 bytes project/users/api/views.py | 8 +- project/utils/image_upload_helpers.py | 8 ++ 24 files changed, 432 insertions(+), 106 deletions(-) create mode 100644 project/portfolios/api/skills/__init__.py create mode 100644 project/portfolios/api/skills/routers.py create mode 100644 project/portfolios/api/skills/serializers.py create mode 100644 project/portfolios/api/skills/views.py create mode 100644 project/portfolios/migrations/0003_skill_alter_professionalexperience_company.py create mode 100644 project/portfolios/migrations/0004_alter_skill_level.py create mode 100644 project/portfolios/migrations/0005_alter_skill_options.py create mode 100644 project/portfolios/migrations/0006_alter_skill_level.py create mode 100644 project/portfolios/migrations/0007_alter_skill_level.py create mode 100644 project/portfolios/migrations/0008_alter_skill_order.py create mode 100644 project/portfolios/migrations/0009_alter_skill_slug.py create mode 100644 project/portfolios/migrations/0010_alter_professionalexperience_slug_alter_skill_order.py rename project/public/staticfiles/icons/{office-building.png => company.png} (100%) create mode 100644 project/public/staticfiles/icons/skill.png diff --git a/frontend/components/Home/SkillSection.tsx b/frontend/components/Home/SkillSection.tsx index e3107f6..9ae942a 100644 --- a/frontend/components/Home/SkillSection.tsx +++ b/frontend/components/Home/SkillSection.tsx @@ -1,18 +1,14 @@ -import { FadeContainer, popUp } from "../../content/FramerMotionVariants" -import { HomeHeading } from "../../pages" -import { motion } from "framer-motion" -import { useDarkMode } from "@context/darkModeContext" -import * as WindowsAnimation from "@lib/windowsAnimation" -import React from "react" +import { FadeContainer, popUp } from '../../content/FramerMotionVariants' +import { HomeHeading } from '../../pages' +import { motion } from 'framer-motion' +import React from 'react' import { useEffect, useState } from 'react' import Image from 'next/image' -import { getAllSkills } from "@lib/backendAPI" -import { SkillType } from "@lib/types" - +import { getAllSkills } from '@lib/backendAPI' +import { SkillType } from '@lib/types' export default function SkillSection() { - const { isDarkMode } = useDarkMode() - const [skills, setSkills] = useState([]) + const [skills, setSkills] = useState([]) useEffect(() => { fetchSkills() @@ -38,31 +34,36 @@ export default function SkillSection() { whileInView="visible" variants={FadeContainer} viewport={{ once: true }} - className="grid grid-cols-3 gap-4 my-10" + className="grid grid-cols-4 gap-4 my-10" > {skills.map((skill: SkillType, index) => { + const level = Number(skill.level) || 0 // Convert level to a number or use 0 if it's null or invalid + const progressPercentage = (level / 5) * 100 // Calculate the progress percentage + const progressBarStyle = { + width: `${progressPercentage}%`, + } + return ( ) => - WindowsAnimation.showHoverAnimation(e, isDarkMode) - } - onMouseLeave={(e: React.MouseEvent) => - WindowsAnimation.removeHoverAnimation(e) - } - className="flex items-center justify-center gap-4 p-4 origin-center transform border border-gray-300 rounded-sm sm:justify-start bg-gray-50 hover:bg-white dark:bg-darkPrimary hover:dark:bg-darkSecondary dark:border-neutral-700 md:origin-top group" + title={skill.title} + className="p-2 origin-center transform border border-gray-300 rounded-sm sm:justify-start bg-gray-50 hover:bg-white dark:bg-darkPrimary hover:dark:bg-darkSecondary dark:border-neutral-700 md:origin-top group" > -
- {/* @ts-ignore */} - {/* */} - Skill Image - {/* {skill.icon} */} +
+
+ {skill.title} +
+ +

+ {skill.title} +

-

- {skill.name} -

+ {skill.level !== null ? ( +
+
+
+ ) : null} ) })} diff --git a/frontend/lib/backendAPI.ts b/frontend/lib/backendAPI.ts index b1a7a6e..65b38c4 100644 --- a/frontend/lib/backendAPI.ts +++ b/frontend/lib/backendAPI.ts @@ -73,35 +73,29 @@ export const getAllExperiences = async (length?: number | undefined) => { // *** SKILLS *** // Skills URL -const SKILLS_PATH = "/todos?_limit=10" -const SKILLS_ENDPOINT = BACKEND_API_BASE_URL + SKILLS_PATH +const SKILLS_PATH = "skills/" +const SKILLS_ENDPOINT = "http://127.0.0.1:8000/api/" + SKILLS_PATH /** * Makes a request to the BACKEND API to retrieve all Skills Data. */ export const getAllSkills = async () => { - // Make a request to the DEV API to retrieve a specific page of posts const allSkills = await fetch( - SKILLS_ENDPOINT - // { - // headers: { - // api_key: DEV_API!, - // }, - // } + SKILLS_ENDPOINT, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } ) - .then((response) => response.json()) - .catch((error) => console.log('Error fetching skills:', error)) - // TODO:Integrate with backend API - // ******* Faking data Starts ******* - const fakeSkillsData = allSkills.map((_skill: { title: string }, index: number) => ({ - name: `Python ${index + 1}`, - icon: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQYY0pvHu6oaaJRADcCoacoP5BKwJN0i1nqFNCnmKvN&s" - })) - // Need to return `fakeSkillsData` - // ******* Faking data Ends ******* - - return fakeSkillsData + if (allSkills.ok) { + const responseData = await allSkills.json() + return responseData.data + } else { + const errorMessage = `Error fetching Skills: ${allSkills.status} ${allSkills.statusText}` + console.log(errorMessage) + } } diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts index 8bcad32..fc00a02 100644 --- a/frontend/lib/types.ts +++ b/frontend/lib/types.ts @@ -103,8 +103,14 @@ export type ExperienceType = { } export type SkillType = { - name: string - icon: string + id: number + slug: string + title: string + image: string + level: string + order: number + created_at: string + updated_at: string } export type CertificateType = { diff --git a/project/config/api_router.py b/project/config/api_router.py index c83ad81..0b7e2f3 100644 --- a/project/config/api_router.py +++ b/project/config/api_router.py @@ -1,10 +1,8 @@ -from .views import ExampleView from .router import router from users.api.routers import * from portfolios.api.professional_experiences.routers import * +from portfolios.api.skills.routers import * -router.register("example", ExampleView, basename="example") - app_name = "api" urlpatterns = router.urls diff --git a/project/config/views.py b/project/config/views.py index e8cb8bf..1e061af 100644 --- a/project/config/views.py +++ b/project/config/views.py @@ -1,35 +1,5 @@ -from knox.auth import TokenAuthentication -from rest_framework.permissions import IsAuthenticated -from rest_framework.viewsets import ViewSet from django.views.generic import TemplateView -from utils.helpers import custom_response_wrapper -from rest_framework.response import Response class IndexView(TemplateView): template_name = "index.html" - - -@custom_response_wrapper -class ExampleView(ViewSet): - authentication_classes = (TokenAuthentication,) - permission_classes = (IsAuthenticated,) - - def list(self, request): - # return ResponseWrapper({"Demo": "Hello, world! This is LIST action"}) - return Response({"Demo": "Hello, world! This is LIST action"}) - - def create(self, request): - return Response({"Demo": "Hello, world! This is CREATE action"}) - - def retrieve(self, request, pk=None): - return Response({"Demo": "Hello, world! This is RETRIEVE action"}) - - def update(self, request, pk=None): - return Response({"Demo": "Hello, world! This is UPDATE action"}) - - def partial_update(self, request, pk=None): - return Response({"Demo": "Hello, world! This is PARTIAL UPDATE action"}) - - def destroy(self, request, pk=None): - return Response({"Demo": "Hello, world! This is DESTROY action"}) diff --git a/project/portfolios/admin.py b/project/portfolios/admin.py index 0fce53f..8d52d2c 100644 --- a/project/portfolios/admin.py +++ b/project/portfolios/admin.py @@ -2,7 +2,7 @@ from django.db import models from utils.mixins import CustomModelAdminMixin from portfolios.models import ( - ProfessionalExperience, + ProfessionalExperience, Skill ) from ckeditor.widgets import CKEditorWidget @@ -18,5 +18,15 @@ class ProfessionalExperienceAdmin(CustomModelAdminMixin, admin.ModelAdmin): class Meta: model = ProfessionalExperience - admin.site.register(ProfessionalExperience, ProfessionalExperienceAdmin) + + +# ---------------------------------------------------- +# *** Skill *** +# ---------------------------------------------------- + +class SkillAdmin(CustomModelAdminMixin, admin.ModelAdmin): + class Meta: + model = Skill + +admin.site.register(Skill, SkillAdmin) diff --git a/project/portfolios/api/professional_experiences/views.py b/project/portfolios/api/professional_experiences/views.py index 345e842..99af409 100644 --- a/project/portfolios/api/professional_experiences/views.py +++ b/project/portfolios/api/professional_experiences/views.py @@ -1,13 +1,13 @@ -from utils.helpers import custom_response_wrapper, ProjectGenericModelViewset -from portfolios.models import ( - ProfessionalExperience, -) -from portfolios.api.professional_experiences.serializers import ( - ProfessionalExperienceSerializer -) +from rest_framework import permissions +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin +from utils.helpers import custom_response_wrapper +from portfolios.models import ProfessionalExperience +from portfolios.api.professional_experiences.serializers import ProfessionalExperienceSerializer @custom_response_wrapper -class ProfessionalExperienceViewset(ProjectGenericModelViewset): +class ProfessionalExperienceViewset(GenericViewSet, ListModelMixin): + permission_classes = (permissions.IsAuthenticated,) queryset = ProfessionalExperience.objects.all() serializer_class = ProfessionalExperienceSerializer diff --git a/project/portfolios/api/skills/__init__.py b/project/portfolios/api/skills/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/portfolios/api/skills/routers.py b/project/portfolios/api/skills/routers.py new file mode 100644 index 0000000..396ea91 --- /dev/null +++ b/project/portfolios/api/skills/routers.py @@ -0,0 +1,5 @@ +from config.router import router +from portfolios.api.skills.views import SkillViewset + + +router.register("skills", SkillViewset, basename="skills") diff --git a/project/portfolios/api/skills/serializers.py b/project/portfolios/api/skills/serializers.py new file mode 100644 index 0000000..e4fce8b --- /dev/null +++ b/project/portfolios/api/skills/serializers.py @@ -0,0 +1,14 @@ +from rest_framework import serializers +from portfolios.models import Skill + + +class SkillSerializer(serializers.ModelSerializer): + image = serializers.SerializerMethodField() + + class Meta: + model = Skill + fields = "__all__" + read_only_fields = ("id", "slug", "created_at", "updated_at") + + def get_image(self, obj): + return obj.get_image() diff --git a/project/portfolios/api/skills/views.py b/project/portfolios/api/skills/views.py new file mode 100644 index 0000000..83c64dd --- /dev/null +++ b/project/portfolios/api/skills/views.py @@ -0,0 +1,13 @@ +from rest_framework import permissions +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin +from utils.helpers import custom_response_wrapper +from portfolios.models import Skill +from portfolios.api.skills.serializers import SkillSerializer + + +@custom_response_wrapper +class SkillViewset(GenericViewSet, ListModelMixin): + permission_classes = (permissions.IsAuthenticated,) + queryset = Skill.objects.all() + serializer_class = SkillSerializer diff --git a/project/portfolios/migrations/0003_skill_alter_professionalexperience_company.py b/project/portfolios/migrations/0003_skill_alter_professionalexperience_company.py new file mode 100644 index 0000000..62ea9ed --- /dev/null +++ b/project/portfolios/migrations/0003_skill_alter_professionalexperience_company.py @@ -0,0 +1,69 @@ +# Generated by Django 4.2.1 on 2023-06-16 16:53 + +from django.db import migrations, models +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0002_alter_professionalexperience_description"), + ] + + operations = [ + migrations.CreateModel( + name="Skill", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("slug", models.SlugField(max_length=255, unique=True)), + ("title", models.CharField(max_length=150, unique=True)), + ( + "image", + models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_skill_image_path, + ), + ), + ( + "level", + models.PositiveSmallIntegerField( + blank=True, + choices=[ + ("None", "-----"), + ("1", "1"), + ("2", "2"), + ("3", "3"), + ("4", "4"), + ("5", "5"), + ], + default="None", + null=True, + ), + ), + ("order", models.PositiveIntegerField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Skill", + "verbose_name_plural": "Skills", + "db_table": "skill", + "ordering": ["-created_at"], + "get_latest_by": "created_at", + }, + ), + migrations.AlterField( + model_name="professionalexperience", + name="company", + field=models.CharField(max_length=150, unique=True), + ), + ] diff --git a/project/portfolios/migrations/0004_alter_skill_level.py b/project/portfolios/migrations/0004_alter_skill_level.py new file mode 100644 index 0000000..66c12cd --- /dev/null +++ b/project/portfolios/migrations/0004_alter_skill_level.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.1 on 2023-06-16 16:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0003_skill_alter_professionalexperience_company"), + ] + + operations = [ + migrations.AlterField( + model_name="skill", + name="level", + field=models.CharField( + blank=True, + choices=[ + ("None", "-----"), + ("1", "1"), + ("2", "2"), + ("3", "3"), + ("4", "4"), + ("5", "5"), + ], + default="None", + null=True, + ), + ), + ] diff --git a/project/portfolios/migrations/0005_alter_skill_options.py b/project/portfolios/migrations/0005_alter_skill_options.py new file mode 100644 index 0000000..ef6f339 --- /dev/null +++ b/project/portfolios/migrations/0005_alter_skill_options.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.1 on 2023-06-16 17:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0004_alter_skill_level"), + ] + + operations = [ + migrations.AlterModelOptions( + name="skill", + options={ + "get_latest_by": "created_at", + "ordering": ("order", "-created_at"), + "verbose_name": "Skill", + "verbose_name_plural": "Skills", + }, + ), + ] diff --git a/project/portfolios/migrations/0006_alter_skill_level.py b/project/portfolios/migrations/0006_alter_skill_level.py new file mode 100644 index 0000000..7ae1632 --- /dev/null +++ b/project/portfolios/migrations/0006_alter_skill_level.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.1 on 2023-06-16 18:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0005_alter_skill_options"), + ] + + operations = [ + migrations.AlterField( + model_name="skill", + name="level", + field=models.CharField( + blank=True, + choices=[ + ("0", "-----"), + ("1", "1"), + ("2", "2"), + ("3", "3"), + ("4", "4"), + ("5", "5"), + ], + default="0", + null=True, + ), + ), + ] diff --git a/project/portfolios/migrations/0007_alter_skill_level.py b/project/portfolios/migrations/0007_alter_skill_level.py new file mode 100644 index 0000000..4976e18 --- /dev/null +++ b/project/portfolios/migrations/0007_alter_skill_level.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.1 on 2023-06-16 18:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0006_alter_skill_level"), + ] + + operations = [ + migrations.AlterField( + model_name="skill", + name="level", + field=models.CharField( + blank=True, + choices=[("1", "1"), ("2", "2"), ("3", "3"), ("4", "4"), ("5", "5")], + default=None, + null=True, + ), + ), + ] diff --git a/project/portfolios/migrations/0008_alter_skill_order.py b/project/portfolios/migrations/0008_alter_skill_order.py new file mode 100644 index 0000000..7190c74 --- /dev/null +++ b/project/portfolios/migrations/0008_alter_skill_order.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2023-06-16 18:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0007_alter_skill_level"), + ] + + operations = [ + migrations.AlterField( + model_name="skill", + name="order", + field=models.PositiveIntegerField(blank=True, null=True), + ), + ] diff --git a/project/portfolios/migrations/0009_alter_skill_slug.py b/project/portfolios/migrations/0009_alter_skill_slug.py new file mode 100644 index 0000000..bccf12a --- /dev/null +++ b/project/portfolios/migrations/0009_alter_skill_slug.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2023-06-16 18:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0008_alter_skill_order"), + ] + + operations = [ + migrations.AlterField( + model_name="skill", + name="slug", + field=models.SlugField(blank=True, max_length=255, unique=True), + ), + ] diff --git a/project/portfolios/migrations/0010_alter_professionalexperience_slug_alter_skill_order.py b/project/portfolios/migrations/0010_alter_professionalexperience_slug_alter_skill_order.py new file mode 100644 index 0000000..9a47b2d --- /dev/null +++ b/project/portfolios/migrations/0010_alter_professionalexperience_slug_alter_skill_order.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.1 on 2023-06-16 18:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0009_alter_skill_slug"), + ] + + operations = [ + migrations.AlterField( + model_name="professionalexperience", + name="slug", + field=models.SlugField(blank=True, max_length=255, unique=True), + ), + migrations.AlterField( + model_name="skill", + name="order", + field=models.PositiveIntegerField(blank=True), + ), + ] diff --git a/project/portfolios/models.py b/project/portfolios/models.py index 35f564a..ae51817 100644 --- a/project/portfolios/models.py +++ b/project/portfolios/models.py @@ -2,10 +2,15 @@ from django.conf import settings from django.utils import dateformat from django.utils.timezone import datetime +from django.db.models.signals import pre_save +from django.db.models import Max +from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ from utils.helpers import CustomModelManager from utils.snippets import autoslugWithFieldAndUUID, image_as_base64, get_static_file_path -from utils.image_upload_helpers import get_professional_experience_company_image_path +from utils.image_upload_helpers import ( + get_professional_experience_company_image_path, get_skill_image_path +) from ckeditor.fields import RichTextField @@ -19,13 +24,13 @@ class ProfessionalExperience(models.Model): Details: Includes Job Experiences and other professional experiences. """ class JobType(models.TextChoices): - FULL_TIME = _('Full Time'), _('Full Time') - PART_TIME = _('Part Time'), _('Part Time') - CONTRACTUAL = _('Contractual'), _('Contractual') - REMOTE = _('Remote'), _('Remote') + FULL_TIME = 'Full Time', _('Full Time') + PART_TIME = 'Part Time', _('Part Time') + CONTRACTUAL = 'Contractual', _('Contractual') + REMOTE = 'Remote', _('Remote') - slug = models.SlugField(max_length=255, unique=True) - company = models.CharField(max_length=150) + company = models.CharField(max_length=150, unique=True) + slug = models.SlugField(max_length=255, unique=True, blank=True) company_image = models.ImageField(upload_to=get_professional_experience_company_image_path, blank=True, null=True) company_url = models.URLField(blank=True, null=True) address = models.CharField(max_length=254, blank=True, null=True) @@ -105,5 +110,74 @@ def get_company_image(self): if self.company_image: image_path = settings.MEDIA_ROOT + self.company_image.url.lstrip("/media/") else: - image_path = get_static_file_path("icons/office-building.png") + image_path = get_static_file_path("icons/company.png") return image_as_base64(image_path) + + +""" *************** Skill *************** """ + + +@autoslugWithFieldAndUUID(fieldname="title") +class Skill(models.Model): + """ + Skill model. + """ + class Level(models.TextChoices): + One = 1, '1' + Two = 2, '2' + Three = 3, '3' + Four = 4, '4' + Five = 5, '5' + + title = models.CharField(max_length=150, unique=True) + slug = models.SlugField(max_length=255, unique=True, blank=True) + image = models.ImageField(upload_to=get_skill_image_path, blank=True, null=True) + level = models.CharField(choices=Level.choices, default=None, blank=True, null=True) + order = models.PositiveIntegerField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'skill' + verbose_name = _('Skill') + verbose_name_plural = _('Skills') + ordering = ('order', '-created_at') + get_latest_by = "created_at" + + def __str__(self): + return self.title + + def get_image(self): + if self.image: + image_path = settings.MEDIA_ROOT + self.image.url.lstrip("/media/") + else: + image_path = get_static_file_path("icons/skill.png") + return image_as_base64(image_path) + + +@receiver(pre_save, sender=Skill) +def generate_order(sender, instance, **kwargs): + """ + This method will generate order for new instances only. + Order will be generated automatically like 1, 2, 3, 4 and so on. + If any order is deleted then it will be reused. Like if 3 is deleted then next created order will be 3 instead of 5. + """ + if not instance.pk: # Only generate order for new instances + if instance.order is None: + deleted_orders = Skill.objects.filter(order__isnull=False).values_list('order', flat=True) + max_order = Skill.objects.aggregate(Max('order')).get('order__max') + + if deleted_orders: + deleted_orders = sorted(deleted_orders) + reused_order = None + for i in range(1, max_order + 2): + if i not in deleted_orders: + reused_order = i + break + if reused_order is not None: + instance.order = reused_order + else: + instance.order = max_order + 1 if max_order is not None else 1 diff --git a/project/public/staticfiles/icons/office-building.png b/project/public/staticfiles/icons/company.png similarity index 100% rename from project/public/staticfiles/icons/office-building.png rename to project/public/staticfiles/icons/company.png diff --git a/project/public/staticfiles/icons/skill.png b/project/public/staticfiles/icons/skill.png new file mode 100644 index 0000000000000000000000000000000000000000..1dc24365c233b5fb90deb86de92389492d5e79f0 GIT binary patch literal 42453 zcmd3Og6=j^lhT5Io3n6jb_CK@Rk1VNZ`vXZJ01P^|Mhftn@r*pT7TkwSF2$NGs z0f!fg$rtcDs-3KkBLrb}{riGTWWgc>FFtpY(soj_HFI+P=wJ%Dy1KG|via<2{L#*o z-PXZ8{ZNDyf~X-mNeOkgjQs_7FFlP}B$J@ zMKfb{bv4SUY6hG7`$1P*!1~6vZuO5_KKC^YD6 zlJf~YYpCwNmD4N&$l1e;2D_p*4P-VA??t${aunYx{=vyWqzMw-g>X8*%vvSwT-?^3 zot^EOZ<>{u6_}lJJ8(}O{H-qKok+op`~F^5(WjkiZ@N$lamv9b2Bqb21dqrY;0^L zhBlSWdKIKJ)@LHV;=PX$CpUK<(g@Y=qkfDQ?RW1mNj!%+NZG(3yUBS^6BWAl5{-F??38H5HPcDQ3d%|d6!ZG4Xj%L}Yv*h$0e>}-2OUELu1r{vg;Xdx(w)o?J4H{?wy zR^7vKsMy5t@Ng)Pv6+p{5G_4&vHD*Ia3{;|`fMh?U=6kfXcFHdz3#Sa)FZoptcfwr zAjytZh*U~}QXsFl%!#*{&;Bct&ikZMtX|o-ak^x)&~%X!D*|>!88vg_pFkX$WrX9x z!b0@62Vglc8!Er{l9F9=nuEQITS1zGWZ^$YL}NamKJC~B*J5|rHq22Gd_|z?$E5-$ z5oQ_|7Um7Rq{=3lMje!3Pi_w0Kf1a@T7WzDXr-mOJEQe!k9{yVH$Q+~B14I9FA_#b zf>`+{;HcMGc}NG@Y3_!H22M9NHjrROOa$RNG@_|ga8fqAI=JY6BCsi856 zj&Vm^r9Gn!7G+k5<_j*b%LtL&^)KD18;tVC3V)$L1Ewr_34B7|j!M%ZI3g5TTx)5G~j zwGj%aa>=u_e`8f~{Xb`C=hUXArnZ;yeTPGReS&4vd%oDqZAnQM1EY(hfA05mc2;MnZ*Bvkj{nmH zvnGQWx*2O>m&yGbzIfG7>+9>h+}zyqP0FNHRLRjXG1>k0^XZ>HeWL5@>l?1Et6QL_ z$A+Qk*lT0J$M#w0OfDe(09Rpnnp}nmKUMu(mGG%dK8jo1B~s&gm|$ zs9+EDTWW;m9K)7B3Bk<>B650d{(1aJgcEOvSB$j=$qnO~Ps`!7Qrkr+=0hf_dY663=E29=H^EntgQF(3pVYFh2!3`vUe>9bZu>TlCP%oPdz#fj*TY+N|rs4t5pC@2`JrmEI`dUocjEnj{Gj0vUW zSSF~nw3LO-HR0*ojB1M@9i6c{E76_5_^+&xkY^Hs8S(M)h1AE~T54)%Dl~#PFj>8l z;^I+V>#0Jp1=NDOnc3L9QQ)z=e8`KVQ=wx(2sx5Y%mDVKI`;+C#dV1e_5A4|esd3?>@3A_Qe{ z_uofE!0ZwH{{4FxwU|x}&GA-3xYSyv7RskG+-xhJvd5aC@sB z@%!{tHM{SBtJa4NF+MwG`{~}(8U|%dfUCL)kn^VwZhjL&NW$w!ht09NCU6uZ0*`vKk>>DZ+%ShU{F5dq z_Ud0;V!e2~t&k2*4;#^Y_3G8FVmE%6)MI31WS(Z}g$!`*G?gAQvYe!(q+J`)r^1oa zYahcT9lBXgHA*!JQLPz){Vku*{IUXW>N8*Z)7Q8-3D;H>8Y=?e>jOIC#h~_43xe8O zHfW~*K#Y?F<4t~keoPg~r+2bUQ&!sXt9AmETqGED0w*^BuN@MRHdI%iuHjt29=G5* z(@ei$ggF9$&cnm=9i4&%sbl!8WogH0O7f8c2TwFHF|icvE`3*_On$T|Qy$V7KMC{Z zA3u(*a~c7#hrWIPzCLVdXZR|8P?#=DgvS2cKj@O9hU4GimIC5YS#LW6thajINhnb_ z^Rf*62V#v}zN!H*9)h4pa#R;&UlFj8LuRbg zJ*d}a9iEz;d{N!&AG@(y-OsKjP@2&`wRb^-T|EfqJ6!rOD3(N1Q{^%iSSI{2*(;2| zvehi6-J0t#gFF{PBBDnC_cdxP(_=R*{tLmZ2Gf=e5c2RxR7T%8*>&K2tqkMdNJ&X$ z4l2fMp@e?Ysb+%#*~S#y#+gS!Tx^aLOZOHF%Jnz&yYnwp~ZBj0F)RaeTvhBY2l zmXP>$Ilfk^&fM_Z$h{LlHJw_C-nY)PrpaOCSUrKah^^@88TY2IQipe(Hft<~Eo&Z6 zCuUTOC(NsKvk);^>FF;gCnm~7>-cV9@AdT;eTWVwr=~Wu_APn|9V#0;?zNgGl~CT3 z@ma-cXlT5yn6zAKp#BHYmUL=WK79D#cB-8rPw#S9bBX2UmE`I(7e0g$SuUw-Pc4j$z7_8ps;noUxm81omr9U%NGKNVzg5NMhASgdh<(1T)` zUNW=aig610^qRZ^JM!Ts&IdcF%%Z?+T%Yq6Z;Qk+T2jatCc6$k-$vj);c&hUR_L0) zY&$Ycvt_bWi;4%T_ZR87<>$i$ReC)=@fA0o(uEwL)$L3;p zQm|~5kB8^WIOYy;%lWUX?0TMlge#%wwEg+>=TN9|{&**S((>VhdOV$ppmwXr#fD|w z-#~nWGT^M%{%x76aNlZmI~PTlr!?NrZ?%8F2x9fEtE)=_e}~I(I2{Il$85HHcd`^8 z%Ox!Gc4 z_a{S=_X(+~?gWg_UL+?Z%=d8e(Nq-!dz!7m9IKhNYL7AV3BqwGc_lN;_$PBCxhnG0 ztMvMnTI1|KlUv4qK9sQMn3$MGrISqv5(JyYMMXc^JW}ysvez?K^-D(nYuTd~R?n!z zj68yV)LBk4zXL3RKOn=H1t0>j&xj>i3_yqv1(7FRigr{d#y3?bwWp-CG(IXe5!QZ-1$XMhS!Y=zWUqwfRT5T3}jo9X^P35(II?&lGp;04=llnP9#n1$#4%I^I zjF0WNT~AZ+%J2e$Gfw1ww*E6UPywWn<+gDC>B$#OoWsut3WQn|mY(alWGHS)d0riQ ze*z{vdZy^`_B2RvNw3~|u6_vRd%ckgGIb;v&Kb}>(vgvod2{w{0ki@JR?TPqm#0UF zAMcDFa)4V$;LN1aVG}j3}pqZ4!#JXO%qj` zCw_-9^oaQlp`%$%GP#cr-H^j?3%W<_*n;Ds9*Lvr*M9dditoh>Qw$m}8^HLz+}#BS z&1g^7<{gJy#S6#TbmPTl1}$qu@prDoQ{tf?@IOgAS|NSd@>{{bjGNjs(ohTL@)o0y znC8er-$uZ2U*C?O-*BjE@p(dWvH?NxJgQC_pzaKry33)zU5v~+KH&PWNT$G`X~+%U zpT)q8qLZ*X3UZ_9@Pdpb@}Plsi$t#PWI z_|gxwScT!b_14;&nuMgjFCh!kVfi6+Ov72hmTz2Ym7-bf_awi)ITnNlrpBl{1<9k+ ziP8^=|7Zd{z17rQ9`!RHJ-)a71&qkp2+-Y+XL<%?}?4R zj!7ne?OCJVp|Jdb6o16;YN_ivI{NTa1$R%Jb!2_8BQto1&T0ySXd(R^5rLw zm(~OX1hoOX60sYdJ5FyM92|tII*>(qd7WzkW91Hi$?i%DO~cIuY-f8}M3h_Ob*Mp_ zut`B&zxTM>!xFtmR&tqsyq~8?#(?Ym)ipFcyoBsWd@YI^#@5)_cnbWVe#WIgpt;-c zTRog%PBriAGf*NIvn9`!cxrz?xHrWe$240{A0hnZ;aow@CzV0+a?z^dqBk+X^T#Kpp_ZA=KvMXeX`$T|( zVS_0n_27Ye(3{|pIDfpAKED&03W0!2toOiz;m z8MJL;V!{b|Z{}aDDJUr5nzGlTrtCn*W%}Jr#Io$j_H~b1bDKJ8k4BS0{QJlo)@g|{ zg&^6AVA)(m0jw5vLbp~PJ1e5T2}s~r^@RZY@s~+qF{Ep__F>Fy;UA~9!>`fXm(Z_U zjvLKaQq%ohZOF44?cKH*mQyJLU2XOkvOU4iSMGVI&0l_~Kr!%cDE@J0RjjBT4=AiZ z2*QrK<{&<5&U$JluqDOH%geW(V}wY=_n9oVxSyF^kI@yrzeIjZ14wZG&i3{;V9fGN z%*-A)KrE`I)v!`QxW@2H$JO>SpT+5`i?!sY88?TO^arRXqZUs=VXts$8?nf#JY!^E z*BIL=U4Hb*#XehWP08L4{@g-*7^d&G*`1e3`PmUAfu@bBLs(^KL!kmX=7x%KZCv{@ zAwVy_!E&xIFAwGzJClh7@pmECNPg0&P=JLOKR9)n@bS9Vx(Yzoxhce`db3_g{+;ah zuan=G@QP>YL8+o-svj6kniOQ6Q*ruMPZZBOTh7Mql+J(gjz-*6Q4U~Hbj~Xa&#U$1 zWu_^MLLAWbOL#)MJT_u~VFIczMp(AcPaOZZ0V;qXwu=FE(A3h>@}seFo^V=Y7c&@2 zhZWzErLC=N)}v$I=uBK(+{ZZ}VO#N>>aKi+n-A#-6FVG=#dAg@=nIZ5BoL2NMUi^D zQY}2nk!bFM;rQl&Q3x@VQ+NXF%A{;b$6zEK@`>!2zm>(Ov18o#gw$0mo}Ahdaq8X{ z0Nxk&aChlDv~~8&?tcQmCJB|E6Hl1$(jDE zd%59D$VO(_uR6hBTIIh8Q{{qWSgM)7NIp(@0{j<3o_=nU#YWTttU6;CL#hIgbWUM6 z$j4UiLx(k-9X~wb8Ep=^zkg*St|bSFC}axh(k=V|w@qtY7K?h)egR6r%SDo*U zVa^k90`CCxM*wPn!|lUkO&hzQpgS!cU0zFm{)TLj>iaNNxOuqch%Zs4X-M|u*zRv} zCsH?*O@+QG)cE0#1~{=^i?9!zH+*brKpk%#Oc8#>x_|uXhIU2jXG_a@BuY+z!m8;^ z<~Kt|fJegQF8`gh2JGFPot=fu%*?6rCje?^4lX^@)6?52i@jFioFT;(Cm~xa8=J>q zkb&8~2$Kt|aSwLqje1XZ6(U%dC>_y3{#XeqOA96=^_|Zun`(+@>cE*wKbIU!Im}}v zYrA=_a8-vWgxHhD3E+YEF)^GMI@G_`B?Ps@#}0Ecd@hIH(v6OQ<#s~GrnK7I+0ps& zwM3gWWYDa+ptyLaSazKNy9mJ!2I!La=g)iZYierbnZ=4nEku4#tzrr{9C)$mHI`Ew zLD@3PB@APE$;7|yMG~Hyj%=2PbZeuBF$?~Ei3=U}!6`V|ba%xKrclffm%1W*L5~>A z!Vnhf7yO-KZT|#n@%%yyiUS>|_#q`EUk>N+0$%M-m1K*6{o@g&uBoX> zWhr*Pc|GGcGKt{qk=bunm&#|mI8Qb-zxpfu2m;E*m$|t)&uGfmY3%gTfFt}vtc&yW z_uQI| z8N+YOwm+_GSM?-)Ee)9d;uH-$CjxU$wb<_abf%`JCSUPA%|@1LV4!zEcV`?y%M1D2 zw{M1fId4K!CT@wsBta0ks;jN#tR(%3@f_#pYCAkv)h;ozhV;vGDJs7yZDf7Ynm5?8 zC|-v;kyML8Re@w&+&?(}?qrnU`ujVi4FCznY;A1^Ubjcn&>~7-_)>{a92G_+ zqK**j`vv7uh1OfRQCk8%+b|3>skg-denvEMapWN*2o8e~oS zt#$tKKkxFpHp+d&gPNB>(6l>RY;AZY%fenhgXK5AbFbj&coxqpD#`fb1>+zQF=6`7 z>{tLfN~B&zxFr!eva{w3yl{Rn)nR?rP4xHoq8tStE{5mJr763M&(YVA3ZQ!&|J@|q z%)iNLOmFIVai82uV>Eh~;I3Iq4H;(-F7GZ67d-Eb#%lv#5Jjly>U-Q|eelfsh{rip zHiIP&6aq6PrD67*;YX!03!W@M6?XZTyd1G3-1;M*`Jjh1`Zxk(V?>55P*4gS(C!MyA$~{J%77EejQLpPEO7=Ah$z=V|fYv8$KYstWXHM z+{b_P=>OdVcTX-paadW3;Xx9bt z3rmFms|E1yYTRif!f3e1j9-|8lWxc`_Lm%3>(73*mfny6i_h;YV0+N^-I3O`Jt0I) z+Qec3DzF;Z^K`6~D+T%aQRxZ=yzd)q@`WK54WIz@V7ECQr}l;QeamjPCl3WgKbw!2 zci}nMrz4CwDSH_NJ0XH`7^LI6B$gEc+Rslm`(CY+?)^Gm+~T|(`h4uUm6sV!ttbyq z**FKcMT+3oh*IOIug=r*<&rL*wNnY<2mH1Xt`!%+J=lEw{7ba!hvll znpKdO34oZoSy54O(AwG>_N%3>OZW83+xkwF^sr@Bmex-O(xco7aSy{|UemQWoqA(C zfZQWvT?D67AW~yYGJAz5t!v;nO?d8b#U+uCvo<&V{y2YFhgv?d{f=q>kZQj>hRVog z#ghy^IGncIvaap#)KtAs@tqt!UINf9jIItB4A5lsPXdIX!r31m1IzOAjL$%-Y^j0Z zxHwYU*i%bOOXB-!I0BXUw<0-dda{idZ;Ko_g8u#_YLSF2Z05S@3$;rvB-glQ_ zOy7w<6F`SZ$EK3UXI&>oOt1EG*se+3iWy?R=wml(;ujP2qHSf8^(glnaE;v>m1#Uj z#7X&k^^k7$^Y#zy3;y?`8~*I!7)Q~2wE6>~q1>0)ewL6*D%=LpT4X$9D9N_TMmR42 z4h#(Z+TY(#1032}nz}AUhQhC=sJQrMqRsp6(beI!eGPtsw5|t`__;Q3PlZJMh_9B2 z(s$A}Qm>*zUpW%;Y{rPwE4f|u;02TKCb+$fQ%u6Yny@EddO(hBt-B>0DX&?Qq;@sJ;+4`{EH}wiQY!j6(4+~T=cn?Js@m^SoTYp zrit8(5h7oBymwQcK0^_178uWty)e`X7+8`#9iX1{tY0(!zEajwo%0xkluf1Z#6C>O zv=^y`e_W~3L%avkQ_{>FC`AG~zP69E2B4~FXk7FGJa0aKc$IVoXrE8|!sWsTVV4E` z%mE`}XDHx@&x6Ng9Gy)4%gH~)X;(bHEPrn#Q;Gb|ACjd@By?1{YJ-zux&5!#+Dp~G z(x@=eCSuW;^ZwlX1STo;OZ`jSv!6E(+=p#~oOLm4#GpRJ_AJq@+w%%91!iRD*f*Y{zDFe>n|^n z4Cpk9zr97X9D7FdaU+WowzMh3IorsadmzMydi}d0!>;TtGKbjRezI7*9irCaF+*uV zHliI{sbSf_{Dr97v0>||#r{BFU$M_#c|!_@lXi3wf_Q*4FTDKM5bsoxXncv(amae* z#j+%N%=VTPtp(=JvgUfZuh{(fe|Ab(9m4!@!}9_nHF?KF@4zou!(mUiEoaA`ddHp% z$L^best}VmvVSdEdQE$bha{=W>sE|Ln@UPsjMk!n8K;gJr-xoFL};Y(ioV3;OCqZF zY$%?X^i_Mn<3Y@S{9^c3nu!j$Z7$YaHqE2Y+)KPhr(+@;m78v*Fv!hcIetX(cWCEw zb+88c=@_MGUst8IEL%z8WbKBp?RE45%!owCjm&saeP7E*67}G!URjS$m#C>ytFZbk zM`-zEBUe;W{*-QB$=dI^U+?gJ66S^4`T3GwZMkuEmC(zozl<$Xqml3RS4c+E3BXnT z&|R%>)yMlcc#)qf@*3|y;;H<#t)()*zQ@I4Z*#{5ExsDoD^Dapu_`p_T;o}121k<{F)r-=;tYPoa2a(B@aVLuSZSRxZ&G!rCiYA%c@x4=BicC?e-=LD;vYku z?!_I5BKQCN9lxR~OItff5ht2h0#Kj=eptzH2<<<-50jlZd2RgP(V5}T4M7Y(QHs%M zZOdGYrRLF)(n=;jdKIE`-1|W`1Z~g0cJ0Kw2Vy@=1=_eGT}=w8bnv#neyX0sfjkr> z52fpLy={PkKOs}tU91ZYBN$fet_BEDmSmiLWBW;xA@I}Ri?)g+?Y#D9>&4NPI~bpT z>Z9fWg;>zeqvlUW?8E69E%MFo2~KSz zQlOX%OnvlDArm6F(z@KYk7eA1CF)>lm?5b@6LyocdWdR0QzMyqB#S)sMe>I`j*H9A z?|$Q&u-4*;8C$@T>@{JgcX*`Phc{980R&>T5)O|ntLX0XpM&-$80MpfQ z*MhDf`mkHU_w#^AWehPnM*wX;(+Ev(k>9IU`VwPQhUWU5ql3_uEXr{0bR*o*Vc&ru zPa7tGz7KG6yJNzV<##eF&cDP~+vaU~fU?nx{Onnl=T@dGpIfWGiHQlu`Q>HE)sRIH zYXp{fSowOxo$ghpvrst5C`?(;z)`M114Q?FmbJWXWiL@W*rUU|6 zfkTE6V|Z(@j~69zLCj(IKaOy*A7Jjn^YJy*^xBvWpZSF~WHBT8fzbeb%*N$KpREdgJlHr)c*0f)y zb~hxQngXQ?oih3d1A?hQH5V8b&CfMj$D6Cxp^0bOgy~MzC{`VH`|uA=Jac(u&-K+x zghkr=E(VZ^+62gUNu*Wy$>Qr)z3blaIYK&i*8H^!+1AWv3Ar6FYZPh`AwrSLuere1pf2!y zPdmkcqg{98Kldj#o|37KMh^Yp50!S?h7Vm15Jf7Q3-7WYydTUn&1%Uhk-9Dr7x3Qq zt${UV+gZnk09)=}Sy{m<9Cv?jY`pd#K%R;MPzyXYuO24TSmub17^XhnZK80Gy)pxX zRm<6jp|Mqe@b zJ)g2K@xK40fWg5*1JuoE2f%RB2F*N+>gp~wW!H(YM%%uj=>XvO5g^k?AOgm21=y1# z0}+>voBJ%_T{_>pssRqHjED|vUu%H?#}>qX(Lrq2}S0DK2&M_0yMEF zOTnR1FIx$E{g)vEFVEwjdQ&1c9G_?)SKwDbCcDIEW`4|Tdfo8-5ATd8D=P@?q z{!x}~f3+H68OR-=@YCdUKo=Bhf=ruFh*X(?J)z$cyug`m4t$^L+c?8^tQN`N^jUg^ zNQ`K{zo^=$qxve!Ni5y5Y69>6Tckvd7HVd;s>*v^r{u27MWx^QCavn+{jsoVO}JTm z@)p^TgKh*>J84L8D`P(^D@zMZ&nH~G!scdSpsjnif)$o2bBIXzxd#Es(*m1Z;8=~Q zN+9};qT*2UqR-ty;w$q#KIn>uJXxI_`Q2k^PT!N=@*)EQZ5YWZXUy%@0EY4RgBQTd zKF<(vv84+eaEeBF~0^oK%_v9Yl)6%`e>x6{B& zkelH@T4;97FMEEB35lV|80zQ3V6$5^^7UL%r6YT(&J2oMS)^V6$e+hD;k8J`k6MYK z(G3k6u0kBqY-0Un^fT`9l=KWlFPf>Gu+@pSBR$(+7s)Bqy32?haq+~lZhXqZNe;^pF^NlnSPCi8Q~P>o3Wx%)4iU4YHARsAJ@ zd}s6P*RP^8i$i5Sv?PQI zysZ68!sz|QcB#$##lTh*h5y{+z)OtDkz{@J zXmha)@f>sE-6h_sz?|2YviBO! z=A1a+`hvBs@GfD6k$>*wq%Jk0P16$Ra=ty^8WCIMj~9*y7kuesCbnUa^Raf-KSBml zz+~X?T_bk8>)N}>t*ktH4)uLmH^PPQ+Tam4uRE*G8p>h>+T0sbN_}PJA3GyCZ?>w2 zSe`8eaI($kys8MUxK{hvD8->x{LMuDXzBIqptY#>o|#`-D`Nkl(o*);?kE=!!%~d` zMEX#EhrP>+MN%Y4s>+Z!^L%X=b!B98PGT0?%<0*Jz#VvyBIrW`?s>>f6)Qv`+dr+c zmEwHQTa;=Zm?;@ma1Wu|ckc0}ki`bd$n6qua%3+{Z$6*%4OMz1t{8*wiHJnksJLqm z?dD^MhHOdpj1cs%)Q()#E2Zy?TIuAi`1|+o_0-d&N`)at-b0Ifbia0x#uxRtK3>@~ zGYiOW%bB#?a?dhAReP*JKIYz35u_CA-%#ENDtzjKi{(nW+{(=ERvxb*`Q$C?lXc=K zqF!uf;qWKtmG-x{905YDBu1v|pQ#Ze@WW`72K+_1kZU%F!}&snS_kiORE7ZG9IR)K(}rVk_PXWkxy+IvLQjIWYkJkH5hl4s&!~3T_cr`5BmJlm#sl- zo{t1)gm;p!c%9reMmE*O)7ShM<|q@9Y-#(BoQ+Rs^c9>Ay4|+e6`XiGU-RC#P3u%X z@_v1rB${RE_Oxf=yYk*HO6G9%Vz%Hi(^;$)Elww+Be?sZ|JcOx+hGgHP3Bt2sasx9p~{l;@*6A63;fk_V%IR(Q&c! z+Ap^v`2-A%#Cj>R>W!^&nK9YDKL2i8D;e#l30sp1hJQ6@< zJNSQDSqo3kJ7*x8AGxzqgE|9HJoy{+8-(U2>LGd&bSy!fuS>s7K9_Y^gpZ>QGo87L zE1pzl$Z~#&;!exnmw8H(;hL>u!_Yyc8!w<6brAjfc1j}K09l{a?Z-*=Cg3I9GgulP zDnEq3xz179uN#Q@PZKs;E?RDcY22}M`D1@PY?EaP=6QXt!~SpPfA66!PLCoJRy`OFZ9j=M{HHM8Hw=b*l{quUJG*fmqLkqmOCH z^OyNllB#_|Mcic{OHF@_4PuaY3J-fz;D7kXkdSxBPdcnkdlk_iN*Vf90UB6UYU(f#YQKE^)n`b)3a}ZQVJW}?xbbi#KO8rM# zj&^rFBDdVHIy%JX6$VlKpLY;J3XE(0U(037exhc4kz2^GX{hTeeJak2+Mc^dABdqs zH`e^6M!!M*`;q4Rj2L2Kj=4l*$aD!#d$Se=W8kC5Wn5trUCurF z@^$Y3USdgt@(VW6HpAU*_VczI>Up(~nU}Rp&IOnuY5j4!kE1Uz)#-B1?pn6pZe#@N zZ(RQg>3~K4=bB{vPU=-nibMRf47VM0y}==OmC<9`y>MUHHKg}!xzU0V56i5^eGW{9 zji%<5DJYDaE4x?WFJZALe=sXR z!pH63aiUE{`!p0oS955|q#DHGs1UGq+=5 za@j=QH{-b1_R?4jKfTIaEVThXBMtr9VS-R0d)x$Cy%v!16LS=6kM@R5w`10-&ZKb_DvKRGY zR+U7dwWjyq6u3tF#H5|tZq){qLXL7@@v4L%+pzRpwOItScg>ysS|v34k}MGM?QVug z^rrD$in-kd$LMz&&xM6$DOHOz&5tfcnHN{A#D0|i&2aQsT>}(;fJu&J3cD_X&M{lA z^g)W$)Ksn3lWwy2d6`-mqwsEU{v0Iu)xd=%Ea4=^i}gfMYdkhktz8Y}`OdA8e2TEL z#(d`@c*=gye7<@6rCvUOTX>!@=_kM&x3qLVc6pc{?@Tn+3Hp+KezxumSr1q~n3vt4 zx!EsAll4TzwH~H#d)$zv?AV&(Jq2+f%wR6dB}vG+btxb(Umu?=xr_K?dw6;_7uVD{ zMu3!pqMY2#`BeFPW1H>IK_`$8w%@GKGdycPJaU8ZUXds6xMGbXebeT`(2=vFBO{k% z!>+$t1%%W`ot3o3>j7Wt@_F zxb;~BPY{;gY>6Fp?w`1()H-(NL6>kdzg}%SD1&Hw=+aziWog+&_qP$^dWHGpGDBB> zZK~Vb=IV}$0;C)O;xG6f`CX~);dV|zEZ=_J6Kd;YPSH*cSWm0;Pu|fE)_e6K1g=Fp zGS&ZK`{v4ZB{gioVa+I?vb)YeT6$r!7DtKP!pjb!;BM9#1HV>v|M5Rv`aQaKhQRqE zEDsE*-=W>85o=RP1S{@-V9E7p9x?jsi(Zu;zu63qO{DdLZe*=D>Fk8EuRauL?!5B0fV3X^(cp zC^Je|q2*+iI{0-RY50Gr{xWD&OZe$`r12wFV z>Cc}(&z|{dzh?OJETY;AF{_X(HFhA3P`08C0kEeCI~uhYjD~K*YPg<@mBW>RH>*dm z)UQ-=mngyj-^^Up0E%DFQa&{se>|_W_f1oJ^Cc7GDmAu4-|~}ZGxi?GLht%cs8A_h zH}*6BPZloQI6?+Z2%=YI$01L9Dp0>WZasGi7&)zbx#|Az#}8{g5!eFIg=ZXhQS z=XvPCPjj=RZs=tW7KojCp-!ZRJmFRc&Llv(M{Ht2$-jm$RXi zgiMeY#LMJ=QxbD3H*(ZDa+&=xcP;7%{|l8Dxy!~&Zyu#w-{ii<@bnCN9mb4%xI?2r zY5kG(YP*BAz7}K6c*_*l6vT3-d}2ho8uXh=cwor=WQ9flzuux_sc5$O(qU1XhfPHmk@uw@K$^30EwCp-T1KYbvSyaR+uOS`l&*)ViMWGN z8%dt&fpxujqpUNyYmc;NyCVDJrCSPmh&}ljIaS~Vx03j0r;&shOP80IijuTF++n*Uet_H>Aball(yXnmtv22?UTMXL z*%HHFGnX3fwqT3Lsq{1J$%?48lKj0z_}MM=-`{8v8vM&WpO{>wdF-(89qxB{FEV7| z=jz&n`3eKv_siM-`?(%no|1-ol;UT2`;E2#f%z+XtVOfHcW?Y~e(J`^**avLl%{*+ zu8}fs%?w3z9~G6+g4dpx@ySY0$#4_NxM5?kbcNWPwsV6vH#ZG_ zF4%zPJy6ar^w;rR1W~g<7fThc1aD~~ z#SLac@#c^EH?%Rt@^MD(Om>Wh83!Gwc?dOb&7V+`GpO2x;WE?_t6WxuA<=BAWFHs& zrWi$Z+X!NsyG~DPm0;09l}sJ~#+)~-=tvvXoSXYk+DMV0Y3TC&@!>98V6Ob}w}R5U zbl_enfsU5-G1GuE*9G}obMqZBXqF3@19hnG7~%vW=OMrQZXS171Hv>u$udPK=48ij zKD#t@+wwjhR0=M~EEak`RZ11O@kfSQQ_|K20hJ?8A!=;+HDQu4+}wK^yOV?Woj#0) zG}vNQRC6v>#cnC;9;gI;NSy(802&;o;#S%FYw5S(>nqLTa?d6!k9!?waRe(b-P?k%+ZQl zI*l@TiN#BN9EKM`y4-SMpgv+A{AEtj&vr$^d7-D^B-8VX^OJP0+?((-W`-)aFnGbe zc8YWAPD2>H8>zSn3umyLykmbR==u#n{&sbK?lrLEB%M$DUR?+}I5-d^AtS>V5u32! zd2A{#XY(>o?k#~o{=g6?fQ4$xFJ3leh{sOAEwj|Ay%#@FXzP_{5%qty0GVAUv?S9D z@N>LMBPdUV4D*?OG2Xlw#_&U@yeL|Y21MVRSWU_wgalk%ak=3{{7F&*hWt2KSoY*{ zBtTpC3!8>JVT@Cq>LMoyMACL9o{)cYapToc)64j*7rMvtFZK=igw77C#S)~7Vim4h z{Hy8<|5NQIgw|1YnC?1jI4hcilXgGkbk3sA)Vrp*Gj_ChP(TG6KIl_zmGd0vyJc^` zYkuf|wJrna6>ZvKapKY1VBxI$tCZ^U>T0QrHakE3db9@gHaP;S9e_T!zP@?y^8CYt zFS=i%6m@V8spUAbb`V;h(^fmz;^p0s%=`Ga?0#n~9HAQ>N~`iB&hYn(S4;3D&mC9l zZL41>boMuYny-7^O^lxNWi`-idcfwT~}XUf3shB+-9XkF$IUhROG^?=TxSpc(bB^_;Q=5t=xh@=AYE@#HQAlB@b z58RZJ*q_r>`Y^|pLqt8VJ0gYZpRbRQ?+>qhI1aBB*Z(}A_0XKPG*gd$uR<$Az`@_J z3;r`O(4tbVg8mpA85Q+_9pRNkyE+ct-Q8V*Ec^Y;irs!P%X+?Ayjq{=(pVOL`app% z&%vkLCC+>jIV0#ve7P}&nw${^Nc)$>mz_0eznrE&rwANdX&Xfl>L%#j$eT=jkdpH5 zy$f;-U#|MqLGUXkGZ{5iedgk;EE}AwQQ8XG`&;|F2G?p?+>%I#@6+CtD~H!-MlTaS zy&&-f8OiQT0!RSE&pNdSMcC{5$DE#FJ(ZYGi==D@XzKmf4bo2a+Jnakok{8dZuei! z*y}O-u2mcwSqJ3@z7h`Quy0O2m3@XYX?pKK)K1 z6ODA|>qlmRTc`cuU^u_Kvy(c+z2R*La%ysGqyo`_AcNL7Jlx)1TKf5ef$dU--G&*XxD4mRg%C6Xw3R7RYY!Zw_2Mb0`|@MJA+w7EN#Ul(1bC~yUv&|+r`&rck*&LFzC3V}3XXIXMswFPe4ZAvaq#?F?}r)4TH!b! zW2z}1^nTG3UxQQ{)yw7dmzlQPfy`uk8@7_i!!>>ihR_cNqlOF-icw%C z8>2ad=k0VI^LGEV@*O^M8f?N)QBsrNIe_@3>IM^sIIZ#oIFkh}#~b!?^X2V2JuB5#Gbm zcGtzNb`SnO`e}dm)6kuiF3e;Q!F;gGW)t7u$&f*TAPHn1S$PUZmmp2W0(0?w%!&wj z&!=e`MF-%Nm#Ltu@8+5_NG;3mFm`yuV2*Fg`Q~1#B$;D%$n1VXCn6R$qmf!Xf=j0v-Q~iP$UFw zSuOs=!by%I1KZVc8;&8-gPs5n3bF3=LoEXv2*rQv?5tC4z~Kify*L3Oq0?NOpZM5W zvT@{=IM6?G1^9CcqW4557+n!Zta`)PxdIo$6X)WQlJgycoIIa?xLkFra6O=pHH9cx zMp^KdqxcS#zLv_i0=v63i;hK35T zzh)1|8bI4|2m9@t;JY~W(Hjok1{C7RkS7+rYlt8M(Q6WiL`&ja_S-8TArEl*UTd4zko3I@a-!o0BWT;Eftw>VST;8`r|*{1Dpur#eM)aeuA+4cg|3Igshmlz}%IS#>HDp ziVvUJELzqvHcxg^x|vs9molhp^4(BLd30DUTrFwLeNqO$X)sMPRGxdY)EcR~nW3dh zg&Q{A;F00V{D|2H6qGm+?I?In8l>SN$2>9>QE%sBzO8TtCf|__osp_0*O=Z=id}t( zi0sZpJU?*i{`&nb>?`SuTR#=IZyJ`h-A4?)AFx2UTsh~DQv4Ep{;fdNYw20pc_YqZQ;)$x!P!o%8X$BxAaEc5F=8sRm6W=tagTQkH8-KSqULe|~8 zx0Mas!*Y2xbu^Jg$YCQ?2CRH#VCg~-=uo^S+?|xoi+pacE7DGFC#epemovV~OI8sO zyEkuKoTls6N-Li?D)q#W*Mlw6Zwmt3Cc)UG7&QTQDjtjDKOTXETJv_j&y3MV6=!wE@wQ0?wnhE2GBZF(Df@)$jW-;ox%&o%LhFIz7F!_f_JFE~QD+Q@Yg}ka7MMc( zG9GXCzK1g&pdE{VL(8{Hn-s_46g2RFwUq~MlGsSFN?=~&b+TRz0)jq#VAVFOwHwVF zi*0Jd#sh9iYa?3V_q7|k=&U|44=D(!*`e=b_N8^a1>2AI7N=q8 z#;2TRahx(cWRs5v^w0?wtGkF7DPDV>Z0Sv$JHxCn`G|`%GBRENkc;oZU!Rn;0=sCu z#6adUW;(yqd@Dw&!L|=*=%0ZI21A;w3y@z*`eOF|-LUk7#GhZ<+r+E3TGaeI; zsC!(0EYm{$Z@Y)dFVD~7e;Dg&D#my4LK(FmS*s7uJ|6Y#o_uez;`}YAm))-JyJLD) z9bbgo+Yl;dVb(Q345(m@|78ZsE$1u90`eIwEp~vJ<*e7{NUqnY-Dwkwx1w$n*i*g) z(MP3EZS@+r9sN4We^N}56b*YgOxyJI>a|jBS?O_bW;A% zKnH`3Qtmm8!W(C4Vy-w+-aV`UnVfed#FDU1jS4{sjyZWzz!d~tOWP4k6bGjoSC2`< ziVfHBMPWYP-!lsA(r>^%R7Of~)Dl!Dwcw6L+&RY!Xp6_e{WJ*3CsAU+Z~rmgL4Z>P!REgg9_Sn#Txg$v@7B4$(-CBB?2ai*)#XY59JRLJJ- zK;L+;aNpAWV#$=%1Z759{syJn{FqV1_mx-WnimbTFjyIw(2(zIDikc(hWLhtB@k<5 zOU;Syj2kzC;|Sgt;B@Zw6Q}vxd=RI76qS^jy|ufW0b&^xrnK%oODKS>jR*N8ro0R{ zQ`5OP;>S-%`sCAxKPTKX4^qaF;-^^8%VG$LA`at}OScRMTghVktMZF_EQ~^@S#n%| zSdI^T3sRAH^;u*e9F`$d9sE%pw4&{w!Kdt~>|OV!BTJa^%F3lf>Euev%I&lmw4jK|M#X+<;&$}zNzS-aCh&JG#T2O|#KN=U@Gi#jD z6f~>^i?$~E`h;KL=t59l+o245%UUvB-Ior2d!NpBCP}yX;veXU4`ZbswRS2qeu~+A zgPFS*uQNvJw!eoyu3ZcYbP9t}6RET3uZl;7SRRnum zpFob6X~jJJ_?{~x&T~Z*K3xIe$n(}jFBWX&-q>|nLN)+8v6^8F?#rGmb@q$_g|zI@ zw9UdiL*Xl`@qrsxh_?%j(IpIh)BGHoPfKRMbT088-Fxto`6elGk~lvuMR<&-E@rOBoNG|M2sf~! zC9X`n$_@<}e*pljHE_UcRi+bFqjkAmlRfPWwI3OME=o-2t@ip4$_pF|PHcJs3|O$j zMwOeXG7~{AziDg~1W)muIcO-W+14B5nWGqH9WM@EcUkf$15%0%$RnJ~o>cweymI!C z^z7WcHE!DAYk@c#)iqBWfQ%7~fKaFL@cp__|FX!XeF>-#Nn{XF_%)J zie9&qqMN74<>DoO?(R>yKuc-)&qrsAyjyMo`BRK>xk@T4_e@&c9i(H8 zG}i4^F7AgZomTkKtMt^C;MxsqZY7aYF7$EItp^8Uw+~~cy<{Q{}Ac zyZheXdu&SKSujp&Z32NojTT2iMX3U|_wP?PM<0bh_GRPZ@yoQKVv*gI5;YUtn{EGu z$=Lkxio-en#rIhwAAdYa?z_!aGC2H~5YGRUBb}nxiH{d*LMYnO2V&L{~=OSvj z7lfw!nJ&hV(su}ljj3k-Gg%>owjO$qLC~uXVx}EM-Eoi-gy6Rbd~k;5t@PC2xw*U3 z#l=O*{!d&jpqaEBBczMA4 z7T}qDpP^UDg#5Rq`J`B&Wsb4_v+Ay#hT$fQBKyS7Li-8#Goe z?MZtZinKeID)M@biA7OzXn5E{`6XPMut7H!;pQ3sD1Z2&L-m1D9HrNS2nQ$r z5l$ne%WoR_XZ^+ITAhF|IWiL{X=kPkU9NzG;0Ov`o!5O)n!or^o zWV`zKdLj|PUtoOwF=*IIUSAVe9;I~tZa>0Rq4_IQp&xLKUGn_;?8E&NZC>;&YN$0Y zCX6|`%fcu9&r1Ms)$G_;t_n;-PgL{6Mw+vS2wGZvo0%2GMFnMYD4g~3SEqTTJKgm6 zM57-35~aZ(Jn*XpuXRRHJaVqhOULPB-V%gVJbA9ip@Icfx%B)=7BD)GUjIH3nl|13 ze+OP#4}gm4&b$U>R#KnfN+W~6Kx}w1ah$x+c9T%Kv)wU7j&FL60SfjI0ZR)oKtE)d z^XGS({ZlM=XrPB_Yxi+emc!{`K8>9%Tev}d!*ibd0uL6P2nDTWOIHTTYO3>P`4X*A zwn5mVLvqZe7_t;dZ)`v0QWbT8<34)VD{g4!L9`M3CQQ5IivSrs`takYnQr7~&3t}Z zgadRHXz|&Ajq-fntPCu-#H=(5Q_wu6m(4w_ww`^BPDOJrKt$#Aq2*Cx1l;KiiW zE|u&?druX=$Zk89a4mtImSj{xSo;ihTeisvyZAA{i z@@w#^aGHG2MeudRwj?O{%@Gd5!n@c4z_k1-M`p)6JiqSv)fkV8up*{j1reIFe*X5m zV(DN0p=2>~J*H1>+|U8w*|eQ!8B5}?7>LDCLNW?#b<0I*ahCURhUk0$HtW75eSv^x zHapZM9s{vEC*I~bqaDgF{zAg^#Zocm@?n<&I{`AZ_)XW2r6Xf%81Tjz@%VKFk+_#z zK1i|mjhzTn6mpB)IePg&G&`@zl7?&L9iyuzed0GbGC&8j)UIzD z;C%kI0;Fd%q}Ak&>RIuXps9xFKo>RQR{RZ2OgPYjJKW=k>qpte%)tSmS}+Syd<}Et z&!#ipEla_iCoUlFPw2vFRjj}#lca46GoI$LfFU~AFsZmn6~duWZ}$phI;cb1-*GcM znrBQ*MYX_0WV+_nJ&7ja62nHGgO(!nz47OI2#7s-w%M7%GwDX1tJ6!28Oj3klP&sIh{638f0l$KL8Jyb8Cl7TMn`ICFe-C` zUM{?7a1jRjtob*vDHL@_=H{q3!QKubI;u@xO}DTafXqCw)N=k|>fl?d&}xPKZiUuq z(E4Lc|6oPUhIFv5M3b+7mp3kCRkAArJ?1%z3dG_n&%Q#IyBy*BGgq$TFPyM3B$MYwb7`L zg6$&>%QgbI;|&KOyktL7dQO}_qb5>OH3;H#Y=u|f8z{_3(a6|!Vqeca9^bPfx;s|{UOX7b>3*vcC8?fN&ngzir9u8=LfPLI z{U}FHQR>8M@MBqeR6nYY-Ro4;k{x_mN}Q8Fnm7VLj`i(U0ZayP#z2IZpjz_=CwBO_ zJ1%(1w(T&Es>oN_yNJQ2#>ZE2L**2z>!q@ti2J-hh+=Ooci%30HgUursF5ESX;$cZ zZ5u8s+OFgJQ=Q-1@`sOGcLjhKBE>x2-OH+cqLl2SVBI4mdM`d09bmHpa9hnWUNn>7JvKG(SZ%d#~sV zOmaBPPohJwTpqEA^Fv?$7R8MQUA!qRbm1T%(E58>89FR@W4(@r6`G({Fx1OcuySz~h&-q33A7eDxuLlyIc^hbCw$T=`oP_S z=-tCd09Y37w5f4f`-M#tA8r>B&Dk$1Sr2YnPmRGlFZomQd1A$rRJz|E3Xi)Dg+A!9 z$ulk99fjAz|F|2TX{0_J!fN8gg&4Tc!(-dhBxqXu{6VD-HJnnJKi`#m8nMpR&m z9pqS~U@)fD?s|U>?cY*Oj9~3vBN;lnY;BX<#%qFaF(kF(&AxI*b*bv>rHu1Hno3gc zZ%>b`iX_dYC+uDWee;!_S}@nkU%LE`@9s$_I<^6_7>2I33;8Ic=kQH#+DGB-6fOE?kPc=1Rh5KRMj$@jxtupgTo&) zHK_C5g>1C?!5!<3yfr)63Nuf#52uf}KCfo*iQt?cf_F_2KJ#Jd5dWN%{-^XC2+X}z zg`pM|frU_DWmq}%ZlweU20i=)2?qNltiPjy(CbxjY+lrm%%2KO4a}+>X*8Ll=U56J z1eP#S7cnRDyze)7uhQBruD@vKIBuOeCsRxI8S@2ieq;>W!R!P516f?~V~aasAXE(G zyx{P`P+D@$^(N-(xtK#HNK47!;du5 z%VVaZG-t);@z-PRadtf<3NsU(`a+4j3~#YOTW2zRPoVPBz7gV!T^zK6wn;I1vhfyp z%pPh@mdD1;YFu5$5k%a$J{+97?H%h z>MSwKEdR{|=RVB1L~^gZD69+IKGx_S3M*drPu3t0&&8??1rB|@Py|SkxpjVXr=X>* z)oYwR(f$W;OUqPQf zf|yGlC2o%aWOE-fF#~1jiX;7Ll|QXj)K3~aL3}Tl!W&VVzLb{sQGc+EHc&PFs8(pB zmgK6&pZXS5f;G@Ym$R|b&M%%^|E>G}37bv;C@da{Vsx5i2B(l=Q#jy4yP!Q}dQ|Bb zaPntR?x3hgmFWcl^7#0%np98v;K>+wqs37D6<9ezh8{75~y zpw1|%(74q4jim&Q6X+Ub<9ps)Vl+#>gY*XY()M&3tGSvaVOAkTt1|y{|D3FS_+7>0^@r@o>xruvLj-@ zb@~4u-vxyCK<8iTU8w(Oizwn&mJU7Xd#pbFOsImUL?}EN<@^@3iN5J7_5`Jge{927 zDtr6n%919&xM$D35#$**1?e9@utoiAj=XZ?Dud;yq$t--8H)vOF2^#zL|xt)ZT68A z6aR}By>?M(52kxQS`X8Xd0YsOeKKv=I|2W1@pgW>^4A--LInW z3hhiob+>VUL{X^;PuGl&RtZN0^V~;@ep~|u_&GAK%vqHZ8JfR84F|N(to0&Q0X4`} zGu__(9s1J?sY*l%|G<{}eEW7t2X(B)F1ypCQ zk*a#mk6G+5E}-JEMnXz9jaXa)sG`smU48fK@8QiuMv_0 z5rNqNapApy$Zf1>_3ODaNFOtoZ2>VPN=4Slk2ULROgmiAv6Aa%6cmLlFq--b*7?*q z2#^)r@;t)xEQ@HVh(ojRb1KP^(CJNLexUI&SzkLs&&SlyL8O$UXc%K=$6wdA;_WDC<1 zi3#CCY_~m$e|+8_8n6(+TE=idiANm#QYz>t_z{Q~Yh9HSkysc}XyDQ149qxSN<|RF z(^lA`UG2gdE78rX=sY$f|AQxZAF*DOkOaSTLDkh5MytW|xH8hiumNy7!Q~?0h|cVr z`vAV29TVL+C|+Vv({*?!$;~TuHbH~~ZUD98U{jnKJWSNt*|TB~bCZKooulLH|NAed zd>20WjsRtE+)xcf^>!O?LwibI@6Xgz2J_6pXnvmA{v`8?aEvdU^!VTe(LklsK`&hF zo0@qylp!kp7o<#y2LVaHw_Llaag-H^e){VY`Jm};uA{Y$(Tm_<295lBH+yd<*lY_H zG1vuyis7;*5tO4a8GRvixp%(Vzdj6lJ_J^8d|AkjbSwX~k{4!9FT8=X{TCi)Y&3_9 zG*)Y!+eYEVLDC%CVW!8xWS*|R15Ib>DXj^fOt*0-_h)sFZ>CZGTS*`yePJ5xCNdLQ z3Z>m@15IEBX`C)HahM7Ek4! zKg(!_samrHlOqPe-F-QJp$N80m{eT=XOPA@X6P~h`z0_i?eSo0zk_bUW#~J%#C}~! z#}w0E(snlUgQL#s?ao*~#+<^2+~>*A;0HDS+lBQ7I2im;-l(UXPwT);=8dAR64cIs zs8H7MkBR138L?vNDXg`+I>71fuCsFZ@=0kK$7rs)EMCO@CyhKznRzd+4ta?j2B&Qv zU!avP$EI7O-;!1PX`7shi`s(U_1R2}zKV}@W*DY%m>J1riFc;~u`-2}v zkgv+-f29U~2m+c@%KRR99ZkG>Gsro>d{y<#P-x{{EGS{! z_aF<_E*dJILft$QI=0L{BjPnf^NUYr?Reqj~;3J6g)`umAFTi*ZBTv(}92;SA_L1B4ks$UWmX%XW^MPio>p>$n z*SYvf&|^B`J1YF4>!H_hm;-9~mkX-!_D=>s&dC8#+GbhAihZ6Zecb@w+Gnq<1A1d8 z0)=*XP|q?KP>v1!HU{R`VaQ!e=~Utyz9=HIhOc-dON?zBf9C;bDrFnMKn#Z1++v8b zq$dOFxPZ@p$N~FC0Fco)?03TK)ZP5)4-d;+M6++-$@|=N_!ldON=DjCup1dkrF;Iu za6D#sf1n2tiakZvIXocZ>7AfR@!LgI`voLg@tN(Rq2b_dzcv0pacj?qa8brm{_2dn zBV)ucTO12^kob{>tAnL}ys^=${bZ_~4tXnigPl;BjT^M7rgg7pIuL{rj; zibx#eudK&)Nk=?v=~93KOlIYa953MY?4ywbfEuk0P|1#spUxn@1pkN&1_eac-65KR zP&Ht>!<=gX4=IC>CWV#TB@*UFBOFgcLtA!cZ}mj!<6 zBL|D_fKH5AH6|1Dz||1KSru;aPv9V}>KR)TN&&&wA)us8%^EBtu|5IPK20ma!)Nxs z`oQQ^>QV?RH1Ce{{K9_OmA%=Dpv?Xoe$MJ7=-S@?wTdR%=~CeFxmAoOYP7i&DyeR~ z7~*i=NwB$!RdvGlWLH=M3Lu-^b*E)d)sfL8lmKPylA+L_x>Us4Z<^MJjU6LfX;>$d zNc4eZ(1$5tN({Q_-V%BKtZw>#zz*mJJH4ipZgDFjvbsd^RkYbp!OxA1=bpHzNj;5T z5!vr`3ZKIUMk}C0Z=e>9TmaU8XS#AGd2+wpCjfk{g6I&1zQvL$u0g8-rzW?UWI-%w zg7r6YJR6{by35KHr~BlOY+0BdDgQizN2F91Mr%+%BCv^DrQ{n*AMgFpXqG?8X$#vsP~C@UXp_>$Tt@NX@hB zag^P&(;8TwrmI%XSK{haG!Nrk(gk7U`q_>SwVACO89c1ArWuA5>l+8KG}@BXIY&OM7P+2_-)nTKLZknokhAaB|QdTAVQk)`J-UVE5+T9|(Y)7~Ih% zHnactw85Gow8H@a&#JvR{3;SLT zW)j5xw6vT50%KP?Rd(O_Z)LJ0qKnK)suT!- z?y%Z>C1hYoTrG7*H}27vJ&IaVh8a7XtDg_P7ta>%febCy+KKfIe`wt4WEj{Jm-P>z z-~M6_=Y;}zu`j41eQVAgKJHze9Mb-VXnyT^jreEG_XoA}TbAEuxJ?Unc@K&==9e94 zVkYvDp~d_;kXjW26vob@Apg4!bChV$yXkP9QV;kIIrFRU5WD6>wQ=(kwmdl@Kqlk8 z+rc?+CH6p7LmO$0Q|cjI({IH-=#6} z++XB2QN(mI*Q5p;e1VVp4rZe}wicwkzTEFpE*>QeI~zUhIwA8#d}w&dQfvMKI!zaG zv*{NvQy_GL02mmkqvNX1al@Q<$YblPM`jFnop2~J~%=ekIux2c~pgI#g@oe*n!kyJ7zj1;ACv`z4qXx zvEPK-p?4&UEEJdzSf}}fW)Y(T5;X!{Jm#EG3pYR*!3)J>ZT56#FHuUR!&`~!K0Y?wunaNr#_ zkcveu-~}1ImXlE>zmOEy+YPVx_j^%mE?go|6})i38n-l8><^`nURQxD@#Q{u0DA2K z*y}FcUS6oorFu@kH94Zhg9bG>rv=|uLA4N0nReRuaJe$_)1N%zq|mN;*}jXfq47&z z>j~PwOGy|Jif#l7*h&>>V?{8@Z2$yK($4~N#8+oPlhi*=rE`BX4Q9w^lik{~{37q{ zxS?9FwA1sVbU}8c^1ugakrGP&+@~{_Dbjo~^*?!zJPPW(CBoDlsmKoX!qR4)s1@Mw z`dLNKbV)bVd@N>}8Fvn4lfI-1jUbEpfn?LqK08Z=(1no$GHARB2bRCN06<5OMl#v_ zhv;YDHoHcswW{!2N(9mCU!U>V*y$&X?^d0bMwD;+7`oAApH&Qc>h=Psza#P!Uc%@! z=Vt(lC3XZO8$~?z_H@cc-@@W-e_KT4hoT?KxiW)Nn>w6rFX4(wxXTsgRjdNt<9{9*AOcKScwmo zW29mDs1O16^neN+B+F_hM2B#~k+E=QkE9!4&`@=hZ1E*J!9h!2~l#-D)(4(`_O(5%Qg0dAaf(MZ&AQTRHWNe$-4mO z&2vs~YhJsY@(BtrQ%zab8U`7?6o`tvGm%bl8+O8DCE>rhv7FNv9c=HUGOqfZ@OE5j zx8-Wi%>R=_WRZyvicFk)a7|AWJSSaCFceDwcpXOjHX9=Jx-!u~KUCkVs=TbSku#65 zO7_(&;##n^a&0bhmMG2^r8{dE^<`nnAmnm-4yT5mW&kLd)~++sG-^q~)t$NcW4_=4+T`W@A<}Elq5b1AFjGtcoGwvOZY~O$6vJ1c08T0Q#IN`ABy?II{<4 zB_1AN5Iq)=sTU`$vbl4o)~Ch3(+I` zSM>0*1%Mqah>&6MFD)pUlJ?+FGU=BLwL_HJ4`@<8ZZ+hLhct5DMh_$JbE_qoSFh;G zjNr;dddPo>cy2(D7!}4PU8Fy-nSl`fJSSJds7i1;7*K?VAN&$UEFsGH#wEAw#!ds` z8?VIq6Vyf4W9~l7zGY5C(HoUoe<19)zMm9}cBX8_Nw)xQzQWh9p@PAQ*ijilHb?gX z711I-)&G_Rm0DpUiBl_dw+7o{?_HOr@}v0F&q)P}G3dY7JLc#1J~$yv_XKEx zoa&+%+Tm5=Aj1;K5rMPkB{*#^)O~AYB%}K8go`xNBQt0Ez7j3??$b6|41Gb5KJ%&! z*oZq)m}Ct-y|%sOKVY9VsJR|?DtNdZs}2y_8Sd`G26WvU$zGBp2&chBWt&sf7kNC2 zgs+TtSb^~3FF+k97rV{OL zV0w{ffHh_=5%B35{#xe~1e1Gl@E1PNnYuE=&=!h13p3eZCK^8~4Y==OgcBnj1bDn3L zV^Yg-{8r@X{QV#gs{Tj;(5lHmcSdxvSc*|UTqB11e3Z~A!mF5zal&ikOF+IvUtE$+ zXP2nv({Ky+pZ6!qCKDqG^uN|g1O@;&?zFHB{v-q}Lpuy8J`H~<-kqf`#V9lBc3OEY zjX6pr9ryZ~JRWL!XHAUd!&N7j0+%tu_25C1NRC|%3~jR}RY;2Ed~*9H;zk6cPO7dc zyuZP~A5%--dl}VSYFJWh>eHmE>eY`NM8y4_oVwP(7q7NwWz%M1f`Rb)2ZNv z@=AyUs|)#tlXPcq3rS-&jhYHD?g*#w@^i`D<{iAMO`A^NzPQTfkrR0^D|!3$*;A&C z`M6N^;W{yRKifpM^zUnKHX^-tS^AH8p5(Skr(VM7Q`u+mBjgg}cQ2sEAs1 z>ujj&N1^RTgUwLT&!%lp$+27hq#+KxDE2O1EPeD=u zW>D+6c8M7UhEaOOdp_;X!%dTs7~E0xso@I;%6=<$8YzCT20qf6P^hhu)K+f@9>dcRG9y1iY2 z6U>3rh@}-jyv?f>b^9CJ=w_&oP^EZC8k>;&62}{5l^T^CZo(u--*t166{ZzCQZ+ep zbnc|`sttG15p#)%>n#?Y00H8&D(dp1NE<9&#`#GsQ)Zgd*N?4LgBWJscPsBrHwLc6 zT};()aCiOjxW#6E_V7XSmi%09GLdSxQ4AB1vD04PH72=D4^!dmgv+=~DApNm&){fM z6cDS=+m%IiAtc)t$43GUI5AD@&2ycyefUY9m;%7R*RQH=#R=X2n$Pufmg75Xcz0w0 z0iT_)r>xHMUBrj@hdE5dCAw^F=Nq7IMG<%h%(p(>p&&4&MLt=lDwAE4uPA<$^{XuJ z@6D(iwVnj5k;kG%v$Cw{0r{uc4>H%my!lm(3}(gVN`2(-(@F*3H=(e(Afgbsl~$ zXbs|IP{6D|+>Vk?sEJl>7q8MNBI06-5$A8g@;kemrV8_UC=k{h!aS* zsJ=2mf;cxc5QK6f@%sdF%rg2dPLItR{6~Q$UwJ1lK&{6K1Gie-YcWN^%q*+bRfz-U zt$*)0uv3d))_4AL2Q_*ZBd|uu*_#qc)BZV;dl}>TR|S0{l}Jm$C-5-7u1CA0fpU{X zA?~`VGbbJt42)pj>aya*6rJq(q(I7tsHi9pzW}pARfUC!9nDzH1U%SFA0pKts>Ys~ zfPynqwn&IxYNf^2Lj{-_Jh{hC5PBO;o#WD$jG0)a1y&4V3?y&{i|+oF4DfKLtqy1b zm^;Cj=b}G?1r?Mq2Y^*2Oymj^O5eVw4&k}Vw?a!kmBgAqFUgenNrIL9Ga@?J)INSh zoo4wWj=gNY*R(qv#OOkFp- zM+_(h3-|09cL8WJNAn|aFm9UlqD7Jr1bW`6ZKg^79&?e1TU{LSgM73mb8xz)$h4>7 z<>TqB~+@U^tHiygXzx*!4poy@`g1S}c16M=>l_A)k%v?;tuppxMn^`~xu00%t1XOp) zZ_XVPFgT?3jkLFz#>HA;BYkddotcu@&n26v4D zkUxS!JB3)ceIWeSKtB10D9?eGuGNV-qr+Q7eLEJ*86D^Q1znQlU;#fh(1w8!y6uJ9 z0KHR?MJ2#%I zLih(}6f(4E7S`Iej_QmC8&=BkRXSMuLWtl9rqds5bZ+T2bnQ5*ou3_z2YnnnDS_q; z-_el0(5KjnN~j^W+HRV%uF@c3v2h}Wol>5?WUW@+1Mqj@S~4`ELOxMH;lsHI)}X~| z=RB!@n8Ja-W#(a9a)g^hA)c=LsiQwiv)U|{BFZ5tsC!0WHEQEQCdEIh@jJF5rY@$f zr^H!thp|?1RJ&~_A-WLh*DEdN4+$TV{=DQR6CG>Z6LPKc35ojD=nhk3&C*nSOsHlF zDkV%>7Qcm@qS1SZB>;H_*8F4%#&S0bpdQtFuv*K)eRU zghLL5@kiXlE|szs71cGvMvs3yDCfsFx@{te;^nn!HGq6zL}=Ihwk6?Y7Nl!j8{-LY zv6LXL)F|nbJw5PD3>#MiXh-lU`oO|d_%jHeL&Z*6h+V(^#~*zf7R$tVFA5DB7#t{_ z?dBFr?2N;*Xy_|c22go-2Lp*O7#lpeh`GY63I3e4sVF|C^cI%H(bO7AxF!dU7mfgM zddXZWQ*owgSDH)dH4;K9i9U2mAoM->@qzp<-DnKc&ZwJ8q{v4D#~uc9czr}eK*Yoo ze`U>T$1TmpOtUxMo%B84d^LAy6nd0E%EecME^v?W#j;Z|a?h9*F7#8Ql8N`rj1R|$ z^79JqInJdY7)$g5wdcp&RF235An=*oqUEWpsmc+<&?qx!IYJbx&pM;O!m@DhmFy25 zihB#UAXX|VkeqPX;zj{O?ZleS^7@ws9IYD^Pu9%(cD)iP7mv`g>M{V(0jdhJ2KTJ7 z_1`C4x=?#T0lW>Wdbwe>Ae+}18kMogExVWG1U?+bJ|3rYLMd4A72=r{h8HY^k~ipH zFav$ITA5?7^C9?foGXW9+}Sw@tF<1rZ$H-HH0h2DQN+O8opBkfkgUJxh2M&7V}aXG zKm4s8J(Eh#6$KF2uNslh1!B|8Sa8}-o~u9~*{S$ZBD}D%qNv<$^P_R-a28z%@R=tt zo?Q{4XWjZKaD#qAHqu=NtTK#7e{~9K@fDOXEY=yVCooLxIFdlk4-$EKO?=qhK?lE!Y?#Sr3 zb$=ain+FtxntViJ*ehT&AEE+j3QL6q;HVpdpvdn|f96d;RJ#-b_W4hcGPznR`J~zdOG=m?i|P_#91gZ zqKc{Gh~dRY?+e@Da!Zb+Im%Ku=8yT3D%dm%)2sD5aH|VJ!{8z|JX0;A&f(4)`sTRI z_yE#e$?_7_=Kh|S<6hDzJ?S3(2WAgX6lO{UcV26?J44ue)t3VHE@F)Zo!xo(bS3u`C@@k>KT>*4L zx??MfPr6?mV}aN3CM{JZRDASkVy+<$)jaqV1ZRPEgiKukQH{sG$uX{w{kjJQg7ARR zH3g}-JLMIT@62MxZQzp$eNHP%-b}+${6sI3vFK@oLgs4ccjsaE{2i^~;I%*-G@z5J zEG>1@bLUxtoI%{(clcs}9Xg*@2svQ&8d3uwnf}vEmOe9B)^bzWJmjMhlu28AAcW;G22bgKp*5 zDz5=4XiiN}>|e5{sCE*KP`dp&^1t`!f}&dBs2Y3by`qR(x6Yrp&;4<|pGeCaj+wZs z)UsJe#uD1B4(Fa}zg$r7idBg3H06W16)$LjPkzG@CU5_sqQ38>a_}Gk5BmOw1Ci%QF|PEPe*YRFK)3g;K}QsyHBV6NLBkc`n%{lGojrV>}!bH@2lml5VgO#deW=TA(dn z$CJ8k-$=mhgZeBwys+7RM>F~yP0Lwkn~}oZQ=&Oc=LGX|yQtIfuZs4hB^c%=sTJ;^ zcz*5dCQA#PJFu8paLSoy%|CCT#FHqDEVE&8HS7+2b}wC`%E7bipmK0m|M~?O&LX@j zpoi>$LiIr!6*Q;A@Z~%mF5vtr6UC4sgvzn7sMH=4=H7+_LUKGg26wb%dE+okh1-onoze~-8HS9w2 zcBUSF&DnYF(QNSN%z%jB1fu3XHJnrq5_xV;w03{UhsYmaNWEQcF5TONqaKMORa@G8q z^?$`(h z_fNe0Y4^jubM7-|X0Ex;nK^c_ghWx|7qfpqTPC!lGThQW!Lm3PI=~iAhb0_MI6LWx z^F$@Vr(J|_(IC(&K1j4sQGbO9sHDiwy&*T}aKq|noxhO1@Os$KaA%lSa zbR!*r0t?xX!;q~wiJ&P%1}Wwrl*{yaMO7nsIteYuh_y-~gpaBQQNcwK^RIV0m;QxL zg*2R9hw?M|k1RHCj7SnwC#Na?;mwPZPv}10fN!hE;w z%k>M1=m#&UXeFe`02`iaQgAX*58LUKGOyPHO`U>dxeZ=Qg2_}j^CrhzG}mi zoQYpW^%m!OieYNY1exoRAhYK-Qxu3T3?@#0R@XjImJ<(bl>X<%`1q}*LyQmSlp#f> z02)4%BF7@LAAN|$uBuZb_5Q+(HJIIvwk(;y{|o2MYT0KsCL<;ZEiU*5WAcp;O#$=K zLa2TJTR>TuM}ke;fKl~Bh{cILV)hH=!H@c}YYx`-#@k~ z7kGe>ElWAz`GA@kcIqAoGdZAlDBK#34SqK0@^`OS)N&|hQE z$8OXFYc^D}KIB1aB8yqreCL*Xne>+ML4{~WV09~=litR*xncK*`a&LE(4F1CETXw- z?7$VJRUOQP6cedFg<3>EF3B%@>ZgF!--^3HWMSZLef?m2W#B8XtxDL6AKh9cUSQit ziM?aTxqt>gGe$E}=-%AnAzd9}g-%Owk8&Vd)W2ZN+9Ew4WU|-FWGUMwv)H^wS4{wV zfc=Mf=p|_(fTdEQ~T=Dd~&& zPQ01=JZ~(zM>l&c-mXt8xHS_m^IiAO+)zK`Pg9Zqwn3Z7|AG`=A4F6Zp0%sh-ZP6U z;(OEhKy};-PI9D3r8jxIt5Wc^b@DfRrC9m{>@^;%%iU!=;5{7x^k!)fvQ!JO&Cf7- zo~S_7)MbVk_%5y%jbIx69-B<$98pC(xoKehhValICpK4xoAyTyHvv{s{!e@gA5$b* zsxr-w+&iX)mrH8FA69B{b>x6@;FaT~EY~^Frv-cVW@yizAEh&=cQi;jdwf8kFUc(_ zb`9muOlFU+v*-k-wsx2E>3IGtQkgV2qtS?|cD!fphU<9jx0gWw%^|n+sV)P_-!*$C zqid5GS;dMZkpNIT_T+nCUIC)^Lf zJQ=M#79EY+=Kn6%)emIL0RLH49!&uGFBs%Z7K~GKR%;?e_qQeP9+l0fb2TgWD{MA5&c+R_N^E=+=Qe)&cxjFCES1i_h=O#q-rf}eOp z?v>KIYfiin^#fr+$?&lgRP7HkaNF;UUoYr|F~}U>T8d8rTPE(R$^-?d7PBY-v`t4A zErzs^#0(p1jq?Y}AIi)!t8{6WT(=v%v{mYzW&VK#-A%uLj6{3xwEgZ2He@6Asa?$` z=DT2&sh6u?-*`Kk4e)6+@9khK=droWT=PG0a4fHd4)U(mKulkF2J&%$_vB1Cj~O0= z_YFvB|EpMA67}qq3hMFj?{;@H9(mqGbhrrq+5}1KVF9cZOL74UyMKO6l=Nk`j_%-i z1|$My*yGL=^{8MD%)7Q6pJWNRWMyBh49q{qrJ9 zDV8{YGxA6wQGYqz3CHcxT&kQIKyusEFd(HtAMImyJQ(zk#Qb)t!pJLvaq)BUfv<{0 z2a#C}f_DL}#ViXQwC&k>5HH4#RtFUeW)Q;&F2GABk|PO*aX~x5=qh5S{!0Xm8~|m} z#hH`YeHQ-seHH=YOKCIpd$LrbluCPAa&QrO?|1GP$X@HMduQSyC>0SHjK?hvH~oO9WZw@g2^`x#&A>z=$~@ox=C2LHJh{5)#X=x z37qt__rGsazij#xu=AaV`U^k>q>FAnqtZ?pzxnmKe(YvXo!KTNp_sdyMs>9bkuM2B zJ_BvmV=WsAzYx26>y{()JB%D>Ma6z`MSGr1g39%ReJamiIBcUVr;QgExKn4_P3gQD znM86ni8$lihg}%Qc+-hCZ~ZwYBHqndwso*WyqXY&;8@-YC=<7-ncW~jcM*+%v`jF! z^r%8>vuYv7c9|UXjc|3t$YMU3R7B_qEP_E6s0Wgn{-{^CN|&np1H_!Pc`yV^JVsB~7v*6c zJJb{)!oCc2?6O5=gYN{i6?B(bQ%727#2nl#K9c^v768g1Ax{puMn9Mujy*{KW79e{ zLh$SKR{fX?x*-#rk5Q&zqqu^z_WtqO-Rfj)DZUuEQt$8+y}Dylv%)*D{H&-~n?Lox z9zJ~Sc*q@V6&M8A2Z$UBR&Saqj4JT~YuZ`aWgmb*fQw+O->fbMs0G5#9hKf;-Tb#} z)N8!8Z`-~b;S%8}N0S!}#VWQM6|%}~n_uaARfOPb+KzvqQFf)J8*PT7CkXgj(1YXN zP)66~_nk7X6X}N-H6-vB2qmE{{+8JYN`eTu9lM=F9_l+{o;uR;`{j>^=E%wv}K}=$Ias zt|UE>+F^C>H1S3yeRaZLSh?fqx-F%i=LF|KRxK8~2sdo7^K0-}mY@1DX}|ED=1m>B z=9Ku&k>bKk4s#_I2kY6GnXG2{)}UGeFZR%>VI>3m`lhTw{5r zjLYdtZm)@tIr3zpeU0|qC6KbUILl*sjhhyRy{n6$4O?s#TjWwQV6SgS8z?Wz=JQoz zHiMCQ4g|u(z#j9uH3NJav}+&tY>2a18*Y!7Y0)2}ifDYmbnHTG5zR4)5aurJ6Ne>2+B67HoQtRX5XKwHhuQX z-V6f5q;Q`8^*r?ru8uT0$1Cbk>Vq^DAH0 zkAd0{Ox6+NMCunpdyfrA+WSt^7r2xXuQrfca?0OVlATjlFq@}wZK7I$$u`f4^-2Pj z`?3S{!{%8Bv{SqCfy5{k^@z{~MxkH*$n_aeKSY4;o5$5LiTcj7mDrpW%61Ht$#}ti zV@QD+2J>uR$P1w>GXN(3N-feX*b9#EMH4vw zl68&i?>qrA26=1XCQEwVNQAo>mhtL?>(OZV-s3T;MA(?5Z$=f>Pm570FnXzP#&%nmN+D z&e2pWTCzLMPA`xBl1u!9Tjw_+bF~zBaXZ}RS^eg^BgwAtOm<7==(f#9jS~L}S!X?K zvrppwTIKUgXPfoaZ_h7XF~^`%ZN8MwhlRJ{R+X_TA-Nm28SxUmPkqW_EV$SGnKp-Q zRK-`QryA!=Le1d23|TCp1{mX+9awh+dJ{OcQIsXY_r@AtmimK9mE2u zn1%7D_Dkk&t*2_v^^*CNC73;4o4b_is1~gMTwA$9P@Zt9g@no<1>8KlEjpGP^ylo#_?%T$lc*z>f@e*g@)i?dgr8zB6}@zEZw9!yWwt z%z>0`H$m?lOoYf7W!S0txms#6db%ik7RT6TelZ4ovMovdDk3TGUZ2!8pa6_#Ac>(s z&_i&_R@&2shO=_Zs=om0OXgIz)JHdV=H`qu%X3@Z7```$c4|r3%Nlii-@z^~7xm)k93hv&9ZV-4<_{yAOj zI+96dKW(TvYdtsST1bD+3+*q_6b-&g<-~E1TcdhY&fT5==310@!Hw&rv51E2!2DR6 zSekh3;y5z|w(eB*;62;vZ6V#?>&+-__t%Li*I@pK=h&!l;{h07Y4OQTOI;h*64rC< z>Cgx*Jmj4lHJAfDk^Y8v&dqh?kVN7?$;q)PG}i#JOZg%#xJkh{8m3w*s*Iqp{`CbmNgnBji$!;k&Wt#e|$d?>h7WR z{6c1pm0j@VNJKBbMF_%1ZNniiD53F%?gw!9}ep_zz{^- zaiX8gZyKl4Vgtog>Q^Bj#tC}zz$JpJjYt1^M2lnlE2Pocs%HON48?B{iIlDcQaQq_ zDf16lpMvF}9+$fS1@BdH)TSqA>{yn%7#dybR+~?I66xfCK)DQ?1RuCwEj_({_0T`n z$yHmD7{!=~#qwddz+qlZgRiBSf2MNyZ+czAkzaDxNu`3%8SgQV2x^R86ZE%)7&A$K z?R%$ig|gH^#*$J+rFcvdai*HPAN4$%`Yzu2{n3kzD?1vhlhF$izI#DHWNE*arT;p~ zS2INXM`oh$+02-}tIS7F&Xn)(#ng)`dIjhihRaLHFEC_v+R>RUy?7pKmaM*hY(6gB z`)+>-O=2PJ)K&_U{>+uxkn*EHDF=b+gEDemp1KwU)7_>@7Co59|}cJcGT)WL7nC0Ud2E zn>>9@4=e+GA*)Bx_vEW)3O=k2-$GzYh5A&WBurz-bSSNBk@EqyhF2AFd3^av4q43I zChBi^F@Kyx_R&>VvGGU}ga2h3M3&&rH~F*rfdK}KZ~^ij=?a;TO*2)#Z51GQl9L;7 zDIfKWqlog+XIRj&@#AuFXWdVxi=%Yq=&)yuRoROXFpswB?IJ2G(A>bGSvk4SPdxs7 zAb}P`Xe;=#H|XNQbM`G)7Y$JoYfVCgSsBan0n^Se0di`D^hnXF04zwZRl>S9)~8;lFwwISWX~+L$Pt1;+3-C;Rn7 zdKLoqT&mHZfW8cNL0iP%jC|K^?#LAz`mJ>IFj$V>Ktd|v)_b>foFg*KZ0)BrQHa&C zV`&^I&&*$QmYU?jsePB&?fpOgnw-2W;2XMAKm>EtHl{(vD<3S)rF8#}s0~~q^xmIw zo3GL%#T^O_Y6@9XK?AoM8+pvfgZWb5$qwqxQIa2)GjNuTv_0a>~45;FAYY| zH(ulHp(jFZRBQAtl+Dt~v;`hzU0RL!v51yBTCSm9a{9}> z!Y5H_`$$b*q$WT6uPK$nvPIXyNNdA8lAo=wwxR~wu5qtJ%PZR#ldNX0E*!nNFVp}* zRf{KAgg7AqaW<;3zxTx zQ7d^_Y1fJK^L;dN&YgNgYkepJf~HHmNSn;Zk{>mscyLED>lNkY`Sm-R@}md=H$A$5xjF&N_7U4B6=D%llpl4{rD~4H=GvOz%(CtczSN$)BCl3(@`2qbYNC;pv|0 zEE(vy2%^D6;M@rxdmB#c{#6Ga(Z@h(ttOSOzXQQZHIkoQIc8`0}I<8JaMoLA&(usu<;V`u9BYbpZa14pbOFA z?rfs<>SLH}1*p)S2b`QAjEe%hg_wW=^Rz!~NqK!$^jPc3(a;$Ket30?%(+}@A8`b2 z9KjOt9c}Ag2Amt5?5Mu3YlRj<56g2`yv8Z>tmbU^qXc+%o5)9^13U>I_Eyvnj~~`4 zxVnh!=-mX4ZRbMOUje>tdWv8PXNkd)B1$2s(@|fmnq$I$aB+8aNP69$Le2Lh@LY(S zR_5fLaNh}Yb}$a2qFOy4m;xGEND%!to+Qz*N!C=0af+!2^nU?2<#CN?_&>*eI4~q} zy;jo=hB>VepTyq~Mlr+>vuT3Zc|}i5tQ$>kHTAWs9!)fW4MZVyab}|_%qWg)HUR^= z6=olax_0b4qGSEJ(1E+JSn^$sWH2$7rGAV$nzs?aLM5}v_^{oo+`l}-Gsdq+v!D}V zO}sQ#S==>2jq%|99V_!UwdeuSYjP&*)9geC9TJB?h~IHD_MjMAuZh72AK zse4%gfgLKv>d)*VspkHgI&x!?o6rUCtNuz^DjWtuGtl6RQ+zrCUYAb@J_dp*;i^RF z3<}0Jh1{CDIc>ou(Av zjs)0%-bngcoqjyfvXk?7ZIoYsWPqU_N3L11NMpwOfJ-QM4R09Un~01SV#f3MyqxPFitgyj&!; z0Ps+B^1s-?Bp0)l-c7XkT$=cf5&apVKBAAoa#09r#z6(=ABr8AoivgU%GbZ4t({>S zHhH#Rqqr%l^7EtXi!2Fs;9oXl!{=IB&FQzmNti2GmUxm>DU$_Q^1ClEP-G#{(9K8v zPq|*&hUSrohOF)(7m{CQ0n#ti}42sbFo#(KEiYrExgk!sri&BL+e zEYIpZ#9Zp(+pGuC?C2U5mzyT-Qdj1KX;#IR)b->vaS*W|NP-C%J?m2~0kqdX3EJ~N z(FVK3hJC~+1oRCt`}L6b3j1^e$6 z6_hju`$JD2r5^(~Ia>bfOQ}lcW@3}1m>7}zUX&5GI4OGdE0s<*GLN>Ei7$qV^iu{u zHX6LmcBi1hHLiP#JQA-6`q8!xklBguQXn?$SRjHfMg8*&43?+9oaoEi zJ$Mqx7!=<{SPI^Rk08Gx2`072PF4QOadKq)sfA^J{IJ7ezo4mW}CcMy3geYzs%cV&LZ)QglS zM+^NWi1fS=IsAm@h*1}p%?C5q|L2cB0h;;Aqao>RAywcO6%a^6MMt?x(K_t^08smE A{Qv*} literal 0 HcmV?d00001 diff --git a/project/users/api/views.py b/project/users/api/views.py index f1d155d..8305168 100644 --- a/project/users/api/views.py +++ b/project/users/api/views.py @@ -1,14 +1,14 @@ from rest_framework import permissions +from rest_framework.decorators import action +from rest_framework.response import Response from rest_framework.authtoken.serializers import AuthTokenSerializer +from rest_framework.viewsets import GenericViewSet from knox.views import LoginView as KnoxLoginView from django.contrib.auth import login from drf_yasg.utils import swagger_auto_schema from django.contrib.auth import get_user_model -from rest_framework.viewsets import ModelViewSet from .serializers import UserSerializer from utils.helpers import custom_response_wrapper -from rest_framework.decorators import action -from rest_framework.response import Response class LoginView(KnoxLoginView): @@ -24,7 +24,7 @@ def post(self, request, format=None): @custom_response_wrapper -class UserViewset(ModelViewSet): +class UserViewset(GenericViewSet): permission_classes = (permissions.IsAuthenticated,) queryset = get_user_model().objects.all() serializer_class = UserSerializer diff --git a/project/utils/image_upload_helpers.py b/project/utils/image_upload_helpers.py index a715042..ead0c9f 100644 --- a/project/utils/image_upload_helpers.py +++ b/project/utils/image_upload_helpers.py @@ -26,3 +26,11 @@ def get_professional_experience_company_image_path(instance, filename): return "ProfessionalExperiences/{company}/Images/{final_filename}".format( company=slugify(instance.company[:50]), final_filename=new_filename ) + + +# Skill Image Path +def get_skill_image_path(instance, filename): + new_filename = get_filename(filename) + return "Skills/{title}/Images/{final_filename}".format( + title=slugify(instance.title[:50]), final_filename=new_filename + ) From 95f79f890f675c97a010043a7cc0fa3b8573daab Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Sat, 17 Jun 2023 15:10:16 +0600 Subject: [PATCH 08/45] Education Section Added #35 #36 --- frontend/components/Education.tsx | 131 ++++++++++++++++++ .../components/Home/ExperienceSection.tsx | 5 +- frontend/components/Modals/MediaModal.tsx | 63 +++++++++ frontend/components/TimelineItem.tsx | 12 +- frontend/lib/backendAPI.ts | 28 ++++ frontend/lib/types.ts | 28 ++++ frontend/package.json | 1 + frontend/pages/about.tsx | 13 +- frontend/styles/globals.css | 21 +++ frontend/yarn.lock | 31 ++++- project/config/api_router.py | 1 + project/portfolios/admin.py | 19 ++- project/portfolios/api/educations/__init__.py | 0 project/portfolios/api/educations/routers.py | 5 + .../portfolios/api/educations/serializers.py | 30 ++++ project/portfolios/api/educations/views.py | 13 ++ .../0011_education_educationmedia.py | 96 +++++++++++++ ...ducation_slug_alter_educationmedia_slug.py | 23 +++ .../migrations/0013_educationmedia_title.py | 19 +++ project/portfolios/models.py | 118 ++++++++++++++-- project/public/staticfiles/icons/school.png | Bin 0 -> 12542 bytes project/users/models.py | 4 +- project/utils/image_upload_helpers.py | 14 ++ project/utils/snippets.py | 4 +- 24 files changed, 648 insertions(+), 31 deletions(-) create mode 100644 frontend/components/Education.tsx create mode 100644 frontend/components/Modals/MediaModal.tsx create mode 100644 project/portfolios/api/educations/__init__.py create mode 100644 project/portfolios/api/educations/routers.py create mode 100644 project/portfolios/api/educations/serializers.py create mode 100644 project/portfolios/api/educations/views.py create mode 100644 project/portfolios/migrations/0011_education_educationmedia.py create mode 100644 project/portfolios/migrations/0012_alter_education_slug_alter_educationmedia_slug.py create mode 100644 project/portfolios/migrations/0013_educationmedia_title.py create mode 100644 project/public/staticfiles/icons/school.png diff --git a/frontend/components/Education.tsx b/frontend/components/Education.tsx new file mode 100644 index 0000000..dd5c583 --- /dev/null +++ b/frontend/components/Education.tsx @@ -0,0 +1,131 @@ +import { FadeContainer, popUp } from '../content/FramerMotionVariants' +import { HomeHeading } from '../pages' +import { motion } from 'framer-motion' +import React from 'react' +import Image from 'next/image' +import { TimelineList } from '@components/TimelineList' +import { EducationType, EducationMediaType } from '@lib/types' +import MediaModal from '@components/Modals/MediaModal' + + +export default function EducationSection({ educations }: { educations: EducationType[] }) { + // ******* Loader Starts ******* + if (educations.length === 0) { + return
Loading...
+ } + // ******* Loader Ends ******* + const handleClick = (param: any) => { + console.log('Button clicked with parameter:', param); + } + + return ( +
+ + + +
+

Here's a brief rundown of my Academic Background.

+ {educations ? ( + + {educations.map((education: EducationType, index) => ( + +
+
+
+ + + +

{education.school}

+ + {education.address ? ( +

{education.address}

+ ) : null} + + {education.image ? ( +
+ {education.school} +

{education.degree}

+
+ ) : null} + +

+ {education.duration} +

+ + {education.field_of_study ? ( +

+ [{education.field_of_study}] +

+ ) : null} + + {education.grade ? ( +

{education.grade}

+ ) : null} +
+
+
+ {education.description ? ( +
+ ) : null} + + {education.activities ? ( +

Activities: {education.activities}

+ ) : null} + + {education.education_media?.length ? ( + // Here there will be a list of media. bullet points. There will be a button. After clicking the button new modal will open with the list of media. +
+
+

Attachments

+ {education.education_media.map((media: EducationMediaType, mediaIndex) => ( +
+ +
+ ))} +
+
+ ) : null} +
+
+
+ ))} +
+ ) : null} +
+
+
+ ) +} diff --git a/frontend/components/Home/ExperienceSection.tsx b/frontend/components/Home/ExperienceSection.tsx index 15a70e6..9041284 100644 --- a/frontend/components/Home/ExperienceSection.tsx +++ b/frontend/components/Home/ExperienceSection.tsx @@ -9,7 +9,6 @@ import { headingFromLeft } from "@content/FramerMotionVariants" import { useRouter } from 'next/router' -// export default function ExperienceSection({ experienceProps = null }) { export default function ExperienceSection({ experiences }: { experiences: ExperienceType[] }) { const router = useRouter() // limit experiences to 1 if on home page otherwise show all @@ -42,7 +41,7 @@ export default function ExperienceSection({ experiences }: { experiences: Experi viewport={{ once: true }} className="grid grid-cols-1 mb-10" > -
+

Here's a brief rundown of my professional experiences.

{experiencesToDisplay ? ( @@ -50,7 +49,7 @@ export default function ExperienceSection({ experiences }: { experiences: Experi = ({ title, file, description }) => { + const [modalIsOpen, setModalIsOpen] = useState(false) + + const openModal = () => { + setModalIsOpen(true) + } + + const closeModal = () => { + setModalIsOpen(false) + } + + return ( + <> + + + + +

{title}

+ {title} +

{description}

+ + +
+ + ) +} + +export default MediaModal diff --git a/frontend/components/TimelineItem.tsx b/frontend/components/TimelineItem.tsx index 986231a..140317d 100644 --- a/frontend/components/TimelineItem.tsx +++ b/frontend/components/TimelineItem.tsx @@ -26,8 +26,8 @@ export function TimelineItem({ }: Props) { return ( <> -
-
+ diff --git a/frontend/lib/backendAPI.ts b/frontend/lib/backendAPI.ts index 65b38c4..6dec5c8 100644 --- a/frontend/lib/backendAPI.ts +++ b/frontend/lib/backendAPI.ts @@ -98,6 +98,34 @@ export const getAllSkills = async () => { } } +// *** EDUCATIONS *** + +// Educations URL +const EDUCATIONS_PATH = "educations/" +const EDUCATIONS_ENDPOINT = "http://127.0.0.1:8000/api/" + EDUCATIONS_PATH + +/** + * Makes a request to the BACKEND API to retrieve all Skills Data. + */ +export const getAllEducations = async () => { + const allEducations = await fetch( + EDUCATIONS_ENDPOINT, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } + ) + + if (allEducations.ok) { + const responseData = await allEducations.json() + return responseData.data + } else { + const errorMessage = `Error fetching Educations: ${allEducations.status} ${allEducations.statusText}` + console.log(errorMessage) + } +} + // *** BLOGS *** diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts index fc00a02..23ab4fb 100644 --- a/frontend/lib/types.ts +++ b/frontend/lib/types.ts @@ -113,6 +113,34 @@ export type SkillType = { updated_at: string } +export type EducationMediaType = { + id: number + education: number + title: string + slug: string + file: string + description: string + created_at: string + updated_at: string +} + +export type EducationType = { + id: number + slug: string + school: string + image?: string + degree: string + address?: string + field_of_study?: string + duration: string + grade?: string + activities?: string + description?: string + education_media?: EducationMediaType[] + created_at: string + updated_at: string +} + export type CertificateType = { id: string title: string diff --git a/frontend/package.json b/frontend/package.json index 15a6eda..7b6ad62 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,6 +27,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^4.3.1", + "react-modal": "^3.16.1", "react-qr-code": "^2.0.7", "react-ripples": "^2.2.1", "react-share": "^4.4.0", diff --git a/frontend/pages/about.tsx b/frontend/pages/about.tsx index 63a68ff..baa81e3 100644 --- a/frontend/pages/about.tsx +++ b/frontend/pages/about.tsx @@ -1,8 +1,8 @@ import MDXContent from "@lib/MDXContent" import pageMeta from "@content/meta" -import { MovieType, PostType, ExperienceType } from "@lib/types" +import { MovieType, PostType, ExperienceType, EducationType } from "@lib/types" import StaticPage from "@components/StaticPage" -import { getAllExperiences, getAllMovies } from "@lib/backendAPI" +import { getAllExperiences, getAllEducations, getAllMovies } from "@lib/backendAPI" import { useEffect, useState } from 'react' import MovieCard from "@components/MovieCard" import { motion } from "framer-motion" @@ -10,6 +10,7 @@ import { FadeContainer, opacityVariant } from "@content/FramerMotionVariants" import AnimatedDiv from "@components/FramerMotion/AnimatedDiv" import SkillSection from "@components/Home/SkillSection" import ExperienceSection from "@components/Home/ExperienceSection" +import Education from "@components/Education" import Certificates from "@components/Certificates" import Projects from "@components/Projects" @@ -22,6 +23,7 @@ export default function About({ }) { const [experiences, setExperiences] = useState([]) + const [educations, setEducations] = useState([]) const [movies, setMovies] = useState([]) const fetchExperiences = async () => { @@ -29,6 +31,11 @@ export default function About({ setExperiences(experiencesData) } + const fetchEducations = async () => { + const educationsData: EducationType[] = await getAllEducations() + setEducations(educationsData) + } + const fetchMovies = async () => { const moviesData = await getAllMovies() setMovies(moviesData) @@ -36,6 +43,7 @@ export default function About({ useEffect(() => { fetchExperiences() + fetchEducations() fetchMovies() }, []) @@ -60,6 +68,7 @@ export default function About({
+
diff --git a/frontend/styles/globals.css b/frontend/styles/globals.css index 46543a9..7cb4681 100644 --- a/frontend/styles/globals.css +++ b/frontend/styles/globals.css @@ -307,3 +307,24 @@ code > .line::before { /* Custom CSS */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.75); + display: flex; + align-items: center; + justify-content: center; /* Add this line */ +} +.modal { + position: relative; + background-color: #fff; + border-radius: 4px; + padding: 20px; + max-width: 500px; + width: 100%; + max-height: 80vh; + overflow-y: auto; +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index ce55b1e..e6c10ba 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3256,6 +3256,11 @@ event-target-shim@^5.0.0: resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +exenv@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d" + integrity sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw== + ext@^1.1.2: version "1.7.0" resolved "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz" @@ -4452,7 +4457,7 @@ longest-streak@^3.0.0: resolved "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz" integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== -loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -5600,7 +5605,7 @@ process-nextick-args@~2.0.0: resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -prop-types@^15.8.1: +prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -5715,6 +5720,21 @@ react-is@^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@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-modal@^3.16.1: + version "3.16.1" + resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.16.1.tgz#34018528fc206561b1a5467fc3beeaddafb39b2b" + integrity sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg== + dependencies: + exenv "^1.2.0" + prop-types "^15.7.2" + react-lifecycles-compat "^3.0.0" + warning "^4.0.3" + react-qr-code@^2.0.7: version "2.0.11" resolved "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.11.tgz" @@ -6898,6 +6918,13 @@ walkdir@^0.4.1: resolved "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz" integrity sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ== +warning@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz" diff --git a/project/config/api_router.py b/project/config/api_router.py index 0b7e2f3..648f0da 100644 --- a/project/config/api_router.py +++ b/project/config/api_router.py @@ -2,6 +2,7 @@ from users.api.routers import * from portfolios.api.professional_experiences.routers import * from portfolios.api.skills.routers import * +from portfolios.api.educations.routers import * app_name = "api" diff --git a/project/portfolios/admin.py b/project/portfolios/admin.py index 8d52d2c..dbe7f0b 100644 --- a/project/portfolios/admin.py +++ b/project/portfolios/admin.py @@ -2,7 +2,7 @@ from django.db import models from utils.mixins import CustomModelAdminMixin from portfolios.models import ( - ProfessionalExperience, Skill + ProfessionalExperience, Skill, Education, EducationMedia ) from ckeditor.widgets import CKEditorWidget @@ -30,3 +30,20 @@ class Meta: model = Skill admin.site.register(Skill, SkillAdmin) + + +# ---------------------------------------------------- +# *** Education *** +# ---------------------------------------------------- + +class EducationMediaAdmin(admin.StackedInline): + model = EducationMedia + + +class EducationAdmin(CustomModelAdminMixin, admin.ModelAdmin): + inlines = [EducationMediaAdmin] + + class Meta: + model = Education + +admin.site.register(Education, EducationAdmin) diff --git a/project/portfolios/api/educations/__init__.py b/project/portfolios/api/educations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/portfolios/api/educations/routers.py b/project/portfolios/api/educations/routers.py new file mode 100644 index 0000000..71a1c21 --- /dev/null +++ b/project/portfolios/api/educations/routers.py @@ -0,0 +1,5 @@ +from config.router import router +from portfolios.api.educations.views import EducationViewset + + +router.register("educations", EducationViewset, basename="educations") diff --git a/project/portfolios/api/educations/serializers.py b/project/portfolios/api/educations/serializers.py new file mode 100644 index 0000000..67a40b8 --- /dev/null +++ b/project/portfolios/api/educations/serializers.py @@ -0,0 +1,30 @@ +from rest_framework import serializers +from portfolios.models import Education, EducationMedia + + +class EducationMediaSerializer(serializers.ModelSerializer): + file = serializers.SerializerMethodField() + class Meta: + model = EducationMedia + fields = ("id", "title", "slug", "file", "description") + read_only_fields = ("id", "slug") + + def get_file(self, obj): + return obj.get_file() + + +class EducationSerializer(serializers.ModelSerializer): + duration = serializers.SerializerMethodField() + image = serializers.SerializerMethodField() + education_media = EducationMediaSerializer(many=True, read_only=True) + + class Meta: + model = Education + fields = "__all__" + read_only_fields = ("id", "slug", "created_at", "updated_at") + + def get_image(self, obj): + return obj.get_image() + + def get_duration(self, obj): + return obj.get_duration() diff --git a/project/portfolios/api/educations/views.py b/project/portfolios/api/educations/views.py new file mode 100644 index 0000000..dea2615 --- /dev/null +++ b/project/portfolios/api/educations/views.py @@ -0,0 +1,13 @@ +from rest_framework import permissions +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin +from utils.helpers import custom_response_wrapper +from portfolios.models import Education +from portfolios.api.educations.serializers import EducationSerializer + + +@custom_response_wrapper +class EducationViewset(GenericViewSet, ListModelMixin): + permission_classes = (permissions.IsAuthenticated,) + queryset = Education.objects.all() + serializer_class = EducationSerializer diff --git a/project/portfolios/migrations/0011_education_educationmedia.py b/project/portfolios/migrations/0011_education_educationmedia.py new file mode 100644 index 0000000..e7daf5a --- /dev/null +++ b/project/portfolios/migrations/0011_education_educationmedia.py @@ -0,0 +1,96 @@ +# Generated by Django 4.2.1 on 2023-06-17 06:16 + +from django.db import migrations, models +import django.db.models.deletion +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0010_alter_professionalexperience_slug_alter_skill_order"), + ] + + operations = [ + migrations.CreateModel( + name="Education", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("school", models.CharField(max_length=150, unique=True)), + ("slug", models.SlugField(max_length=255, unique=True)), + ( + "image", + models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_education_school_image_path, + ), + ), + ("degree", models.CharField(max_length=150)), + ("address", models.CharField(blank=True, max_length=254, null=True)), + ("field_of_study", models.CharField(max_length=200)), + ("start_date", models.DateField()), + ("end_date", models.DateField(blank=True, null=True)), + ("currently_studying", models.BooleanField(default=False)), + ("grade", models.CharField(blank=True, max_length=254, null=True)), + ("activities", models.CharField(blank=True, max_length=254, null=True)), + ("description", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Education", + "verbose_name_plural": "Educations", + "db_table": "education", + "ordering": ("-end_date", "-created_at"), + "get_latest_by": "created_at", + }, + ), + migrations.CreateModel( + name="EducationMedia", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("slug", models.SlugField(max_length=255, unique=True)), + ( + "file", + models.FileField( + upload_to=utils.image_upload_helpers.get_education_media_path + ), + ), + ("description", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "education", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="education_media", + to="portfolios.education", + ), + ), + ], + options={ + "verbose_name": "Education Media", + "verbose_name_plural": "Education Media", + "db_table": "education_media", + "get_latest_by": "created_at", + "order_with_respect_to": "education", + }, + ), + ] diff --git a/project/portfolios/migrations/0012_alter_education_slug_alter_educationmedia_slug.py b/project/portfolios/migrations/0012_alter_education_slug_alter_educationmedia_slug.py new file mode 100644 index 0000000..5ded032 --- /dev/null +++ b/project/portfolios/migrations/0012_alter_education_slug_alter_educationmedia_slug.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.1 on 2023-06-17 06:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0011_education_educationmedia"), + ] + + operations = [ + migrations.AlterField( + model_name="education", + name="slug", + field=models.SlugField(blank=True, max_length=255, unique=True), + ), + migrations.AlterField( + model_name="educationmedia", + name="slug", + field=models.SlugField(blank=True, max_length=255, unique=True), + ), + ] diff --git a/project/portfolios/migrations/0013_educationmedia_title.py b/project/portfolios/migrations/0013_educationmedia_title.py new file mode 100644 index 0000000..b6b1456 --- /dev/null +++ b/project/portfolios/migrations/0013_educationmedia_title.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.1 on 2023-06-17 08:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0012_alter_education_slug_alter_educationmedia_slug"), + ] + + operations = [ + migrations.AddField( + model_name="educationmedia", + name="title", + field=models.CharField(default="Media Sample", max_length=150), + preserve_default=False, + ), + ] diff --git a/project/portfolios/models.py b/project/portfolios/models.py index ae51817..2eeef04 100644 --- a/project/portfolios/models.py +++ b/project/portfolios/models.py @@ -7,9 +7,9 @@ from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ from utils.helpers import CustomModelManager -from utils.snippets import autoslugWithFieldAndUUID, image_as_base64, get_static_file_path +from utils.snippets import autoSlugWithFieldAndUUID, autoSlugFromUUID, image_as_base64, get_static_file_path from utils.image_upload_helpers import ( - get_professional_experience_company_image_path, get_skill_image_path + get_professional_experience_company_image_path, get_skill_image_path, get_education_school_image_path, get_education_media_path ) from ckeditor.fields import RichTextField @@ -17,7 +17,7 @@ """ *************** Professional Experience *************** """ -@autoslugWithFieldAndUUID(fieldname="company") +@autoSlugWithFieldAndUUID(fieldname="company") class ProfessionalExperience(models.Model): """ Professional Experience model. @@ -58,23 +58,23 @@ def __str__(self): def get_duration(self): if self.end_date is None and not self.currently_working: - raise ValueError("End date is required to calculate duration in days. Please provide end date or mark as currently working.") + raise ValueError(_("End date is required to calculate duration in days. Please provide end date or mark as currently working.")) if self.currently_working and self.end_date is not None: - raise ValueError("End date is not required when marked as currently working. Please remove end date or mark as not currently working.") + raise ValueError(_("End date is not required when marked as currently working. Please remove end date or mark as not currently working.")) end_date = None if self.end_date is not None: end_date = self.end_date.strftime("%b %Y") if self.currently_working: - end_date = "Present" + end_date = _("Present") start_date = self.start_date.strftime("%b %Y") return f"{start_date} - {end_date}" def get_duration_in_days(self): if self.end_date is None and not self.currently_working: - raise ValueError("End date is required to calculate duration in days. Please provide end date or mark as currently working.") + raise ValueError(_("End date is required to calculate duration in days. Please provide end date or mark as currently working.")) if self.currently_working and self.end_date is not None: - raise ValueError("End date is not required when marked as currently working. Please remove end date or mark as not currently working.") + raise ValueError(_("End date is not required when marked as currently working. Please remove end date or mark as not currently working.")) end_date = None if self.end_date is not None: @@ -86,15 +86,15 @@ def get_duration_in_days(self): years = duration.days // 365 months = (duration.days % 365) // 30 - days = (duration.days % 365) % 30 + # days = (duration.days % 365) % 30 duration_str = "" if years > 0: duration_str += f"{years} Year{'s' if years > 1 else ''}, " if months > 0: - duration_str += f"{months} Month{'s' if months > 1 else ''}, " - if days > 0: - duration_str += f"{days} Day{'s' if days > 1 else ''}" + duration_str += f"{months} Month{'s' if months > 1 else ''}" + # if days > 0: + # duration_str += f"{days} Day{'s' if days > 1 else ''}" return duration_str @@ -117,7 +117,7 @@ def get_company_image(self): """ *************** Skill *************** """ -@autoslugWithFieldAndUUID(fieldname="title") +@autoSlugWithFieldAndUUID(fieldname="title") class Skill(models.Model): """ Skill model. @@ -181,3 +181,95 @@ def generate_order(sender, instance, **kwargs): instance.order = reused_order else: instance.order = max_order + 1 if max_order is not None else 1 + + +""" *************** Education *************** """ + + +@autoSlugWithFieldAndUUID(fieldname="school") +class Education(models.Model): + school = models.CharField(max_length=150, unique=True) + slug = models.SlugField(max_length=255, unique=True, blank=True) + image = models.ImageField(upload_to=get_education_school_image_path, blank=True, null=True) + degree = models.CharField(max_length=150) + address = models.CharField(max_length=254, blank=True, null=True) + field_of_study = models.CharField(max_length=200) + start_date = models.DateField() + end_date = models.DateField(blank=True, null=True) + currently_studying = models.BooleanField(default=False) + grade = models.CharField(max_length=254, blank=True, null=True) + activities = models.CharField(max_length=254, blank=True, null=True) + description = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'education' + verbose_name = _('Education') + verbose_name_plural = _('Educations') + ordering = ('-end_date', '-created_at') + get_latest_by = "created_at" + + def __str__(self): + return self.school + + def get_image(self): + if self.image: + image_path = settings.MEDIA_ROOT + self.image.url.lstrip("/media/") + else: + image_path = get_static_file_path("icons/school.png") + return image_as_base64(image_path) + + def get_end_date(self): + if self.currently_studying: + return _('Present') + elif self.end_date: + return self.end_date.strftime("%B %Y") + return _('Not Specified') + + def get_duration(self): + if self.end_date is None and not self.currently_studying: + raise ValueError(_("End date is required to calculate duration in days. Please provide end date or mark as currently studying.")) + if self.currently_studying and self.end_date is not None: + raise ValueError(_("End date is not required when marked as currently studying. Please remove end date or mark as not currently studying.")) + + end_date = None + if self.end_date is not None: + end_date = self.end_date.strftime("%b %Y") + if self.currently_studying: + end_date = _("Present") + start_date = self.start_date.strftime("%b %Y") + return f"{start_date} - {end_date}" + + +@autoSlugFromUUID() +class EducationMedia(models.Model): + education = models.ForeignKey(Education, on_delete=models.CASCADE, related_name="education_media") + title = models.CharField(max_length=150) + slug = models.SlugField(max_length=255, unique=True, blank=True) + file = models.FileField(upload_to=get_education_media_path) + description = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'education_media' + verbose_name = _('Education Media') + verbose_name_plural = _('Education Media') + get_latest_by = "created_at" + order_with_respect_to = 'education' + + def __str__(self): + return self.education.__str__() + + def get_file(self): + if self.file: + file_path = settings.MEDIA_ROOT + self.file.url.lstrip("/media/") + return image_as_base64(file_path) + return diff --git a/project/public/staticfiles/icons/school.png b/project/public/staticfiles/icons/school.png new file mode 100644 index 0000000000000000000000000000000000000000..e92ce858fdc5155ae37bcdd3094685007ccfeeff GIT binary patch literal 12542 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 literal 0 HcmV?d00001 diff --git a/project/users/models.py b/project/users/models.py index fd4e4dc..e0ce334 100644 --- a/project/users/models.py +++ b/project/users/models.py @@ -8,7 +8,7 @@ ) from django.utils import timezone from django.http import Http404 -from utils.snippets import autoslugFromUUID, generate_unique_username_from_email +from utils.snippets import autoSlugFromUUID, generate_unique_username_from_email from utils.image_upload_helpers import get_user_image_path from django.utils.translation import gettext_lazy as _ from django.conf import settings @@ -70,7 +70,7 @@ def get_by_slug(self, slug): return instance -@autoslugFromUUID() +@autoSlugFromUUID() class User(AbstractBaseUser, PermissionsMixin): class Gender(models.TextChoices): MALE = "Male", _("Male") diff --git a/project/utils/image_upload_helpers.py b/project/utils/image_upload_helpers.py index ead0c9f..ad83f3a 100644 --- a/project/utils/image_upload_helpers.py +++ b/project/utils/image_upload_helpers.py @@ -34,3 +34,17 @@ def get_skill_image_path(instance, filename): return "Skills/{title}/Images/{final_filename}".format( title=slugify(instance.title[:50]), final_filename=new_filename ) + + +# Education School Image Path +def get_education_school_image_path(instance, filename): + new_filename = get_filename(filename) + return "Educations/{school}/Images/{final_filename}".format( + school=slugify(instance.school[:50]), final_filename=new_filename + ) + +def get_education_media_path(instance, filename): + new_filename = get_filename(filename) + return "Educations/{school}/Media/{final_filename}".format( + school=slugify(instance.education.school[:50]), final_filename=new_filename + ) diff --git a/project/utils/snippets.py b/project/utils/snippets.py index af0974e..b44a1f8 100644 --- a/project/utils/snippets.py +++ b/project/utils/snippets.py @@ -171,7 +171,7 @@ def simple_random_string_with_timestamp(size=None): # return decorator -def autoslugWithFieldAndUUID(fieldname): +def autoSlugWithFieldAndUUID(fieldname): """[Generates auto slug integrating model's field value and UUID] Args: @@ -239,7 +239,7 @@ def generate_slug(sender, instance, *args, raw=False, **kwargs): # return decorator -def autoslugFromUUID(): +def autoSlugFromUUID(): """[Generates auto slug using UUID]""" def decorator(model): From cfbb61e89ff530eea47337bfc0703c4256a5ca93 Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Sun, 18 Jun 2023 02:38:29 +0600 Subject: [PATCH 09/45] Education and Certification Section Added #35 #36 --- frontend/components/Certificates.tsx | 125 +++++--- frontend/components/Education.tsx | 11 +- frontend/components/Home/SkillSection.tsx | 4 +- frontend/components/Modals/MediaModal.tsx | 76 +++-- frontend/components/OgImage.tsx | 2 +- frontend/components/PDFViewer.tsx | 61 ++++ frontend/components/PageTop.tsx | 2 +- frontend/lib/backendAPI.ts | 63 ++-- frontend/lib/types.ts | 22 +- frontend/package.json | 1 + frontend/styles/globals.css | 7 +- frontend/yarn.lock | 268 +++++++++++++++++- project/config/api_router.py | 1 + project/portfolios/admin.py | 19 +- .../portfolios/api/certifications/__init__.py | 0 .../portfolios/api/certifications/routers.py | 5 + .../api/certifications/serializers.py | 30 ++ .../portfolios/api/certifications/views.py | 13 + .../migrations/0014_certification.py | 58 ++++ .../migrations/0015_certificationmedia.py | 55 ++++ ...0016_alter_certification_image_and_more.py | 30 ++ project/portfolios/models.py | 80 +++++- .../public/staticfiles/icons/certificate.png | Bin 0 -> 16700 bytes project/utils/image_upload_helpers.py | 14 + 24 files changed, 819 insertions(+), 128 deletions(-) create mode 100644 frontend/components/PDFViewer.tsx create mode 100644 project/portfolios/api/certifications/__init__.py create mode 100644 project/portfolios/api/certifications/routers.py create mode 100644 project/portfolios/api/certifications/serializers.py create mode 100644 project/portfolios/api/certifications/views.py create mode 100644 project/portfolios/migrations/0014_certification.py create mode 100644 project/portfolios/migrations/0015_certificationmedia.py create mode 100644 project/portfolios/migrations/0016_alter_certification_image_and_more.py create mode 100644 project/public/staticfiles/icons/certificate.png diff --git a/frontend/components/Certificates.tsx b/frontend/components/Certificates.tsx index 16e31c3..0b7d541 100644 --- a/frontend/components/Certificates.tsx +++ b/frontend/components/Certificates.tsx @@ -1,16 +1,15 @@ -import { FadeContainer } from "../content/FramerMotionVariants" -import { HomeHeading } from "../pages" -import { motion } from "framer-motion" -import React from "react" +import { FadeContainer } from '../content/FramerMotionVariants' +import { HomeHeading } from '../pages' +import { motion } from 'framer-motion' +import React from 'react' import { useEffect, useState } from 'react' -import { getAllCertificates } from "@lib/backendAPI" -import AnimatedDiv from "@components/FramerMotion/AnimatedDiv" -import Image from "next/image" -import { popUpFromBottomForText } from "@content/FramerMotionVariants" -import Link from "next/link" -import { getFormattedDate } from "@utils/date" -import { CertificateType } from "@lib/types" - +import { getAllCertificates } from '@lib/backendAPI' +import AnimatedDiv from '@components/FramerMotion/AnimatedDiv' +import Image from 'next/image' +import { popUpFromBottomForText } from '@content/FramerMotionVariants' +import Link from 'next/link' +import { CertificateType, MediaType } from '@lib/types' +import MediaModal from '@components/Modals/MediaModal' export default function CertificateSection() { const [certificates, setCertificates] = useState([]) @@ -42,43 +41,85 @@ export default function CertificateSection() { className="grid grid-cols-1 mb-10" >
-

Here are some certificates that I have obtained.

- {certificates.map((cer: CertificateType) => { +

Here are some Certificates that I have obtained.

+ {certificates.map((certificate: CertificateType) => { return ( -
-
- {cer.orgName} +
+ {certificate.organization} +
+
+
+
+
+ {certificate.credential_url ? ( + + {certificate.title} + + ) : ( +

+ {certificate.title} +

+ )} + +

+ • Organization: {certificate.organization} + {certificate.address ? , {certificate.address} : null} +

+
+
-
- - {cer.title} - -

- {cer.orgName} •{" "} - {getFormattedDate(new Date(cer.issuedDate))} -

+ +
+
+ • Issue Date: {certificate.issue_date} + • Expiration Date: {certificate.expiration_date} + {certificate.credential_id ? ( + • Credential ID: {certificate.credential_id} + ): null} + + {/* Certification Media */} + {certificate.certification_media?.length ? ( + // Here there will be a list of media. bullet points. There will be a button. After clicking the button new modal will open with the list of media. +
+
+

Attachments

+ + {certificate.certification_media.map((media: MediaType, mediaIndex) => ( +
+ +
+ ))} +
+
+ ) : null} +
-

) })} diff --git a/frontend/components/Education.tsx b/frontend/components/Education.tsx index dd5c583..a75f456 100644 --- a/frontend/components/Education.tsx +++ b/frontend/components/Education.tsx @@ -4,7 +4,7 @@ import { motion } from 'framer-motion' import React from 'react' import Image from 'next/image' import { TimelineList } from '@components/TimelineList' -import { EducationType, EducationMediaType } from '@lib/types' +import { EducationType, MediaType } from '@lib/types' import MediaModal from '@components/Modals/MediaModal' @@ -14,9 +14,6 @@ export default function EducationSection({ educations }: { educations: Education return
Loading...
} // ******* Loader Ends ******* - const handleClick = (param: any) => { - console.log('Button clicked with parameter:', param); - } return (
@@ -37,9 +34,9 @@ export default function EducationSection({ educations }: { educations: Education -
+

Attachments

- {education.education_media.map((media: EducationMediaType, mediaIndex) => ( + {education.education_media.map((media: MediaType, mediaIndex) => (
{skills.map((skill: SkillType, index) => { const level = Number(skill.level) || 0 // Convert level to a number or use 0 if it's null or invalid @@ -55,7 +55,7 @@ export default function SkillSection() { {skill.title}
-

+

{skill.title}

diff --git a/frontend/components/Modals/MediaModal.tsx b/frontend/components/Modals/MediaModal.tsx index 00fa9a2..8bffda8 100644 --- a/frontend/components/Modals/MediaModal.tsx +++ b/frontend/components/Modals/MediaModal.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' import ReactModal from 'react-modal' +import PDFViewer from '@components/PDFViewer' // Set the app element to avoid accessibility warnings ReactModal.setAppElement('#__next') @@ -21,9 +22,27 @@ const MediaModal: React.FC = ({ title, file, description }) => setModalIsOpen(false) } + function getFileExtensionFromBase64(base64String: string): string { + const mimeType = base64String.match(/data:(.*?);/)?.[1] + const [, fileExtension] = mimeType?.split('/') ?? [] + + return fileExtension || '' + } + + const renderFile = (file: string) => { + const fileExtension = getFileExtensionFromBase64(file) + if (fileExtension === 'pdf') { + return + } + return {title} + } + return ( <> - @@ -34,27 +53,40 @@ const MediaModal: React.FC = ({ title, file, description }) => className="modal dark:bg-slate-800 dark:text-slate-100" overlayClassName="modal-overlay" > - -

{title}

- {title} -

{description}

- - + {/* Modal Body */} +
+ {/* Header Section */} +
+ +

{title}

+
+ + {/* Modal Content */} +
+ {/* File media */} + {renderFile(file)} + +

{description}

+
+ + {/* Modal Footer */} + +
) diff --git a/frontend/components/OgImage.tsx b/frontend/components/OgImage.tsx index c040fe7..69da7d6 100644 --- a/frontend/components/OgImage.tsx +++ b/frontend/components/OgImage.tsx @@ -1,7 +1,7 @@ import Image from "next/image"; function OgImage({ src, alt }: { src: string; alt: string }) { return ( -
+
{alt} = ({ base64String }) => { + const [numPages, setNumPages] = useState(null) + const [isSmallDevice, setIsSmallDevice] = useState(false) + + useLayoutEffect(() => { + const handleResize = () => { + setIsSmallDevice(window.innerWidth < 1024) + } + + handleResize() // Initial check + + window.addEventListener('resize', handleResize) + + return () => { + window.removeEventListener('resize', handleResize) + } + }, []) + + const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => { + setNumPages(numPages) + } + + return ( +
+ + {Array.from(new Array(numPages || 0), (el, index) => ( +
+ +
+ ))} +
+
+ ) +} + +export default PDFViewer diff --git a/frontend/components/PageTop.tsx b/frontend/components/PageTop.tsx index 3822bf1..9025b03 100644 --- a/frontend/components/PageTop.tsx +++ b/frontend/components/PageTop.tsx @@ -22,7 +22,7 @@ export default function PageTop({ > {pageTitle} diff --git a/frontend/lib/backendAPI.ts b/frontend/lib/backendAPI.ts index 6dec5c8..1ac0846 100644 --- a/frontend/lib/backendAPI.ts +++ b/frontend/lib/backendAPI.ts @@ -105,7 +105,7 @@ const EDUCATIONS_PATH = "educations/" const EDUCATIONS_ENDPOINT = "http://127.0.0.1:8000/api/" + EDUCATIONS_PATH /** - * Makes a request to the BACKEND API to retrieve all Skills Data. + * Makes a request to the BACKEND API to retrieve all Educations Data. */ export const getAllEducations = async () => { const allEducations = await fetch( @@ -126,6 +126,34 @@ export const getAllEducations = async () => { } } +// *** CERTIFICATES *** + +// Certificates URL +const CERTIFICATES_PATH = "certifications/" +const CERTIFICATES_ENDPOINT = "http://127.0.0.1:8000/api/" + CERTIFICATES_PATH + +/** + * Makes a request to the BACKEND API to retrieve all Certificates Data. + */ +export const getAllCertificates = async () => { + const allCertificates = await fetch( + CERTIFICATES_ENDPOINT, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } + ) + + if (allCertificates.ok) { + const responseData = await allCertificates.json() + return responseData.data + } else { + const errorMessage = `Error fetching Educations: ${allCertificates.status} ${allCertificates.statusText}` + console.log(errorMessage) + } +} + // *** BLOGS *** @@ -203,39 +231,6 @@ export const getAllProjects = async () => { return fakeProjectsData } -// *** CERTIFICATES *** - -// Certificate URL -const CERTIFICATES_PATH = "/posts?_limit=5" -const CERTIFICATES_ENDPOINT = BACKEND_API_BASE_URL + CERTIFICATES_PATH - -/** - * Makes a request to the BACKEND API to retrieve all Certificate Data. - */ -export const getAllCertificates = async () => { - - const allCertificates = await fetch( - CERTIFICATES_ENDPOINT - ) - .then((response) => response.json()) - .catch((error) => console.log('Error fetching Certificates:', error)) - - // TODO:Integrate with backend API - // ******* Faking data Starts ******* - const fakeCertificatesData = allCertificates.map((certificate: { title: any, body: any }, index: number) => ({ - id: index, - title: certificate.title.split(' ').slice(0, 3).join(' ').toUpperCase(), - orgName: "Hackerrank", - orgLogo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQYY0pvHu6oaaJRADcCoacoP5BKwJN0i1nqFNCnmKvN&s", - issuedDate: new Date(), - url: "https://github.com/NumanIbnMazid" - })) - // Need to return `allExperiences` - // ******* Faking data Ends ******* - - return fakeCertificatesData -} - // *** MOVIES *** diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts index 23ab4fb..05f2901 100644 --- a/frontend/lib/types.ts +++ b/frontend/lib/types.ts @@ -113,9 +113,8 @@ export type SkillType = { updated_at: string } -export type EducationMediaType = { +export type MediaType = { id: number - education: number title: string slug: string file: string @@ -136,7 +135,7 @@ export type EducationType = { grade?: string activities?: string description?: string - education_media?: EducationMediaType[] + education_media?: MediaType[] created_at: string updated_at: string } @@ -144,11 +143,18 @@ export type EducationType = { export type CertificateType = { id: string title: string - issuedDate: string - orgName: string - orgLogo: string - url: string - pinned: boolean + slug: string + organization: string + address?: string + image: string + issue_date: string + expiration_date?: string + credential_id?: string + credential_url?: string + description?: string + certification_media?: MediaType[] + created_at: string + updated_at: string } export type SocialPlatform = { diff --git a/frontend/package.json b/frontend/package.json index 7b6ad62..5fd7496 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "react-dom": "^18.2.0", "react-icons": "^4.3.1", "react-modal": "^3.16.1", + "react-pdf": "^7.1.2", "react-qr-code": "^2.0.7", "react-ripples": "^2.2.1", "react-share": "^4.4.0", diff --git a/frontend/styles/globals.css b/frontend/styles/globals.css index 7cb4681..0a37b9a 100644 --- a/frontend/styles/globals.css +++ b/frontend/styles/globals.css @@ -321,10 +321,11 @@ code > .line::before { .modal { position: relative; background-color: #fff; - border-radius: 4px; + border-radius: 15px; padding: 20px; - max-width: 500px; - width: 100%; + padding-top: 0px !important; + min-width: 400px; + max-width: 100%; max-height: 80vh; overflow-y: auto; } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index e6c10ba..1507fd8 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1055,6 +1055,21 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@mapbox/node-pre-gyp@^1.0.0": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c" + integrity sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + "@mdx-js/mdx@2.1.5", "@mdx-js/mdx@^2.0.0": version "2.1.5" resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-2.1.5.tgz#577937349fd555154382c2f805f5f52834a64903" @@ -1919,6 +1934,11 @@ "@typescript-eslint/types" "5.59.7" eslint-visitor-keys "^3.3.0" +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz" @@ -2019,6 +2039,19 @@ app-module-path@^2.2.0: resolved "https://registry.npmjs.org/app-module-path/-/app-module-path-2.2.0.tgz" integrity sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ== +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + arg@^5.0.2: version "5.0.2" resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz" @@ -2323,6 +2356,15 @@ caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.300014 resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz" integrity sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A== +canvas@^2.11.2: + version "2.11.2" + resolved "https://registry.yarnpkg.com/canvas/-/canvas-2.11.2.tgz#553d87b1e0228c7ac0fc72887c3adbac4abbd860" + integrity sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.0" + nan "^2.17.0" + simple-get "^3.0.3" + catharsis@^0.9.0: version "0.9.0" resolved "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz" @@ -2387,6 +2429,11 @@ chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + classnames@^2.3.2: version "2.3.2" resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz" @@ -2430,7 +2477,7 @@ clone@^1.0.2: resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== -clsx@^1.1.1: +clsx@^1.1.1, clsx@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== @@ -2459,6 +2506,11 @@ color-name@^1.1.4, color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-support@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + comma-separated-tokens@^2.0.0: version "2.0.3" resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz" @@ -2489,6 +2541,11 @@ concat-map@0.0.1: resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +console-control-strings@^1.0.0, console-control-strings@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz" @@ -2583,6 +2640,13 @@ decode-named-character-reference@^1.0.0: dependencies: character-entities "^2.0.0" +decompress-response@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" + integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== + dependencies: + mimic-response "^2.0.0" + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" @@ -2636,6 +2700,11 @@ del@^4.1.1: pify "^4.0.1" rimraf "^2.6.3" +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + dependency-tree@^8.0.0: version "8.1.2" resolved "https://registry.npmjs.org/dependency-tree/-/dependency-tree-8.1.2.tgz" @@ -2652,6 +2721,11 @@ dequal@^2.0.0: resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== +detect-libc@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" + integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== + detective-amd@^3.0.1, detective-amd@^3.1.0: version "3.1.2" resolved "https://registry.npmjs.org/detective-amd/-/detective-amd-3.1.2.tgz" @@ -3429,6 +3503,13 @@ fs-extra@^9.0.1: jsonfile "^6.0.1" universalify "^2.0.0" +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" @@ -3464,6 +3545,21 @@ functions-have-names@^1.2.2: resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + gaxios@^5.0.0, gaxios@^5.0.1: version "5.0.2" resolved "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz" @@ -3751,6 +3847,11 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" +has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== + has@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" @@ -4513,6 +4614,11 @@ magic-string@^0.25.0, magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.8" +make-cancellable-promise@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/make-cancellable-promise/-/make-cancellable-promise-1.3.1.tgz#3bd89704c75afe6251cdd6a82baca1fcfbd2c792" + integrity sha512-DWOzWdO3xhY5ESjVR+wVFy03rpt0ZccS4bunccNwngoX6rllKlMZm6S9ZnJ5nMuDDweqDMjtaO0g6tZeh+cCUA== + make-dir@^3.0.2, make-dir@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" @@ -4520,6 +4626,11 @@ make-dir@^3.0.2, make-dir@^3.1.0: dependencies: semver "^6.0.0" +make-event-props@^1.5.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/make-event-props/-/make-event-props-1.6.1.tgz#1d587017c3f1f3b42719b775af93d5253656ccdd" + integrity sha512-JhvWq/iz1BvlmnPvLJjXv+xnMPJZuychrDC68V+yCGQJn5chcA8rLGKo5EP1XwIKVrigSXKLmbeXAGkf36wdCQ== + markdown-extensions@^1.0.0: version "1.1.1" resolved "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz" @@ -4666,6 +4777,13 @@ memory-fs@^0.5.0: errno "^0.1.3" readable-stream "^2.0.1" +merge-refs@^1.1.3: + version "1.2.1" + resolved "https://registry.yarnpkg.com/merge-refs/-/merge-refs-1.2.1.tgz#abddc800375395a4a4eb5c45ebf2a52557fdbe34" + integrity sha512-pRPz39HQz2xzHdXAGvtJ9S8aEpNgpUjzb5yPC3ytozodmsHg+9nqgRs7/YOmn9fM/TLzntAC8AdGTidKxOq9TQ== + dependencies: + "@types/react" "*" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" @@ -4986,6 +5104,11 @@ mimic-fn@^2.1.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-response@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" + integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== + minimatch@^3.0.4, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" @@ -5005,7 +5128,27 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== -mkdirp@^1.0.4: +minipass@^3.0.0: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -5044,6 +5187,11 @@ ms@2.1.2, ms@^2.1.1: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +nan@^2.17.0: + version "2.17.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" + integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== + nanoid@^3.3.4: version "3.3.4" resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz" @@ -5180,6 +5328,13 @@ node-source-walk@^4.0.0, node-source-walk@^4.2.0, node-source-walk@^4.2.2: dependencies: "@babel/parser" "^7.0.0" +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" @@ -5190,6 +5345,16 @@ normalize-range@^0.1.2: resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" + nprogress@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz" @@ -5260,7 +5425,7 @@ object.values@^1.1.5, object.values@^1.1.6: define-properties "^1.1.4" es-abstract "^1.20.4" -once@^1.3.0, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== @@ -5407,6 +5572,21 @@ path-type@^4.0.0: resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +path2d-polyfill@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz#24c554a738f42700d6961992bf5f1049672f2391" + integrity sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA== + +pdfjs-dist@3.6.172: + version "3.6.172" + resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-3.6.172.tgz#f9efdfc5e850e1fecfc70b7f6f45c5dc990d8096" + integrity sha512-bfOhCg+S9DXh/ImWhWYTOiq3aVMFSCvzGiBzsIJtdMC71kVWDBw7UXr32xh0y56qc5wMVylIeqV3hBaRsu+e+w== + dependencies: + path2d-polyfill "^2.0.1" + web-streams-polyfill "^3.2.1" + optionalDependencies: + canvas "^2.11.2" + periscopic@^3.0.0: version "3.0.4" resolved "https://registry.npmjs.org/periscopic/-/periscopic-3.0.4.tgz" @@ -5605,7 +5785,7 @@ process-nextick-args@~2.0.0: resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -5735,6 +5915,20 @@ react-modal@^3.16.1: react-lifecycles-compat "^3.0.0" warning "^4.0.3" +react-pdf@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/react-pdf/-/react-pdf-7.1.2.tgz#c6979cff9ac09c3e5ab7ea9e0182f79a499768e5" + integrity sha512-hmTUKh3WVYDJlP8XvebGN8HH0Gk/tXh9WgNAtvdHn79FHL78UEPSbVj3veHHGqmMa2hz1wJCItLUqGVP68Qsjw== + dependencies: + clsx "^1.2.1" + make-cancellable-promise "^1.0.0" + make-event-props "^1.5.0" + merge-refs "^1.1.3" + pdfjs-dist "3.6.172" + prop-types "^15.6.2" + tiny-invariant "^1.0.0" + tiny-warning "^1.0.0" + react-qr-code@^2.0.7: version "2.0.11" resolved "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.11.tgz" @@ -5818,6 +6012,15 @@ readable-stream@^3.1.1, readable-stream@^3.4.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" @@ -6169,6 +6372,11 @@ serialize-javascript@^6.0.1: dependencies: randombytes "^2.1.0" +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" @@ -6199,11 +6407,25 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -signal-exit@^3.0.2: +signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.7" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.1.tgz#cc7ba77cfbe761036fbfce3d021af25fc5584d55" + integrity sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA== + dependencies: + decompress-response "^4.2.0" + once "^1.3.1" + simple-concat "^1.0.0" + slash@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" @@ -6269,7 +6491,7 @@ stream-shift@^1.0.0: resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== -string-width@^4.1.0, string-width@^4.2.0: +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6491,6 +6713,18 @@ tapable@^2.2.0: resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== +tar@^6.1.11: + version "6.1.15" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.15.tgz#c9738b0b98845a3b344d334b8fa3041aaba53a69" + integrity sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + temp-dir@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz" @@ -6545,6 +6779,16 @@ tiny-glob@^0.2.9: globalyzer "0.1.0" globrex "^0.1.2" +tiny-invariant@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" + integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== + +tiny-warning@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + tmp@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz" @@ -6932,6 +7176,11 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +web-streams-polyfill@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" + integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" @@ -6997,6 +7246,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +wide-align@^1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" diff --git a/project/config/api_router.py b/project/config/api_router.py index 648f0da..d1fe892 100644 --- a/project/config/api_router.py +++ b/project/config/api_router.py @@ -3,6 +3,7 @@ from portfolios.api.professional_experiences.routers import * from portfolios.api.skills.routers import * from portfolios.api.educations.routers import * +from portfolios.api.certifications.routers import * app_name = "api" diff --git a/project/portfolios/admin.py b/project/portfolios/admin.py index dbe7f0b..ddc6e7a 100644 --- a/project/portfolios/admin.py +++ b/project/portfolios/admin.py @@ -2,7 +2,7 @@ from django.db import models from utils.mixins import CustomModelAdminMixin from portfolios.models import ( - ProfessionalExperience, Skill, Education, EducationMedia + ProfessionalExperience, Skill, Education, EducationMedia, Certification, CertificationMedia ) from ckeditor.widgets import CKEditorWidget @@ -47,3 +47,20 @@ class Meta: model = Education admin.site.register(Education, EducationAdmin) + + +# ---------------------------------------------------- +# *** Certification *** +# ---------------------------------------------------- + +class CertificationMediaAdmin(admin.StackedInline): + model = CertificationMedia + + +class CertificationAdmin(CustomModelAdminMixin, admin.ModelAdmin): + inlines = [CertificationMediaAdmin] + + class Meta: + model = Certification + +admin.site.register(Certification, CertificationAdmin) diff --git a/project/portfolios/api/certifications/__init__.py b/project/portfolios/api/certifications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/portfolios/api/certifications/routers.py b/project/portfolios/api/certifications/routers.py new file mode 100644 index 0000000..c37dc6e --- /dev/null +++ b/project/portfolios/api/certifications/routers.py @@ -0,0 +1,5 @@ +from config.router import router +from portfolios.api.certifications.views import CertificationViewset + + +router.register("certifications", CertificationViewset, basename="certifications") diff --git a/project/portfolios/api/certifications/serializers.py b/project/portfolios/api/certifications/serializers.py new file mode 100644 index 0000000..3b0c54e --- /dev/null +++ b/project/portfolios/api/certifications/serializers.py @@ -0,0 +1,30 @@ +from rest_framework import serializers +from portfolios.models import Certification, CertificationMedia + + +class CertificationMediaSerializer(serializers.ModelSerializer): + file = serializers.SerializerMethodField() + class Meta: + model = CertificationMedia + fields = ("id", "title", "slug", "file", "description") + read_only_fields = ("id", "slug") + + def get_file(self, obj): + return obj.get_file() + + +class CertificationSerializer(serializers.ModelSerializer): + image = serializers.SerializerMethodField() + certification_media = CertificationMediaSerializer(many=True, read_only=True) + expiration_date = serializers.SerializerMethodField() + + class Meta: + model = Certification + fields = "__all__" + read_only_fields = ("id", "slug", "created_at", "updated_at") + + def get_image(self, obj): + return obj.get_image() + + def get_expiration_date(self, obj): + return obj.get_expiration_date() diff --git a/project/portfolios/api/certifications/views.py b/project/portfolios/api/certifications/views.py new file mode 100644 index 0000000..26b7ff1 --- /dev/null +++ b/project/portfolios/api/certifications/views.py @@ -0,0 +1,13 @@ +from rest_framework import permissions +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin +from utils.helpers import custom_response_wrapper +from portfolios.models import Certification +from portfolios.api.certifications.serializers import CertificationSerializer + + +@custom_response_wrapper +class CertificationViewset(GenericViewSet, ListModelMixin): + permission_classes = (permissions.IsAuthenticated,) + queryset = Certification.objects.all() + serializer_class = CertificationSerializer diff --git a/project/portfolios/migrations/0014_certification.py b/project/portfolios/migrations/0014_certification.py new file mode 100644 index 0000000..0146ff4 --- /dev/null +++ b/project/portfolios/migrations/0014_certification.py @@ -0,0 +1,58 @@ +# Generated by Django 4.2.1 on 2023-06-17 09:34 + +from django.db import migrations, models +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0013_educationmedia_title"), + ] + + operations = [ + migrations.CreateModel( + name="Certification", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=150)), + ("slug", models.SlugField(blank=True, max_length=255, unique=True)), + ("organization", models.CharField(max_length=150)), + ("address", models.CharField(blank=True, max_length=254, null=True)), + ( + "image", + models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_education_school_image_path, + ), + ), + ("issue_date", models.DateField()), + ("expiration_date", models.DateField(blank=True, null=True)), + ("does_not_expire", models.BooleanField(default=False)), + ( + "credential_id", + models.CharField(blank=True, max_length=254, null=True), + ), + ("credential_url", models.URLField(blank=True, null=True)), + ("description", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Certification", + "verbose_name_plural": "Certifications", + "db_table": "certification", + "ordering": ["-issue_date"], + "get_latest_by": "created_at", + }, + ), + ] diff --git a/project/portfolios/migrations/0015_certificationmedia.py b/project/portfolios/migrations/0015_certificationmedia.py new file mode 100644 index 0000000..64f8d64 --- /dev/null +++ b/project/portfolios/migrations/0015_certificationmedia.py @@ -0,0 +1,55 @@ +# Generated by Django 4.2.1 on 2023-06-17 09:41 + +from django.db import migrations, models +import django.db.models.deletion +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0014_certification"), + ] + + operations = [ + migrations.CreateModel( + name="CertificationMedia", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=150)), + ("slug", models.SlugField(blank=True, max_length=255, unique=True)), + ( + "file", + models.FileField( + upload_to=utils.image_upload_helpers.get_education_media_path + ), + ), + ("description", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "certification", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="certification_media", + to="portfolios.certification", + ), + ), + ], + options={ + "verbose_name": "Certification Media", + "verbose_name_plural": "Certification Media", + "db_table": "certification_media", + "get_latest_by": "created_at", + "order_with_respect_to": "certification", + }, + ), + ] diff --git a/project/portfolios/migrations/0016_alter_certification_image_and_more.py b/project/portfolios/migrations/0016_alter_certification_image_and_more.py new file mode 100644 index 0000000..a4a3aa1 --- /dev/null +++ b/project/portfolios/migrations/0016_alter_certification_image_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.1 on 2023-06-17 09:49 + +from django.db import migrations, models +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0015_certificationmedia"), + ] + + operations = [ + migrations.AlterField( + model_name="certification", + name="image", + field=models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_certification_image_path, + ), + ), + migrations.AlterField( + model_name="certificationmedia", + name="file", + field=models.FileField( + upload_to=utils.image_upload_helpers.get_certification_media_path + ), + ), + ] diff --git a/project/portfolios/models.py b/project/portfolios/models.py index 2eeef04..0dd03aa 100644 --- a/project/portfolios/models.py +++ b/project/portfolios/models.py @@ -9,7 +9,8 @@ from utils.helpers import CustomModelManager from utils.snippets import autoSlugWithFieldAndUUID, autoSlugFromUUID, image_as_base64, get_static_file_path from utils.image_upload_helpers import ( - get_professional_experience_company_image_path, get_skill_image_path, get_education_school_image_path, get_education_media_path + get_professional_experience_company_image_path, get_skill_image_path, get_education_school_image_path, get_education_media_path, + get_certification_image_path, get_certification_media_path ) from ckeditor.fields import RichTextField @@ -273,3 +274,80 @@ def get_file(self): file_path = settings.MEDIA_ROOT + self.file.url.lstrip("/media/") return image_as_base64(file_path) return + + +""" *************** Certification *************** """ + + +@autoSlugWithFieldAndUUID(fieldname="title") +class Certification(models.Model): + title = models.CharField(max_length=150) + slug = models.SlugField(max_length=255, unique=True, blank=True) + organization = models.CharField(max_length=150) + address = models.CharField(max_length=254, blank=True, null=True) + image = models.ImageField(upload_to=get_certification_image_path, blank=True, null=True) + issue_date = models.DateField() + expiration_date = models.DateField(blank=True, null=True) + does_not_expire = models.BooleanField(default=False) + credential_id = models.CharField(max_length=254, blank=True, null=True) + credential_url = models.URLField(blank=True, null=True) + description = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'certification' + verbose_name = _('Certification') + verbose_name_plural = _('Certifications') + ordering = ['-issue_date'] + get_latest_by = "created_at" + + def __str__(self): + return self.title + + def get_image(self): + if self.image: + image_path = settings.MEDIA_ROOT + self.image.url.lstrip("/media/") + else: + image_path = get_static_file_path("icons/certificate.png") + return image_as_base64(image_path) + + def get_expiration_date(self): + if self.does_not_expire: + return _('Does not expire') + elif self.expiration_date: + return self.expiration_date.strftime("%B %Y") + return _('Not Specified') + + +@autoSlugFromUUID() +class CertificationMedia(models.Model): + certification = models.ForeignKey(Certification, on_delete=models.CASCADE, related_name="certification_media") + title = models.CharField(max_length=150) + slug = models.SlugField(max_length=255, unique=True, blank=True) + file = models.FileField(upload_to=get_certification_media_path) + description = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'certification_media' + verbose_name = _('Certification Media') + verbose_name_plural = _('Certification Media') + get_latest_by = "created_at" + order_with_respect_to = 'certification' + + def __str__(self): + return self.certification.__str__() + + def get_file(self): + if self.file: + file_path = settings.MEDIA_ROOT + self.file.url.lstrip("/media/") + return image_as_base64(file_path) + return diff --git a/project/public/staticfiles/icons/certificate.png b/project/public/staticfiles/icons/certificate.png new file mode 100644 index 0000000000000000000000000000000000000000..0bc28fed6176beb8c75716e5cc10e475175c5f26 GIT binary patch literal 16700 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 SgoryIMfI`fBjf|i;Qs@0Hl}U> literal 0 HcmV?d00001 diff --git a/project/utils/image_upload_helpers.py b/project/utils/image_upload_helpers.py index ad83f3a..8e3165a 100644 --- a/project/utils/image_upload_helpers.py +++ b/project/utils/image_upload_helpers.py @@ -48,3 +48,17 @@ def get_education_media_path(instance, filename): return "Educations/{school}/Media/{final_filename}".format( school=slugify(instance.education.school[:50]), final_filename=new_filename ) + + +# Certification Image Path +def get_certification_image_path(instance, filename): + new_filename = get_filename(filename) + return "Certifications/{organization}/Images/{final_filename}".format( + organization=slugify(instance.organization[:50]), final_filename=new_filename + ) + +def get_certification_media_path(instance, filename): + new_filename = get_filename(filename) + return "Certifications/{organization}/Media/{final_filename}".format( + organization=slugify(instance.certification.organization[:50]), final_filename=new_filename + ) From 2df392bf6361b186b030b4579b0d01dddcef2bc9 Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Sun, 18 Jun 2023 02:53:38 +0600 Subject: [PATCH 10/45] Media Modal Updated --- frontend/components/Modals/MediaModal.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/components/Modals/MediaModal.tsx b/frontend/components/Modals/MediaModal.tsx index 8bffda8..be83a69 100644 --- a/frontend/components/Modals/MediaModal.tsx +++ b/frontend/components/Modals/MediaModal.tsx @@ -1,6 +1,7 @@ import { useState } from 'react' import ReactModal from 'react-modal' import PDFViewer from '@components/PDFViewer' +import Image from 'next/image' // Set the app element to avoid accessibility warnings ReactModal.setAppElement('#__next') @@ -34,13 +35,13 @@ const MediaModal: React.FC = ({ title, file, description }) => if (fileExtension === 'pdf') { return } - return {title} + return {title} } return ( <>
diff --git a/frontend/pages/projects.tsx b/frontend/pages/projects.tsx index 326bc86..9d71a5b 100644 --- a/frontend/pages/projects.tsx +++ b/frontend/pages/projects.tsx @@ -1,10 +1,25 @@ import React from "react" -import ProjectSection from "@components/Projects" +import ProjectSection from "@components/ProjectSection" import { motion } from "framer-motion" import { FadeContainer } from "@content/FramerMotionVariants" +import { ProjectType } from "@lib/types" +import { useEffect, useState } from "react" +import { getAllProjects } from "@lib/backendAPI" + export default function Projects() { + const [projects, setProjects] = useState([]) + + const fetchProjects = async () => { + const projectsData: ProjectType[] = await getAllProjects() + setProjects(projectsData) + } + + useEffect(() => { + fetchProjects() + }, []) + return ( <>
@@ -16,7 +31,7 @@ export default function Projects() { className="grid min-h-screen py-20 place-content-center" >
- +
diff --git a/project/config/api_router.py b/project/config/api_router.py index d1fe892..8719b0a 100644 --- a/project/config/api_router.py +++ b/project/config/api_router.py @@ -4,6 +4,7 @@ from portfolios.api.skills.routers import * from portfolios.api.educations.routers import * from portfolios.api.certifications.routers import * +from portfolios.api.projects.routers import * app_name = "api" diff --git a/project/portfolios/admin.py b/project/portfolios/admin.py index ddc6e7a..60262da 100644 --- a/project/portfolios/admin.py +++ b/project/portfolios/admin.py @@ -2,7 +2,7 @@ from django.db import models from utils.mixins import CustomModelAdminMixin from portfolios.models import ( - ProfessionalExperience, Skill, Education, EducationMedia, Certification, CertificationMedia + ProfessionalExperience, Skill, Education, EducationMedia, Certification, CertificationMedia, Project, ProjectMedia ) from ckeditor.widgets import CKEditorWidget @@ -64,3 +64,20 @@ class Meta: model = Certification admin.site.register(Certification, CertificationAdmin) + + +# ---------------------------------------------------- +# *** Project *** +# ---------------------------------------------------- + +class ProjectMediaAdmin(admin.StackedInline): + model = ProjectMedia + + +class ProjectAdmin(CustomModelAdminMixin, admin.ModelAdmin): + inlines = [ProjectMediaAdmin] + + class Meta: + model = Project + +admin.site.register(Project, ProjectAdmin) diff --git a/project/portfolios/api/certifications/serializers.py b/project/portfolios/api/certifications/serializers.py index 3b0c54e..4fbd4fc 100644 --- a/project/portfolios/api/certifications/serializers.py +++ b/project/portfolios/api/certifications/serializers.py @@ -16,6 +16,7 @@ def get_file(self, obj): class CertificationSerializer(serializers.ModelSerializer): image = serializers.SerializerMethodField() certification_media = CertificationMediaSerializer(many=True, read_only=True) + issue_date = serializers.SerializerMethodField() expiration_date = serializers.SerializerMethodField() class Meta: @@ -26,5 +27,8 @@ class Meta: def get_image(self, obj): return obj.get_image() + def get_issue_date(self, obj): + return obj.get_issue_date() + def get_expiration_date(self, obj): return obj.get_expiration_date() diff --git a/project/portfolios/api/projects/__init__.py b/project/portfolios/api/projects/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/portfolios/api/projects/routers.py b/project/portfolios/api/projects/routers.py new file mode 100644 index 0000000..9c952ce --- /dev/null +++ b/project/portfolios/api/projects/routers.py @@ -0,0 +1,5 @@ +from config.router import router +from portfolios.api.projects.views import ProjectViewset + + +router.register("projects", ProjectViewset, basename="projects") diff --git a/project/portfolios/api/projects/serializers.py b/project/portfolios/api/projects/serializers.py new file mode 100644 index 0000000..1af60e8 --- /dev/null +++ b/project/portfolios/api/projects/serializers.py @@ -0,0 +1,34 @@ +from rest_framework import serializers +from portfolios.models import Project, ProjectMedia + + +class ProjectMediaMediaSerializer(serializers.ModelSerializer): + file = serializers.SerializerMethodField() + class Meta: + model = ProjectMedia + fields = ("id", "title", "slug", "file", "description") + read_only_fields = ("id", "slug") + + def get_file(self, obj): + return obj.get_file() + + +class ProjectSerializer(serializers.ModelSerializer): + image = serializers.SerializerMethodField() + duration = serializers.SerializerMethodField() + duration_in_days = serializers.SerializerMethodField() + project_media = ProjectMediaMediaSerializer(many=True, read_only=True) + + class Meta: + model = Project + fields = "__all__" + read_only_fields = ("id", "slug", "created_at", "updated_at") + + def get_image(self, obj): + return obj.get_image() + + def get_duration(self, obj): + return obj.get_duration() + + def get_duration_in_days(self, obj): + return obj.get_duration_in_days() diff --git a/project/portfolios/api/projects/views.py b/project/portfolios/api/projects/views.py new file mode 100644 index 0000000..f34d418 --- /dev/null +++ b/project/portfolios/api/projects/views.py @@ -0,0 +1,13 @@ +from rest_framework import permissions +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin +from utils.helpers import custom_response_wrapper +from portfolios.models import Project +from portfolios.api.projects.serializers import ProjectSerializer + + +@custom_response_wrapper +class ProjectViewset(GenericViewSet, ListModelMixin): + permission_classes = (permissions.IsAuthenticated,) + queryset = Project.objects.all() + serializer_class = ProjectSerializer diff --git a/project/portfolios/migrations/0017_project_alter_certification_title_projectmedia.py b/project/portfolios/migrations/0017_project_alter_certification_title_projectmedia.py new file mode 100644 index 0000000..c928e7e --- /dev/null +++ b/project/portfolios/migrations/0017_project_alter_certification_title_projectmedia.py @@ -0,0 +1,99 @@ +# Generated by Django 4.2.1 on 2023-06-22 17:18 + +from django.db import migrations, models +import django.db.models.deletion +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0016_alter_certification_image_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="Project", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=200, unique=True)), + ("slug", models.SlugField(blank=True, max_length=255, unique=True)), + ( + "image", + models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_project_image_path, + ), + ), + ("short_description", models.CharField(max_length=254)), + ("technology", models.TextField(blank=True, null=True)), + ("start_date", models.DateField()), + ("end_date", models.DateField(blank=True, null=True)), + ("currently_working", models.BooleanField(default=False)), + ("url", models.URLField(blank=True, null=True)), + ("description", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Project", + "verbose_name_plural": "Projects", + "db_table": "project", + "ordering": ["-created_at"], + "get_latest_by": "created_at", + }, + ), + migrations.AlterField( + model_name="certification", + name="title", + field=models.CharField(max_length=150, unique=True), + ), + migrations.CreateModel( + name="ProjectMedia", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("slug", models.SlugField(max_length=255, unique=True)), + ( + "file", + models.FileField( + upload_to=utils.image_upload_helpers.get_project_media_path + ), + ), + ("description", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_media", + to="portfolios.project", + ), + ), + ], + options={ + "verbose_name": "Project Media", + "verbose_name_plural": "Project Media", + "db_table": "project_media", + "get_latest_by": "created_at", + "order_with_respect_to": "project", + }, + ), + ] diff --git a/project/portfolios/migrations/0018_remove_certificationmedia_created_at_and_more.py b/project/portfolios/migrations/0018_remove_certificationmedia_created_at_and_more.py new file mode 100644 index 0000000..5aa8110 --- /dev/null +++ b/project/portfolios/migrations/0018_remove_certificationmedia_created_at_and_more.py @@ -0,0 +1,69 @@ +# Generated by Django 4.2.1 on 2023-06-22 17:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0017_project_alter_certification_title_projectmedia"), + ] + + operations = [ + migrations.RemoveField( + model_name="certificationmedia", + name="created_at", + ), + migrations.RemoveField( + model_name="certificationmedia", + name="description", + ), + migrations.RemoveField( + model_name="certificationmedia", + name="slug", + ), + migrations.RemoveField( + model_name="certificationmedia", + name="title", + ), + migrations.RemoveField( + model_name="certificationmedia", + name="updated_at", + ), + migrations.RemoveField( + model_name="educationmedia", + name="created_at", + ), + migrations.RemoveField( + model_name="educationmedia", + name="description", + ), + migrations.RemoveField( + model_name="educationmedia", + name="slug", + ), + migrations.RemoveField( + model_name="educationmedia", + name="title", + ), + migrations.RemoveField( + model_name="educationmedia", + name="updated_at", + ), + migrations.RemoveField( + model_name="projectmedia", + name="created_at", + ), + migrations.RemoveField( + model_name="projectmedia", + name="description", + ), + migrations.RemoveField( + model_name="projectmedia", + name="slug", + ), + migrations.RemoveField( + model_name="projectmedia", + name="updated_at", + ), + ] diff --git a/project/portfolios/migrations/0019_certificationmedia_created_at_and_more.py b/project/portfolios/migrations/0019_certificationmedia_created_at_and_more.py new file mode 100644 index 0000000..7363e5f --- /dev/null +++ b/project/portfolios/migrations/0019_certificationmedia_created_at_and_more.py @@ -0,0 +1,101 @@ +# Generated by Django 4.2.1 on 2023-06-22 17:43 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0018_remove_certificationmedia_created_at_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="certificationmedia", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + migrations.AddField( + model_name="certificationmedia", + name="description", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="certificationmedia", + name="slug", + field=models.SlugField(blank=True, max_length=255, unique=True), + ), + migrations.AddField( + model_name="certificationmedia", + name="title", + field=models.CharField(default="Example", max_length=150), + preserve_default=False, + ), + migrations.AddField( + model_name="certificationmedia", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="educationmedia", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + migrations.AddField( + model_name="educationmedia", + name="description", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="educationmedia", + name="slug", + field=models.SlugField(blank=True, max_length=255, unique=True), + ), + migrations.AddField( + model_name="educationmedia", + name="title", + field=models.CharField(default="Example", max_length=150), + preserve_default=False, + ), + migrations.AddField( + model_name="educationmedia", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="projectmedia", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + migrations.AddField( + model_name="projectmedia", + name="description", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="projectmedia", + name="slug", + field=models.SlugField(blank=True, max_length=255, unique=True), + ), + migrations.AddField( + model_name="projectmedia", + name="title", + field=models.CharField(default="Example", max_length=150), + preserve_default=False, + ), + migrations.AddField( + model_name="projectmedia", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/project/portfolios/migrations/0020_project_order_alter_project_description.py b/project/portfolios/migrations/0020_project_order_alter_project_description.py new file mode 100644 index 0000000..794fb97 --- /dev/null +++ b/project/portfolios/migrations/0020_project_order_alter_project_description.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.1 on 2023-06-22 17:55 + +import ckeditor.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0019_certificationmedia_created_at_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="project", + name="order", + field=models.PositiveIntegerField(blank=True, default=1), + preserve_default=False, + ), + migrations.AlterField( + model_name="project", + name="description", + field=ckeditor.fields.RichTextField(blank=True, null=True), + ), + ] diff --git a/project/portfolios/migrations/0021_rename_url_project_github_url_project_preview_url.py b/project/portfolios/migrations/0021_rename_url_project_github_url_project_preview_url.py new file mode 100644 index 0000000..456df1a --- /dev/null +++ b/project/portfolios/migrations/0021_rename_url_project_github_url_project_preview_url.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.1 on 2023-06-22 18:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0020_project_order_alter_project_description"), + ] + + operations = [ + migrations.RenameField( + model_name="project", + old_name="url", + new_name="github_url", + ), + migrations.AddField( + model_name="project", + name="preview_url", + field=models.URLField(blank=True, null=True), + ), + ] diff --git a/project/portfolios/migrations/0022_alter_project_options.py b/project/portfolios/migrations/0022_alter_project_options.py new file mode 100644 index 0000000..fc5785d --- /dev/null +++ b/project/portfolios/migrations/0022_alter_project_options.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.1 on 2023-06-22 18:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0021_rename_url_project_github_url_project_preview_url"), + ] + + operations = [ + migrations.AlterModelOptions( + name="project", + options={ + "get_latest_by": "created_at", + "ordering": ["order"], + "verbose_name": "Project", + "verbose_name_plural": "Projects", + }, + ), + ] diff --git a/project/portfolios/migrations/0023_alter_professionalexperience_options_and_more.py b/project/portfolios/migrations/0023_alter_professionalexperience_options_and_more.py new file mode 100644 index 0000000..f4857ca --- /dev/null +++ b/project/portfolios/migrations/0023_alter_professionalexperience_options_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.1 on 2023-06-22 19:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0022_alter_project_options"), + ] + + operations = [ + migrations.AlterModelOptions( + name="professionalexperience", + options={ + "get_latest_by": "created_at", + "ordering": ("-present", "-start_date"), + "verbose_name": "Professional Experience", + "verbose_name_plural": "Professional Experiences", + }, + ), + migrations.RenameField( + model_name="education", + old_name="currently_studying", + new_name="present", + ), + migrations.RenameField( + model_name="professionalexperience", + old_name="currently_working", + new_name="present", + ), + migrations.RenameField( + model_name="project", + old_name="currently_working", + new_name="present", + ), + ] diff --git a/project/portfolios/models.py b/project/portfolios/models.py index 0dd03aa..6b45d10 100644 --- a/project/portfolios/models.py +++ b/project/portfolios/models.py @@ -7,10 +7,11 @@ from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ from utils.helpers import CustomModelManager +from utils.mixins import ModelMediaMixin, DurationMixin from utils.snippets import autoSlugWithFieldAndUUID, autoSlugFromUUID, image_as_base64, get_static_file_path from utils.image_upload_helpers import ( get_professional_experience_company_image_path, get_skill_image_path, get_education_school_image_path, get_education_media_path, - get_certification_image_path, get_certification_media_path + get_certification_image_path, get_certification_media_path, get_project_image_path, get_project_media_path ) from ckeditor.fields import RichTextField @@ -19,7 +20,7 @@ @autoSlugWithFieldAndUUID(fieldname="company") -class ProfessionalExperience(models.Model): +class ProfessionalExperience(models.Model, DurationMixin): """ Professional Experience model. Details: Includes Job Experiences and other professional experiences. @@ -39,7 +40,7 @@ class JobType(models.TextChoices): job_type = models.CharField(max_length=20, choices=JobType.choices, default=JobType.FULL_TIME) start_date = models.DateField() end_date = models.DateField(blank=True, null=True) - currently_working = models.BooleanField(default=False) + present = models.BooleanField(default=False) description = RichTextField(blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -51,62 +52,12 @@ class Meta: db_table = 'professional_experience' verbose_name = _('Professional Experience') verbose_name_plural = _('Professional Experiences') - ordering = ('-currently_working', '-start_date') + ordering = ('-present', '-start_date') get_latest_by = "created_at" def __str__(self): return self.company - def get_duration(self): - if self.end_date is None and not self.currently_working: - raise ValueError(_("End date is required to calculate duration in days. Please provide end date or mark as currently working.")) - if self.currently_working and self.end_date is not None: - raise ValueError(_("End date is not required when marked as currently working. Please remove end date or mark as not currently working.")) - - end_date = None - if self.end_date is not None: - end_date = self.end_date.strftime("%b %Y") - if self.currently_working: - end_date = _("Present") - start_date = self.start_date.strftime("%b %Y") - return f"{start_date} - {end_date}" - - def get_duration_in_days(self): - if self.end_date is None and not self.currently_working: - raise ValueError(_("End date is required to calculate duration in days. Please provide end date or mark as currently working.")) - if self.currently_working and self.end_date is not None: - raise ValueError(_("End date is not required when marked as currently working. Please remove end date or mark as not currently working.")) - - end_date = None - if self.end_date is not None: - end_date = self.end_date - if self.currently_working: - end_date = datetime.now().date() - - duration = end_date - self.start_date - - years = duration.days // 365 - months = (duration.days % 365) // 30 - # days = (duration.days % 365) % 30 - - duration_str = "" - if years > 0: - duration_str += f"{years} Year{'s' if years > 1 else ''}, " - if months > 0: - duration_str += f"{months} Month{'s' if months > 1 else ''}" - # if days > 0: - # duration_str += f"{days} Day{'s' if days > 1 else ''}" - - return duration_str - - def get_end_date(self): - if self.currently_working: - return _('Present') - elif self.end_date: - # return formatted date (supporting translation) - return dateformat.format(self.end_date, "F Y") - return _('Not Specified') - def get_company_image(self): if self.company_image: image_path = settings.MEDIA_ROOT + self.company_image.url.lstrip("/media/") @@ -159,6 +110,8 @@ def get_image(self): return image_as_base64(image_path) +# Signals + @receiver(pre_save, sender=Skill) def generate_order(sender, instance, **kwargs): """ @@ -188,7 +141,7 @@ def generate_order(sender, instance, **kwargs): @autoSlugWithFieldAndUUID(fieldname="school") -class Education(models.Model): +class Education(models.Model, DurationMixin): school = models.CharField(max_length=150, unique=True) slug = models.SlugField(max_length=255, unique=True, blank=True) image = models.ImageField(upload_to=get_education_school_image_path, blank=True, null=True) @@ -197,7 +150,7 @@ class Education(models.Model): field_of_study = models.CharField(max_length=200) start_date = models.DateField() end_date = models.DateField(blank=True, null=True) - currently_studying = models.BooleanField(default=False) + present = models.BooleanField(default=False) grade = models.CharField(max_length=254, blank=True, null=True) activities = models.CharField(max_length=254, blank=True, null=True) description = models.TextField(blank=True, null=True) @@ -224,40 +177,27 @@ def get_image(self): image_path = get_static_file_path("icons/school.png") return image_as_base64(image_path) - def get_end_date(self): - if self.currently_studying: - return _('Present') - elif self.end_date: - return self.end_date.strftime("%B %Y") - return _('Not Specified') - def get_duration(self): - if self.end_date is None and not self.currently_studying: - raise ValueError(_("End date is required to calculate duration in days. Please provide end date or mark as currently studying.")) - if self.currently_studying and self.end_date is not None: - raise ValueError(_("End date is not required when marked as currently studying. Please remove end date or mark as not currently studying.")) + # def get_duration(self): + # if self.end_date is None and not self.current: + # raise ValueError(_("End date is required to calculate duration in days. Please provide end date or mark as present.")) + # if self.current and self.end_date is not None: + # raise ValueError(_("End date is not required when marked as present. Please remove end date or mark as not present.")) + + # end_date = None + # if self.end_date is not None: + # end_date = self.end_date.strftime("%b %Y") + # if self.current: + # end_date = _("Present") + # start_date = self.start_date.strftime("%b %Y") + # return f"{start_date} - {end_date}" - end_date = None - if self.end_date is not None: - end_date = self.end_date.strftime("%b %Y") - if self.currently_studying: - end_date = _("Present") - start_date = self.start_date.strftime("%b %Y") - return f"{start_date} - {end_date}" @autoSlugFromUUID() -class EducationMedia(models.Model): +class EducationMedia(ModelMediaMixin): education = models.ForeignKey(Education, on_delete=models.CASCADE, related_name="education_media") - title = models.CharField(max_length=150) - slug = models.SlugField(max_length=255, unique=True, blank=True) file = models.FileField(upload_to=get_education_media_path) - description = models.TextField(blank=True, null=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - # custom model manager - objects = CustomModelManager() class Meta: db_table = 'education_media' @@ -269,19 +209,13 @@ class Meta: def __str__(self): return self.education.__str__() - def get_file(self): - if self.file: - file_path = settings.MEDIA_ROOT + self.file.url.lstrip("/media/") - return image_as_base64(file_path) - return - """ *************** Certification *************** """ @autoSlugWithFieldAndUUID(fieldname="title") class Certification(models.Model): - title = models.CharField(max_length=150) + title = models.CharField(max_length=150, unique=True) slug = models.SlugField(max_length=255, unique=True, blank=True) organization = models.CharField(max_length=150) address = models.CharField(max_length=254, blank=True, null=True) @@ -315,26 +249,21 @@ def get_image(self): image_path = get_static_file_path("icons/certificate.png") return image_as_base64(image_path) + def get_issue_date(self): + return self.issue_date.strftime("%-d %B, %Y") + def get_expiration_date(self): if self.does_not_expire: return _('Does not expire') elif self.expiration_date: - return self.expiration_date.strftime("%B %Y") + return self.expiration_date.strftime("%-d %B, %Y") return _('Not Specified') @autoSlugFromUUID() -class CertificationMedia(models.Model): +class CertificationMedia(ModelMediaMixin): certification = models.ForeignKey(Certification, on_delete=models.CASCADE, related_name="certification_media") - title = models.CharField(max_length=150) - slug = models.SlugField(max_length=255, unique=True, blank=True) file = models.FileField(upload_to=get_certification_media_path) - description = models.TextField(blank=True, null=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - # custom model manager - objects = CustomModelManager() class Meta: db_table = 'certification_media' @@ -346,8 +275,86 @@ class Meta: def __str__(self): return self.certification.__str__() - def get_file(self): - if self.file: - file_path = settings.MEDIA_ROOT + self.file.url.lstrip("/media/") - return image_as_base64(file_path) - return + +""" *************** Project *************** """ + + +@autoSlugWithFieldAndUUID(fieldname="title") +class Project(models.Model, DurationMixin): + title = models.CharField(max_length=200, unique=True) + slug = models.SlugField(max_length=255, unique=True, blank=True) + image = models.ImageField(upload_to=get_project_image_path, blank=True, null=True) + short_description = models.CharField(max_length=254) + technology = models.TextField(blank=True, null=True) + start_date = models.DateField() + end_date = models.DateField(blank=True, null=True) + present = models.BooleanField(default=False) + preview_url = models.URLField(blank=True, null=True) + github_url = models.URLField(blank=True, null=True) + description = RichTextField(blank=True, null=True) + order = models.PositiveIntegerField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'project' + verbose_name = _('Project') + verbose_name_plural = _('Projects') + ordering = ['order'] + get_latest_by = "created_at" + + def __str__(self): + return self.title + + def get_image(self): + if self.image: + image_path = settings.MEDIA_ROOT + self.image.url.lstrip("/media/") + else: + image_path = get_static_file_path("icons/project.png") + return image_as_base64(image_path) + + +@autoSlugFromUUID() +class ProjectMedia(ModelMediaMixin): + project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="project_media") + file = models.FileField(upload_to=get_project_media_path) + + class Meta: + db_table = 'project_media' + verbose_name = _('Project Media') + verbose_name_plural = _('Project Media') + get_latest_by = "created_at" + order_with_respect_to = 'project' + + def __str__(self): + return self.project.__str__() + + +# Signals + +@receiver(pre_save, sender=Project) +def generate_order(sender, instance, **kwargs): + """ + This method will generate order for new instances only. + Order will be generated automatically like 1, 2, 3, 4 and so on. + If any order is deleted then it will be reused. Like if 3 is deleted then next created order will be 3 instead of 5. + """ + if not instance.pk: # Only generate order for new instances + if instance.order is None: + deleted_orders = Project.objects.filter(order__isnull=False).values_list('order', flat=True) + max_order = Project.objects.aggregate(Max('order')).get('order__max') + + if deleted_orders: + deleted_orders = sorted(deleted_orders) + reused_order = None + for i in range(1, max_order + 2): + if i not in deleted_orders: + reused_order = i + break + if reused_order is not None: + instance.order = reused_order + else: + instance.order = max_order + 1 if max_order is not None else 1 diff --git a/project/public/staticfiles/icons/project.png b/project/public/staticfiles/icons/project.png new file mode 100644 index 0000000000000000000000000000000000000000..382fbe334ae613ed8e1e794b91dedcc2791631c5 GIT binary patch literal 3641 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 LNkvXXu0mjf;V 0: + duration_str += f"{years} Year{'s' if years > 1 else ''}, " + if months > 0: + duration_str += f"{months} Month{'s' if months > 1 else ''}" + # if days > 0: + # duration_str += f"{days} Day{'s' if days > 1 else ''}" + + if years < 1 and months < 1: + duration_str = f"{days} Day{'s' if days > 1 else ''}" + + return duration_str From ce503417efb367fd5ee2bd4df37f462f0b3f0a03 Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Fri, 23 Jun 2023 02:22:41 +0600 Subject: [PATCH 16/45] Unnecessary comment removed --- project/portfolios/models.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/project/portfolios/models.py b/project/portfolios/models.py index 6b45d10..c0a23ea 100644 --- a/project/portfolios/models.py +++ b/project/portfolios/models.py @@ -178,22 +178,6 @@ def get_image(self): return image_as_base64(image_path) - # def get_duration(self): - # if self.end_date is None and not self.current: - # raise ValueError(_("End date is required to calculate duration in days. Please provide end date or mark as present.")) - # if self.current and self.end_date is not None: - # raise ValueError(_("End date is not required when marked as present. Please remove end date or mark as not present.")) - - # end_date = None - # if self.end_date is not None: - # end_date = self.end_date.strftime("%b %Y") - # if self.current: - # end_date = _("Present") - # start_date = self.start_date.strftime("%b %Y") - # return f"{start_date} - {end_date}" - - - @autoSlugFromUUID() class EducationMedia(ModelMediaMixin): education = models.ForeignKey(Education, on_delete=models.CASCADE, related_name="education_media") From c5742f8eb133882c28a39b96d5ce85793a2cc1aa Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Fri, 23 Jun 2023 03:58:54 +0600 Subject: [PATCH 17/45] autoSlugFromUUID method updated --- project/utils/snippets.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/project/utils/snippets.py b/project/utils/snippets.py index b44a1f8..ae7eb58 100644 --- a/project/utils/snippets.py +++ b/project/utils/snippets.py @@ -2,15 +2,9 @@ import string import time from django.utils.text import slugify -from urllib.parse import urlparse from django.db import models from django.dispatch import receiver import uuid - -# PDF imports -from io import BytesIO -from django.http import HttpResponse -from django.template.loader import get_template import os import base64 from django.contrib.staticfiles import finders @@ -240,8 +234,6 @@ def generate_slug(sender, instance, *args, raw=False, **kwargs): def autoSlugFromUUID(): - """[Generates auto slug using UUID]""" - def decorator(model): assert hasattr(model, "slug"), "Model is missing a slug field" @@ -249,7 +241,17 @@ def decorator(model): def generate_slug(sender, instance, *args, raw=False, **kwargs): if not raw and not instance.slug: try: - instance.slug = str(uuid.uuid4()) + slug = str(uuid.uuid4()) + Klass = instance.__class__ + qs_exists = Klass.objects.filter(slug=slug).exists() + if qs_exists: + new_slug = "{slug}-{randstr}".format( + slug=slug, + randstr=random_string_generator(size=4) + ) + instance.slug = new_slug + else: + instance.slug = slug except Exception as e: instance.slug = simple_random_string() From 14b09cfdb2b83e35ea238a50f1f73599604abefd Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Sat, 24 Jun 2023 13:38:53 +0600 Subject: [PATCH 18/45] Project Technology section map added --- frontend/components/ProjectSection.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/frontend/components/ProjectSection.tsx b/frontend/components/ProjectSection.tsx index 1175d30..d3f18ac 100644 --- a/frontend/components/ProjectSection.tsx +++ b/frontend/components/ProjectSection.tsx @@ -67,11 +67,19 @@ export default function ProjectSection({ projects }: { projects: ProjectType[] } >
)} + {project.technology && (
- - {project.technology} - + {project.technology.split(',').map((technology, index) => { + return ( + + {technology} + + ) + })}
)} From aacbce4935d00510682dcc6e3d40acc027a02486 Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Sat, 24 Jun 2023 14:53:01 +0600 Subject: [PATCH 19/45] Project Details Initial Phase Added --- frontend/components/ProjectSection.tsx | 12 +- frontend/components/Projects_DELETE.tsx | 5 +- frontend/lib/backendAPI.ts | 23 ++- frontend/pages/projects/[slug].tsx | 145 ++++++++++++++++++ .../{projects.tsx => projects/index.tsx} | 0 project/portfolios/api/projects/views.py | 5 +- 6 files changed, 181 insertions(+), 9 deletions(-) create mode 100644 frontend/pages/projects/[slug].tsx rename frontend/pages/{projects.tsx => projects/index.tsx} (100%) diff --git a/frontend/components/ProjectSection.tsx b/frontend/components/ProjectSection.tsx index d3f18ac..11d9010 100644 --- a/frontend/components/ProjectSection.tsx +++ b/frontend/components/ProjectSection.tsx @@ -45,9 +45,15 @@ export default function ProjectSection({ projects }: { projects: ProjectType[] }
-

- {project.title} -

+ +

+ {project.title} +

+

{project.short_description} diff --git a/frontend/components/Projects_DELETE.tsx b/frontend/components/Projects_DELETE.tsx index 59dfa62..6ae0e57 100644 --- a/frontend/components/Projects_DELETE.tsx +++ b/frontend/components/Projects_DELETE.tsx @@ -52,8 +52,9 @@ export default function ProjectSection() { className="grid grid-cols-1 gap-4 mx-auto md:ml-[20%] xl:ml-[24%]" > {projects.map((project: ProjectType) => { - if (project.name === "" && project.githubURL === "") return null; - return ; + if (project.title === "" && project.github_url === "") return null; + // return ; + return null })}

diff --git a/frontend/lib/backendAPI.ts b/frontend/lib/backendAPI.ts index 1d7690e..129d1ce 100644 --- a/frontend/lib/backendAPI.ts +++ b/frontend/lib/backendAPI.ts @@ -156,12 +156,12 @@ export const getAllCertificates = async () => { // *** PROJECTS *** -// Certificates URL +// Projects URL const PROJECTS_PATH = "projects/" const PROJECTS_ENDPOINT = "http://127.0.0.1:8000/api/" + PROJECTS_PATH /** - * Makes a request to the BACKEND API to retrieve all Certificates Data. + * Makes a request to the BACKEND API to retrieve all Projects Data. */ export const getAllProjects = async () => { const allProjects = await fetch( @@ -182,6 +182,25 @@ export const getAllProjects = async () => { } } +export const getProjectDetails = async (slug: string) => { + const projectDetails = await fetch( + PROJECTS_ENDPOINT + slug, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } + ) + + if (projectDetails.ok) { + const responseData = await projectDetails.json() + return responseData.data + } else { + const errorMessage = `Error fetching Project Details: ${projectDetails.status} ${projectDetails.statusText}` + console.log(errorMessage) + } +} + // *** MOVIES *** // Experience URL diff --git a/frontend/pages/projects/[slug].tsx b/frontend/pages/projects/[slug].tsx new file mode 100644 index 0000000..c56ef5a --- /dev/null +++ b/frontend/pages/projects/[slug].tsx @@ -0,0 +1,145 @@ +import { BsGithub } from 'react-icons/bs' +import { MdOutlineLink } from 'react-icons/md' +import Link from 'next/link' +import OgImage from '@components/OgImage' +import { ProjectType } from '@lib/types' +import { motion } from 'framer-motion' +import { popUp } from '../../content/FramerMotionVariants' +import { FadeContainer } from '../../content/FramerMotionVariants' +import { HomeHeading } from '..' +import React from 'react' +import { useEffect, useState } from 'react' +import { getProjectDetails } from '@lib/backendAPI' +import { useRouter } from 'next/router' + +export default function ProjectDetailsSection() { + const router = useRouter(); + const { slug } = router.query; // Retrieve the slug parameter from the URL + + const [project, setProject] = useState() + + const fetchProjectDetails = async (slug: string) => { + try { + const projectData: ProjectType = await getProjectDetails(slug); + setProject(projectData); + } catch (error) { + // Handle error case + console.error(error); + } + }; + + // Add this useEffect to trigger the API request when slug is available + useEffect(() => { + if (typeof slug === 'string') { + fetchProjectDetails(slug); + } + }, [slug]); + + return ( + <> + {project && ( +
+ +
+ + + +
+ +
+ + +
+ +

{project.title}

+ + +

{project.short_description}

+ +

+ + {project.duration} + ({project.duration_in_days}) + +

+ + {project.description && ( +
+ )} + + {project.technology && ( +
+ {project.technology.split(',').map((technology, index) => { + return ( + + {technology} + + ) + })} +
+ )} + +
+ {project.github_url && ( + + + + )} + + {project.preview_url && ( + + + + )} +
+
+
+
+
+
+
+
+
+ )} + + ) +} diff --git a/frontend/pages/projects.tsx b/frontend/pages/projects/index.tsx similarity index 100% rename from frontend/pages/projects.tsx rename to frontend/pages/projects/index.tsx diff --git a/project/portfolios/api/projects/views.py b/project/portfolios/api/projects/views.py index f34d418..2b7aa7f 100644 --- a/project/portfolios/api/projects/views.py +++ b/project/portfolios/api/projects/views.py @@ -1,13 +1,14 @@ from rest_framework import permissions from rest_framework.viewsets import GenericViewSet -from rest_framework.mixins import ListModelMixin +from rest_framework.mixins import ListModelMixin, RetrieveModelMixin from utils.helpers import custom_response_wrapper from portfolios.models import Project from portfolios.api.projects.serializers import ProjectSerializer @custom_response_wrapper -class ProjectViewset(GenericViewSet, ListModelMixin): +class ProjectViewset(GenericViewSet, ListModelMixin, RetrieveModelMixin): permission_classes = (permissions.IsAuthenticated,) queryset = Project.objects.all() serializer_class = ProjectSerializer + lookup_field = 'slug' From 264b42f5933a3fb219ceb7890f8ae055d1303263 Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Mon, 26 Jun 2023 04:05:54 +0600 Subject: [PATCH 20/45] Project Details Page Added #35 #36 --- frontend/components/Projects_DELETE.tsx | 64 ------ frontend/pages/blank.tsx | 44 +++++ frontend/pages/projects/[slug].tsx | 249 +++++++++++++++--------- frontend/public/sitemap.xml | 8 +- project/utils/mixins.py | 4 +- project/utils/snippets.py | 21 ++ 6 files changed, 227 insertions(+), 163 deletions(-) delete mode 100644 frontend/components/Projects_DELETE.tsx create mode 100644 frontend/pages/blank.tsx diff --git a/frontend/components/Projects_DELETE.tsx b/frontend/components/Projects_DELETE.tsx deleted file mode 100644 index 6ae0e57..0000000 --- a/frontend/components/Projects_DELETE.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { FadeContainer } from "../content/FramerMotionVariants" -import { HomeHeading } from "../pages" -import { motion } from "framer-motion" -import React from "react" -import { useEffect, useState } from 'react' -import { getAllProjects } from "@lib/backendAPI" -import AnimatedDiv from "@components/FramerMotion/AnimatedDiv" -import Project from "@components/ProjectSection" -import { ProjectType } from "@lib/types" - - -export default function ProjectSection() { - const [projects, setProjects] = useState([]) - - useEffect(() => { - fetchProjects() - }, []) - - const fetchProjects = async () => { - const projectsData = await getAllProjects() - setProjects(projectsData) - } - - // ******* Loader Starts ******* - if (projects.length === 0) { - return
Loading...
- } - // ******* Loader Ends ******* - - return ( -
- - - -
-

- I've been making various types of projects some of them were basics - and some of them were complicated. So far I've made{" "} - - {projects.length}+ - {" "} - projects. -

- - {projects.map((project: ProjectType) => { - if (project.title === "" && project.github_url === "") return null; - // return ; - return null - })} - -
-
-
- ) -} diff --git a/frontend/pages/blank.tsx b/frontend/pages/blank.tsx new file mode 100644 index 0000000..7cbc5f2 --- /dev/null +++ b/frontend/pages/blank.tsx @@ -0,0 +1,44 @@ +import { motion } from 'framer-motion' +import { FadeContainer } from '../content/FramerMotionVariants' +import { HomeHeading } from '.' +import React from 'react' +import AnimatedDiv from '@components/FramerMotion/AnimatedDiv' +import { opacityVariant } from '@content/FramerMotionVariants' + +export default function ProjectDetailsSection() { + const item = 123; + + return ( + <> + {item && ( +
+ +
+ + + + + + {/* place content here */} + + + +
+
+
+ )} + + ) +} diff --git a/frontend/pages/projects/[slug].tsx b/frontend/pages/projects/[slug].tsx index c56ef5a..0af1a7e 100644 --- a/frontend/pages/projects/[slug].tsx +++ b/frontend/pages/projects/[slug].tsx @@ -1,97 +1,155 @@ import { BsGithub } from 'react-icons/bs' import { MdOutlineLink } from 'react-icons/md' import Link from 'next/link' -import OgImage from '@components/OgImage' -import { ProjectType } from '@lib/types' +import { ProjectType, MediaType } from '@lib/types' import { motion } from 'framer-motion' -import { popUp } from '../../content/FramerMotionVariants' import { FadeContainer } from '../../content/FramerMotionVariants' import { HomeHeading } from '..' import React from 'react' import { useEffect, useState } from 'react' import { getProjectDetails } from '@lib/backendAPI' import { useRouter } from 'next/router' +import AnimatedDiv from '@components/FramerMotion/AnimatedDiv' +import { opacityVariant } from '@content/FramerMotionVariants' +import Image from 'next/image' +import PDFViewer from '@components/PDFViewer' + export default function ProjectDetailsSection() { - const router = useRouter(); - const { slug } = router.query; // Retrieve the slug parameter from the URL + const router = useRouter() + const { slug } = router.query // Retrieve the slug parameter from the URL const [project, setProject] = useState() const fetchProjectDetails = async (slug: string) => { try { - const projectData: ProjectType = await getProjectDetails(slug); - setProject(projectData); + const projectData: ProjectType = await getProjectDetails(slug) + setProject(projectData) } catch (error) { // Handle error case - console.error(error); + console.error(error) } - }; + } // Add this useEffect to trigger the API request when slug is available useEffect(() => { if (typeof slug === 'string') { - fetchProjectDetails(slug); + fetchProjectDetails(slug) } - }, [slug]); + }, [slug]) + + function getFileExtensionFromBase64(base64String: string): string { + const mimeType = base64String.match(/data:(.*?);/)?.[1] + const [, fileExtension] = mimeType?.split('/') ?? [] + return fileExtension || '' + } + + const isImageFile = (file: string): boolean => { + const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg'] + const fileExtension = getFileExtensionFromBase64(file) + return imageExtensions.includes(fileExtension.toLowerCase()) + } return ( <> - {project && ( -
- -
- - - -
- -
- - -
- -

{project.title}

- - -

{project.short_description}

+ {project && ( +
+ +
+ + + + + {/* project cover image */} +
+ {project.title} +
+
+
+ {/*

{project.title}

*/} +

{project.short_description}

- + {project.duration} - ({project.duration_in_days}) + ({project.duration_in_days})

+
+ + {/* project description */} + {project.description && ( +
+ )} + + {/* project media */} + {project.project_media?.length ? ( + // Here there will be a list of media. bullet points. There will be a button. After clicking the button new modal will open with the list of media. +
+

Attachments

+ {project.project_media.map((media: MediaType, mediaIndex) => ( +
+ + {/* serial number */} +

#{mediaIndex + 1}

+ + {/* media title */} +

{media.title}

- {project.description && ( -
- )} + {/* Image file */} + {isImageFile(media.file) ? ( + {media.title} + ) : null} - {project.technology && ( + {/* pdf file */} + {getFileExtensionFromBase64(media.file) === 'pdf' ? ( + + ): null} + + {/* media description */} +

{media.description}

+ +
+ ))} +
+ ) : null} + + {/* project technology */} + {project.technology && ( +
+

Technology

{project.technology.split(',').map((technology, index) => { return ( @@ -104,42 +162,47 @@ export default function ProjectDetailsSection() { ) })}
- )} - -
- {project.github_url && ( - - - - )} - - {project.preview_url && ( - - - - )}
-
+ )} + + {/* project links */} + {project.github_url || project.preview_url ? ( +
+

Links

+
+ {project.github_url && ( + + + + )} + + {project.preview_url && ( + + + + )} +
+
+ ) : null}
-
-
- -
-
-
- )} + + +
+ +
+ )} ) } diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml index b4c38b5..ce246a6 100644 --- a/frontend/public/sitemap.xml +++ b/frontend/public/sitemap.xml @@ -6,19 +6,19 @@ - https://j471n.in/certificates + https://j471n.in/blank - https://j471n.in + https://j471n.in/certificates - https://j471n.in/privacy + https://j471n.in - https://j471n.in/projects + https://j471n.in/privacy diff --git a/project/utils/mixins.py b/project/utils/mixins.py index dac29b5..2f89fe9 100644 --- a/project/utils/mixins.py +++ b/project/utils/mixins.py @@ -3,7 +3,7 @@ from django.utils.timezone import datetime from django.utils.translation import gettext_lazy as _ from utils.helpers import CustomModelManager -from utils.snippets import image_as_base64 +from utils.snippets import file_as_base64 """ @@ -50,7 +50,7 @@ class Meta: def get_file(self): if self.file: file_path = settings.MEDIA_ROOT + self.file.url.lstrip("/media/") - return image_as_base64(file_path) + return file_as_base64(file_path) return diff --git a/project/utils/snippets.py b/project/utils/snippets.py index ae7eb58..4018ef8 100644 --- a/project/utils/snippets.py +++ b/project/utils/snippets.py @@ -340,3 +340,24 @@ def image_as_base64(image_file): encoded_string = base64.b64encode(img_f.read()).decode("utf-8") return f"data:image/{extension};base64,{encoded_string}" + + +def file_as_base64(file_path): + """ + Convert a file to base64. + + :param file_path: The complete path of the file. + :return: The base64 representation of the file. + """ + if not os.path.isfile(file_path): + print(f"File not found: {file_path}") + return + + # Get the file extension dynamically + extension = os.path.splitext(file_path)[1][1:] + encoded_string = "" + + with open(file_path, "rb") as file: + encoded_string = base64.b64encode(file.read()).decode("utf-8") + + return f"data:application/{extension};base64,{encoded_string}" From 403434fa38339fa84ec1cbece3e162d426124f71 Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Mon, 26 Jun 2023 21:34:07 +0600 Subject: [PATCH 21/45] Movies and Interests Section added #35 #36 --- frontend/components/Interest.tsx | 66 ++++++++++++ frontend/components/MovieCard.tsx | 2 +- frontend/lib/backendAPI.ts | 68 ++++++++---- frontend/lib/types.ts | 13 +++ frontend/pages/about.tsx | 2 + project/config/api_router.py | 2 + project/portfolios/admin.py | 24 ++++- project/portfolios/api/interests/__init__.py | 0 project/portfolios/api/interests/routers.py | 5 + .../portfolios/api/interests/serializers.py | 14 +++ project/portfolios/api/interests/views.py | 13 +++ project/portfolios/api/movies/__init__.py | 0 project/portfolios/api/movies/routers.py | 5 + project/portfolios/api/movies/serializers.py | 14 +++ project/portfolios/api/movies/views.py | 13 +++ .../0024_interest_alter_project_options.py | 57 ++++++++++ .../migrations/0025_alter_interest_slug.py | 18 ++++ project/portfolios/migrations/0026_movie.py | 51 +++++++++ project/portfolios/models.py | 102 +++++++++++++++++- project/public/staticfiles/icons/interest.png | Bin 0 -> 4405 bytes project/public/staticfiles/icons/movie.png | Bin 0 -> 1612 bytes project/utils/image_upload_helpers.py | 16 +++ 22 files changed, 460 insertions(+), 25 deletions(-) create mode 100644 frontend/components/Interest.tsx create mode 100644 project/portfolios/api/interests/__init__.py create mode 100644 project/portfolios/api/interests/routers.py create mode 100644 project/portfolios/api/interests/serializers.py create mode 100644 project/portfolios/api/interests/views.py create mode 100644 project/portfolios/api/movies/__init__.py create mode 100644 project/portfolios/api/movies/routers.py create mode 100644 project/portfolios/api/movies/serializers.py create mode 100644 project/portfolios/api/movies/views.py create mode 100644 project/portfolios/migrations/0024_interest_alter_project_options.py create mode 100644 project/portfolios/migrations/0025_alter_interest_slug.py create mode 100644 project/portfolios/migrations/0026_movie.py create mode 100644 project/public/staticfiles/icons/interest.png create mode 100644 project/public/staticfiles/icons/movie.png diff --git a/frontend/components/Interest.tsx b/frontend/components/Interest.tsx new file mode 100644 index 0000000..8b8d375 --- /dev/null +++ b/frontend/components/Interest.tsx @@ -0,0 +1,66 @@ +import { FadeContainer, popUp } from '../content/FramerMotionVariants' +import { HomeHeading } from '../pages' +import { motion } from 'framer-motion' +import React from 'react' +import { useEffect, useState } from 'react' +import Image from 'next/image' +import { getAllInterests } from '@lib/backendAPI' +import { InterestType } from '@lib/types' + + +export default function InterestSection() { + const [interests, setInterests] = useState([]) + + useEffect(() => { + fetchInterests() + }, []) + + const fetchInterests = async () => { + const interestsData = await getAllInterests() + setInterests(interestsData) + } + + // ******* Loader Starts ******* + if (interests.length === 0) { + return
Loading...
+ } + // ******* Loader Ends ******* + + return ( +
+ + + +

Here are some of my interests.

+
+ {interests.map((interest: InterestType, index) => { + return ( + +
+
+ {interest.title} +
+ +

+ {interest.title} +

+
+
+ ) + })} +
+
+
+ ) +} diff --git a/frontend/components/MovieCard.tsx b/frontend/components/MovieCard.tsx index 7164dfc..ed542fe 100644 --- a/frontend/components/MovieCard.tsx +++ b/frontend/components/MovieCard.tsx @@ -8,7 +8,7 @@ import { AiFillStar } from "react-icons/ai"; export default function MovieCard({ movie }: { movie: MovieType }) { return ( - + { } } + +// *** INTERESTS *** + +// Interests URL +const INTERESTS_PATH = "interests/" +const INTERESTS_ENDPOINT = "http://127.0.0.1:8000/api/" + INTERESTS_PATH + +/** + * Makes a request to the BACKEND API to retrieve all Interests Data. + */ +export const getAllInterests = async () => { + const allInterests = await fetch( + INTERESTS_ENDPOINT, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } + ) + + if (allInterests.ok) { + const responseData = await allInterests.json() + return responseData.data + } else { + const errorMessage = `Error fetching Interests: ${allInterests.status} ${allInterests.statusText}` + console.log(errorMessage) + } +} + + // *** MOVIES *** -// Experience URL -const MOVIE_PATH = "/posts?_limit=5" -const MOVIE_ENDPOINT = BACKEND_API_BASE_URL + MOVIE_PATH +// Movies URL +const MOVIE_PATH = "movies/" +const MOVIE_ENDPOINT = "http://127.0.0.1:8000/api/" + MOVIE_PATH /** - * Makes a request to the BACKEND API to retrieve all Movie Data. + * Makes a request to the BACKEND API to retrieve all Movies Data. */ export const getAllMovies = async () => { - const allMovies = await fetch( - MOVIE_ENDPOINT + MOVIE_ENDPOINT, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } ) - .then((response) => response.json()) - .catch((error) => console.log('Error fetching Movies:', error)) - // ******* Faking data Starts ******* - const fakeMoviesData = allMovies.map((movie: { title: any, body: any }, index: number) => ({ - id: index, - url: "https://github.com/NumanIbnMazid", - name: movie.title.split(' ').slice(0, 3).join(' ').toUpperCase(), - image: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQYY0pvHu6oaaJRADcCoacoP5BKwJN0i1nqFNCnmKvN&s", - watched: false, - rating: 4 - })) - // Need to return `allMovies` - // ******* Faking data Ends ******* - return fakeMoviesData + if (allMovies.ok) { + const responseData = await allMovies.json() + return responseData.data + } else { + const errorMessage = `Error fetching Movies: ${allMovies.status} ${allMovies.statusText}` + console.log(errorMessage) + } } - // *** BLOGS *** // Blogs URL diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts index 99bf376..a852bcd 100644 --- a/frontend/lib/types.ts +++ b/frontend/lib/types.ts @@ -163,6 +163,16 @@ export type CertificateType = { updated_at: string } +export type InterestType = { + id: number + slug: string + title: string + icon: string + order: number + created_at: string + updated_at: string +} + export type SocialPlatform = { title: string Icon: IconType @@ -272,10 +282,13 @@ export type Snippet = { export type MovieType = { id: number + slug: string name: string image: string url: string year: number watched: boolean rating: number + created_at: string + updated_at: string } diff --git a/frontend/pages/about.tsx b/frontend/pages/about.tsx index 3be2e15..a50f573 100644 --- a/frontend/pages/about.tsx +++ b/frontend/pages/about.tsx @@ -13,6 +13,7 @@ import ExperienceSection from "@components/Home/ExperienceSection" import Education from "@components/Education" import Certificates from "@components/Certificates" import ProjectSection from "@components/ProjectSection" +import InterestSection from "@components/Interest" export default function About({ @@ -78,6 +79,7 @@ export default function About({ +
diff --git a/project/config/api_router.py b/project/config/api_router.py index 8719b0a..c77abb3 100644 --- a/project/config/api_router.py +++ b/project/config/api_router.py @@ -5,6 +5,8 @@ from portfolios.api.educations.routers import * from portfolios.api.certifications.routers import * from portfolios.api.projects.routers import * +from portfolios.api.interests.routers import * +from portfolios.api.movies.routers import * app_name = "api" diff --git a/project/portfolios/admin.py b/project/portfolios/admin.py index 60262da..52af6f1 100644 --- a/project/portfolios/admin.py +++ b/project/portfolios/admin.py @@ -2,7 +2,7 @@ from django.db import models from utils.mixins import CustomModelAdminMixin from portfolios.models import ( - ProfessionalExperience, Skill, Education, EducationMedia, Certification, CertificationMedia, Project, ProjectMedia + ProfessionalExperience, Skill, Education, EducationMedia, Certification, CertificationMedia, Project, ProjectMedia, Interest, Movie ) from ckeditor.widgets import CKEditorWidget @@ -81,3 +81,25 @@ class Meta: model = Project admin.site.register(Project, ProjectAdmin) + + +# ---------------------------------------------------- +# *** Interest *** +# ---------------------------------------------------- + +class InterestAdmin(CustomModelAdminMixin, admin.ModelAdmin): + class Meta: + model = Interest + +admin.site.register(Interest, InterestAdmin) + + +# ---------------------------------------------------- +# *** Movie *** +# ---------------------------------------------------- + +class MovieAdmin(CustomModelAdminMixin, admin.ModelAdmin): + class Meta: + model = Movie + +admin.site.register(Movie, MovieAdmin) diff --git a/project/portfolios/api/interests/__init__.py b/project/portfolios/api/interests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/portfolios/api/interests/routers.py b/project/portfolios/api/interests/routers.py new file mode 100644 index 0000000..c4f1c2b --- /dev/null +++ b/project/portfolios/api/interests/routers.py @@ -0,0 +1,5 @@ +from config.router import router +from portfolios.api.interests.views import InterestViewset + + +router.register("interests", InterestViewset, basename="interests") diff --git a/project/portfolios/api/interests/serializers.py b/project/portfolios/api/interests/serializers.py new file mode 100644 index 0000000..24e4bdc --- /dev/null +++ b/project/portfolios/api/interests/serializers.py @@ -0,0 +1,14 @@ +from rest_framework import serializers +from portfolios.models import Interest + + +class InterestSerializer(serializers.ModelSerializer): + icon = serializers.SerializerMethodField() + + class Meta: + model = Interest + fields = "__all__" + read_only_fields = ("id", "slug", "created_at", "updated_at") + + def get_icon(self, obj): + return obj.get_icon() diff --git a/project/portfolios/api/interests/views.py b/project/portfolios/api/interests/views.py new file mode 100644 index 0000000..eb10c4b --- /dev/null +++ b/project/portfolios/api/interests/views.py @@ -0,0 +1,13 @@ +from rest_framework import permissions +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin +from utils.helpers import custom_response_wrapper +from portfolios.models import Interest +from portfolios.api.interests.serializers import InterestSerializer + + +@custom_response_wrapper +class InterestViewset(GenericViewSet, ListModelMixin): + permission_classes = (permissions.IsAuthenticated,) + queryset = Interest.objects.all() + serializer_class = InterestSerializer diff --git a/project/portfolios/api/movies/__init__.py b/project/portfolios/api/movies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/portfolios/api/movies/routers.py b/project/portfolios/api/movies/routers.py new file mode 100644 index 0000000..15daedf --- /dev/null +++ b/project/portfolios/api/movies/routers.py @@ -0,0 +1,5 @@ +from config.router import router +from portfolios.api.movies.views import MovieViewset + + +router.register("movies", MovieViewset, basename="movies") diff --git a/project/portfolios/api/movies/serializers.py b/project/portfolios/api/movies/serializers.py new file mode 100644 index 0000000..1dbec6a --- /dev/null +++ b/project/portfolios/api/movies/serializers.py @@ -0,0 +1,14 @@ +from rest_framework import serializers +from portfolios.models import Movie + + +class MovieSerializer(serializers.ModelSerializer): + image = serializers.SerializerMethodField() + + class Meta: + model = Movie + fields = "__all__" + read_only_fields = ("id", "slug", "created_at", "updated_at") + + def get_image(self, obj): + return obj.get_image() diff --git a/project/portfolios/api/movies/views.py b/project/portfolios/api/movies/views.py new file mode 100644 index 0000000..48d1f62 --- /dev/null +++ b/project/portfolios/api/movies/views.py @@ -0,0 +1,13 @@ +from rest_framework import permissions +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin +from utils.helpers import custom_response_wrapper +from portfolios.models import Movie +from portfolios.api.movies.serializers import MovieSerializer + + +@custom_response_wrapper +class MovieViewset(GenericViewSet, ListModelMixin): + permission_classes = (permissions.IsAuthenticated,) + queryset = Movie.objects.all() + serializer_class = MovieSerializer diff --git a/project/portfolios/migrations/0024_interest_alter_project_options.py b/project/portfolios/migrations/0024_interest_alter_project_options.py new file mode 100644 index 0000000..95d7e41 --- /dev/null +++ b/project/portfolios/migrations/0024_interest_alter_project_options.py @@ -0,0 +1,57 @@ +# Generated by Django 4.2.1 on 2023-06-26 14:15 + +from django.db import migrations, models +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0023_alter_professionalexperience_options_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="Interest", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=200)), + ("slug", models.SlugField(max_length=255, unique=True)), + ( + "icon", + models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_interest_image_path, + ), + ), + ("order", models.PositiveIntegerField(blank=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Interest", + "verbose_name_plural": "Interests", + "db_table": "interest", + "ordering": ("order", "-created_at"), + "get_latest_by": "created_at", + }, + ), + migrations.AlterModelOptions( + name="project", + options={ + "get_latest_by": "created_at", + "ordering": ("order", "-created_at"), + "verbose_name": "Project", + "verbose_name_plural": "Projects", + }, + ), + ] diff --git a/project/portfolios/migrations/0025_alter_interest_slug.py b/project/portfolios/migrations/0025_alter_interest_slug.py new file mode 100644 index 0000000..a814ceb --- /dev/null +++ b/project/portfolios/migrations/0025_alter_interest_slug.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2023-06-26 14:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0024_interest_alter_project_options"), + ] + + operations = [ + migrations.AlterField( + model_name="interest", + name="slug", + field=models.SlugField(blank=True, max_length=255, unique=True), + ), + ] diff --git a/project/portfolios/migrations/0026_movie.py b/project/portfolios/migrations/0026_movie.py new file mode 100644 index 0000000..2f9cc1c --- /dev/null +++ b/project/portfolios/migrations/0026_movie.py @@ -0,0 +1,51 @@ +# Generated by Django 4.2.1 on 2023-06-26 15:20 + +from django.db import migrations, models +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0025_alter_interest_slug"), + ] + + operations = [ + migrations.CreateModel( + name="Movie", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=200)), + ("slug", models.SlugField(blank=True, max_length=255, unique=True)), + ( + "image", + models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_movie_image_path, + ), + ), + ("url", models.URLField(blank=True, null=True)), + ("year", models.PositiveIntegerField(blank=True, null=True)), + ("watched", models.BooleanField(default=True)), + ("rating", models.PositiveIntegerField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Movie", + "verbose_name_plural": "Movies", + "db_table": "movie", + "ordering": ("-updated_at",), + "get_latest_by": "created_at", + }, + ), + ] diff --git a/project/portfolios/models.py b/project/portfolios/models.py index c0a23ea..7078c0a 100644 --- a/project/portfolios/models.py +++ b/project/portfolios/models.py @@ -11,7 +11,8 @@ from utils.snippets import autoSlugWithFieldAndUUID, autoSlugFromUUID, image_as_base64, get_static_file_path from utils.image_upload_helpers import ( get_professional_experience_company_image_path, get_skill_image_path, get_education_school_image_path, get_education_media_path, - get_certification_image_path, get_certification_media_path, get_project_image_path, get_project_media_path + get_certification_image_path, get_certification_media_path, get_project_image_path, get_project_media_path, get_interest_image_path, + get_movie_image_path ) from ckeditor.fields import RichTextField @@ -287,7 +288,7 @@ class Meta: db_table = 'project' verbose_name = _('Project') verbose_name_plural = _('Projects') - ordering = ['order'] + ordering = ('order', '-created_at') get_latest_by = "created_at" def __str__(self): @@ -342,3 +343,100 @@ def generate_order(sender, instance, **kwargs): instance.order = reused_order else: instance.order = max_order + 1 if max_order is not None else 1 + + +""" *************** Interest *************** """ + + +@autoSlugWithFieldAndUUID(fieldname="title") +class Interest(models.Model): + title = models.CharField(max_length=200) + slug = models.SlugField(max_length=255, unique=True, blank=True) + icon = models.ImageField(upload_to=get_interest_image_path, blank=True, null=True) + order = models.PositiveIntegerField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'interest' + verbose_name = _('Interest') + verbose_name_plural = _('Interests') + ordering = ('order', '-created_at') + get_latest_by = "created_at" + + def __str__(self): + return self.title + + def get_icon(self): + if self.icon: + icon_path = settings.MEDIA_ROOT + self.icon.url.lstrip("/media/") + else: + icon_path = get_static_file_path("icons/interest.png") + return image_as_base64(icon_path) + + +# Signals + +@receiver(pre_save, sender=Interest) +def generate_order(sender, instance, **kwargs): + """ + This method will generate order for new instances only. + Order will be generated automatically like 1, 2, 3, 4 and so on. + If any order is deleted then it will be reused. Like if 3 is deleted then next created order will be 3 instead of 5. + """ + if not instance.pk: # Only generate order for new instances + if instance.order is None: + deleted_orders = Interest.objects.filter(order__isnull=False).values_list('order', flat=True) + max_order = Interest.objects.aggregate(Max('order')).get('order__max') + + if deleted_orders: + deleted_orders = sorted(deleted_orders) + reused_order = None + for i in range(1, max_order + 2): + if i not in deleted_orders: + reused_order = i + break + if reused_order is not None: + instance.order = reused_order + else: + instance.order = max_order + 1 if max_order is not None else 1 + + + +""" *************** Movie *************** """ + + +@autoSlugWithFieldAndUUID(fieldname="name") +class Movie(models.Model): + name = models.CharField(max_length=200) + slug = models.SlugField(max_length=255, unique=True, blank=True) + image = models.ImageField(upload_to=get_movie_image_path, blank=True, null=True) + url = models.URLField(blank=True, null=True) + year = models.PositiveIntegerField(blank=True, null=True) + watched = models.BooleanField(default=True) + rating = models.PositiveIntegerField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'movie' + verbose_name = _('Movie') + verbose_name_plural = _('Movies') + ordering = ('-updated_at',) + get_latest_by = "created_at" + + def __str__(self): + return self.name + + def get_image(self): + if self.image: + image_path = settings.MEDIA_ROOT + self.image.url.lstrip("/media/") + else: + image_path = get_static_file_path("icons/movie.png") + return image_as_base64(image_path) diff --git a/project/public/staticfiles/icons/interest.png b/project/public/staticfiles/icons/interest.png new file mode 100644 index 0000000000000000000000000000000000000000..20c02af9d8831efde189d5a8ce40439ee6c85e5d GIT binary patch literal 4405 zcmV-55z6j~P)+b_-uGS=QjpkV(p{PU&-u@(dhgYJ_y6v{-&-2ZpMjz@QGesXOYaICi4l0RTjWW( zz>&`cPFgDR`bip%W`O3WN&+vr37pg@@&x*3sUDxx{FKOTXy88yJjqvt44xDXJ7af3 z?cQT>v9Jy z%b*}K4_;hr254#msH=k;`zpXNDwu)5la(54CBcRa^x(-0FMEIZvho5GzN5Xz%7?rg zMFmZpzZN>LHUnI|NI_`GF9AGlaZmZ)p)<^ASzRKQvj~+DOVig z%|D9-F26SZqhLd(h`bam@+4B^qy&MNj2 zG?ko$!x_c!wDueY0NKwmh=K5i;a`6=!3ADFRN%=TfhSMj=h5ICvS+$`L9>9@U1;O<>0ODVLdQ9S^tuU7%kk7VJ{ZJ?h`Uvr=L zTm65CJeekPdLMx&pSauTK5*p&e)+wH_tGx<7lD^9;fq2hN5h@-a=gBE3z`p|0{!Sv zNS&9)!jv+}D_7_Q8@H7Lkz=;VkrK@H+?z!b9knxD>_qAOSm=UUmk$lI~jF2z9YINw4^*m{}{Jw(vM~ zV|2NBvj>QsXU@XOwA~OkQO+~8`fo)}|EVPaPT~Ux*6mkU>H6VX(2Za#nrJIQtGypw zu+xELNfWMKRab0-oVZ{DWH#YdG|8C|X#%+QHcfpWU1;j*Uu3iDD6fQ|v5S>@V1r4c zBv%w3opWK*Ug+-VfX7!`pmgA;_@T~PN6aM&lRqaz84bV1J2hB17eS#p{?48mhOba;a8^s~Yk>#jT z57g{Arka=~;V*0{R_g$fBd3&#(EUyQe~Vi31h0Rd$rh_-*AX?0T4LjvFsRQjrQ>3? z$-*gVR~@9t6CGa6GHhVNSJ$g6zh?Xr==^;+v`q?t0Ih^Sgr>egFpJwUzl%{OFwNXy z2|OuNZGJ{P{BcbYbX>RsaN`DzDhhz}>kkm2lIXhwA#ilDpp46OV(K?$jeic00-1sv?wnI`ZXE(`A*2~PrY z;Ld_92pYsD6e*iRo4p^Lv(xqT2~BMEkhce9DK80q$=U{poa8O?1 zC;%!5yu?6oPo}np0F=k)D?+4Na=JcFK7WUCTHwfed~S0HE0o&nzoJ5UaMBB`BF%r} z&P#sy?AnDH^x3q98T1{?rY?eoLzot< zGz&GL6u(?Na0*_uw?n!g3p)!38KH4r1{BU%38~Ie@KCn_x@U$%*VMj2NST{NUvM~a zhoS*l7Q>Dr6lbtQ&x~q(uA~~ahUQodd9tZ)!+S?_4#BsrcL6m4zgI-WLWV1ImXXdT z0D?xbImJ%(fK@Zy=x6`8Lg@U8qqi#WS&JI51HT;Kc9hZaR45kSBwGv59l__C4%b0T zO+6i7@8l2F%a%cre>D3(+yuaUJEj3^{xBEPKXnC9d5({coP^qJmWc{H*^Tvj2vv_r zQ=-z zoOBNFqwq~%oTbl(j18m!l!Y><>pPSIP$Efa*)7;WtDI-XTS2U z18Tv7O-hWZO1|E8t+`(ix@LyMW+#J5_?_ic_$Mc7V8PG;x{_F1Xubz|a{g~(%8EjI z+4;R;=^4dT&zIKFw3;Sv4h9N9fo~-J9yrRR0Skw)g-M#}4r#OJuxMbbht8VQss@N0 zDWWSNa-<0_U`9m&xZgqJa$g{P8<+!|Cb0?)i>CUbN`SLxDF7ugxk~bKNb?#=k{76hS+2Nj=BGh(+v!O_Iq?r*gsJFh*|<6Zxe{0EWz^km_Jan z&grr6O=T5)U0DUeIfrK!kEGf@q()6-Xf5j{1InG=?4v8(*erOU7-U_+0rP+_b#vBUhl<}^I3 zIt8&G#45Ib2DM&e@rAr(OFgnYNXUC^3+r^j5op|h0xp-HgrnK}VQ=hq+Q6g}ksaW7 z0U&5~GpIM`q-QTrR^X*?u~u-`>ZYpCbH@Kt?(c}%4#g?^pg6e@iW7HJ#*+F$UA+oK zb#6JmUXrv2YIjt?(}v3c=gvX%-h&YF9*YzrPi|wUs$aEp(YXAKIY#Nm%M&$c^aJaB z(xLs}@qPjD80h;O^H`6$IB^aIqgPDZN2rm>~ZJ#EOpi*JbkplD=Yj3IaD zl03L^^bB-kt4`L8u7*Y$@7MdJs@frN(&bm$V2Q$QY)%>{aAbwZNjn9O920q|O5~(M ztoH?;Jf@*=us`e#+elkuF9RTQ1_xztz}qnZmXlSmF>4DXC#FMu%xcOp;fWBsFb2p6 zVc;oOf(SmYl!u+36F4aX+wr}b)iDa=zGLXSkzg1dLSw#}c6DKA%I8g=Uua$@c?|UZ zjhv%*)72QpIy~55Q5uy3Pg?MraoQ5tynZX>Y|Vwl_!L!mh2xeaL6~6~1TTmI?@{VF zm%eK+0R0x*eX^jfG{2D&`)j7Y7NY%sj+`<>Jj%S!Xq*}jxjA`|mzNK#SElt6Ug6k? zl@O+ngHT;8%pYm&x1zK}Xf|%P`oX9*=x8-RCPksS*ytvE*pLtLGmR)b>7GB#NLL<( z&&t@)r|=3#2E|hlg1wi5haFYp=a?#MtaVc~&M%1yj~qTP4!lPMKy+Y2p8!x((8p2M zO%3g#QC3Bwmb9Q}- zV0HBu&oR{x$l<P;o#ApTp^5G4=0ob3c0uVnnpjV~nZbKY3)#KwBhRsN zJiTtMnHgmDTzy&n4=-XTMcL6D=_Xc&?gMpiYpj_>PST@L^9C87srrUA-nQRJv8p->W)1ZUGzp)Nieilq=(JWf5#&~0H)@h5Rs zIP_(|(9g*;$$Vqfs>$a~pI_h}fw_>9lMyyAjD@6+m%yrz7Q@zgQQ$MUXHO6RKk!}W z5dY#y#FiE(RiZ#{4x#W~R28@n3!wtmWu-HRcx($)#{hZqN1>M4tT7 zsx?96$Z8a1u2VRS+0q6hi(bGKiEY7$j1Pv+6Z!D;XdW2k=Ep?uFTf$H1K#$p2e(1W zsShjl2VQtgmLo4jTLU;JH&Nk76g&sV)Q2$ptvD)NX><}41jN(NF5_2%W9lPth`moi zaGrkxL|#wBA$Er~)g)a>fxP;271pi z=^+IG`J)N)X%S9ZsK|XWv@$^&^069;43)%-e#6M8Y;FFQeUtqUaV=d%iLq5bw&zhNO0$|H40FdxM zDgZ8HR(5 vm7%~%qXdo=;9ukaFN}Y<#v}bd)I|Rmpk;wC?D3MN00000NkvXXu0mjf+QvMG literal 0 HcmV?d00001 diff --git a/project/public/staticfiles/icons/movie.png b/project/public/staticfiles/icons/movie.png new file mode 100644 index 0000000000000000000000000000000000000000..7e79ed72af6fc55954d176411f30108473c82530 GIT binary patch literal 1612 zcmYjSeLT~79RF=dnWyOFSrIvHak;QYi6wE$Nv=_ohqaU$L+sWA<>5qS+AU=*jvkO` zv1oB!T+BpE9w!UUbRNbqS>k@b?(X%vKfb@u>-YV>Ki~KJ^ZUGCpI?^${=Hfn#u@+s zXzlaH27vJxv{tVIpTqe{E?`&_>y3*C0E9lYV8Bh5Apoc?@5Am6OlD28`pAq!hA+hk zza}ihR}C9He!Tek#Z)`< zzc6Dl5SqqoW)_a7>K!o-xis|X&U4aV(>0i0bc~x<6mMz9+VJNp)!1RBS&sc5Da^h3 zy4Y8q!q=TvzSy-XY-CzHGC_9tg@is;NamBrA3Pi_^4 z>(AOVe#{xl3Eyy>s+vr8z|Yg#3L5IIr8|bq>@tQ?CWeoadKWydR;V$?)d|#>{pQjY7$JjqgOlM8|5TXS>pVjD^fymVqd zy|g?va!$!ahMm-)oefjkx!0B8nZ#!e*1q}d7a)2@yKumq4qeIDJ6_c;vg8COy*jeS zw%n5A>dAEMtge47Ud6!t^CqgUj}HP`Z|=mgHwtb<^bfW@0VI3RLda`T{eu&d6wdwK zOkxX#<0mTkY?GT4iLSdlsfAR@qa!yT>mOWl1_(3eP%__{BwOl%5sp?OJ8~s$&Lr|s zfICiXC2)*Hr{A4CTC3{sG}j{6#I<{LDz@S^ZFjlZLuc83B3{DU!lj94P#$tSNyg~` zB$=NhsToM3ktm?_5+TW+b_7Vsjeqh&Cbrok~yoJSQ9 z;Ug`YA2ka-&6nrMBzEu=&ch%kks+ae;<@TH+sUhnEmKflHX z-BQ^8z8WN(|`G zg#g|*7=d^X>d8cS$ctN?;DzpXHOA#bkTnhpxTd9fT~Cu1Og9+uDW?m#NwRGBZF!Q3 z?IiL$L<%RW7)pHm2-WP=dh%V!E)#!68q}M6Y|=+9{>gVD2ZaAQlRSwLhqa1(r_vK_ z@s|#jR`1NOe8dnXCbDMAZQ8WZbSxJFsYDCj8{sNwI_&@hpPrn7lx`5|xmlGb%8H0E zLUR!00(28MjSI5Tl6OEh&GZwAT(rEsI4rMDg_E0#ZKDK|-r2-up{{j3eL}S>Tq-&W zS^H)m(?Ue3nIch*{g>Zvg-nTx7IcunlFe_!ZR<5=K-0!?B_0J2y6f+;;r}XFCzAV? zgFB=HaGy#K@aos2!W*|&`t3kfANeVi6Bof^tXYb&wvJ*GupBKn*d{BSM@#YJI2vX% zjPIQx{3t}x3^r|r^2d@Q`Z-Y9brj}%RrCCMy)PkR;{65<1}eWx3Ooc&_^V}{BP`nL%1bq a8J3|p Date: Mon, 26 Jun 2023 21:40:59 +0600 Subject: [PATCH 22/45] Margin fixed --- frontend/pages/blank.tsx | 2 +- frontend/pages/projects/[slug].tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/pages/blank.tsx b/frontend/pages/blank.tsx index 7cbc5f2..aa37942 100644 --- a/frontend/pages/blank.tsx +++ b/frontend/pages/blank.tsx @@ -19,7 +19,7 @@ export default function ProjectDetailsSection() { viewport={{ once: true }} className="pageTop" > -
+
-
+
Date: Mon, 26 Jun 2023 22:00:06 +0600 Subject: [PATCH 23/45] Image priority removed --- frontend/pages/projects/[slug].tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/pages/projects/[slug].tsx b/frontend/pages/projects/[slug].tsx index 0cabaa1..e53e305 100644 --- a/frontend/pages/projects/[slug].tsx +++ b/frontend/pages/projects/[slug].tsx @@ -120,7 +120,7 @@ export default function ProjectDetailsSection() {

{media.title}

{/* Image file */} - {isImageFile(media.file) ? ( + {isImageFile(media.file) && ( - ) : null} + )} {/* pdf file */} - {getFileExtensionFromBase64(media.file) === 'pdf' ? ( + {getFileExtensionFromBase64(media.file) === 'pdf' && ( - ): null} + )} {/* media description */}

{media.description}

From 7317f2464dae14a4db43fc6fa6e942c5519c7e7c Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Tue, 27 Jun 2023 00:25:16 +0600 Subject: [PATCH 24/45] Static contents refined --- TODO.md | 4 + frontend/components/Certificates.tsx | 5 +- frontend/components/Contact/Contact.tsx | 6 +- frontend/components/Contact/ContactForm.tsx | 2 +- frontend/components/Education.tsx | 6 +- .../components/Home/ExperienceSection.tsx | 55 +++-- frontend/components/Home/SkillSection.tsx | 5 +- frontend/components/Interest.tsx | 5 +- frontend/components/ProjectSection.tsx | 24 ++- frontend/lib/backendAPI.ts | 18 +- frontend/lib/types.ts | 3 + frontend/pages/about.tsx | 2 +- frontend/pages/index.tsx | 201 +++++++++--------- frontend/static_pages/about.mdx | 5 +- frontend/static_pages/privacy-policy.mdx | 33 ++- project/users/admin.py | 3 + project/users/api/serializers.py | 24 +-- ...r_github_user_linkedin_user_resume_link.py | 28 +++ project/users/models.py | 3 + 19 files changed, 258 insertions(+), 174 deletions(-) create mode 100644 project/users/migrations/0004_user_github_user_linkedin_user_resume_link.py diff --git a/TODO.md b/TODO.md index 2196941..48f9db1 100644 --- a/TODO.md +++ b/TODO.md @@ -18,6 +18,10 @@ - A demo body element - Footer +- Utilities + + - Configure EmailJS (Contact Form). Better if mail would send via backend api. + ## Bug Fix - diff --git a/frontend/components/Certificates.tsx b/frontend/components/Certificates.tsx index 0b7d541..88122c7 100644 --- a/frontend/components/Certificates.tsx +++ b/frontend/components/Certificates.tsx @@ -41,7 +41,10 @@ export default function CertificateSection() { className="grid grid-cols-1 mb-10" >
-

Here are some Certificates that I have obtained.

+

+ Here, I will showcase the certifications and professional achievements I have earned throughout my career. + Each certificate I have obtained represents a milestone in my journey and demonstrates my commitment to excellence. +

{certificates.map((certificate: CertificateType) => { return ( - Is there something on your mind you'd like to talk about? Whether it's - related to work or just a casual conversation, I am here and ready to - listen. Please don't hesitate to reach out to me at any time. 🙋‍♂️. + Do you have something on your mind that you'd like to discuss? Whether it's work-related or simply a casual conversation, I'm here and eager to lend an ear. + Please don't hesitate to get in touch with me at any time. 🌟 I'm always ready for engaging discussions and open to connecting with you. + Let's start a conversation and explore new ideas together. 🗣️
diff --git a/frontend/components/Contact/ContactForm.tsx b/frontend/components/Contact/ContactForm.tsx index 8b6b377..b810011 100644 --- a/frontend/components/Contact/ContactForm.tsx +++ b/frontend/components/Contact/ContactForm.tsx @@ -30,7 +30,7 @@ export default function Form() { }; const emailData = { - to_name: "Jatin Sharma", + to_name: "Numan Ibn Mazid", first_name: target.first_name.value.trim(), last_name: target.last_name.value.trim(), email: target.email.value.trim(), diff --git a/frontend/components/Education.tsx b/frontend/components/Education.tsx index 9d8e030..6816e78 100644 --- a/frontend/components/Education.tsx +++ b/frontend/components/Education.tsx @@ -27,7 +27,11 @@ export default function EducationSection({ educations }: { educations: Education className="grid grid-cols-1 mb-10" >
-

Here's a brief rundown of my Academic Background.

+

+ I believe that education plays a crucial role in personal and professional growth. Throughout my academic journey, + I have pursued knowledge and embraced learning opportunities that have shaped my skills and perspectives. + Here is an overview of my educational background and academic achievements. +

{educations ? ( {educations.map((education: EducationType, index) => ( diff --git a/frontend/components/Home/ExperienceSection.tsx b/frontend/components/Home/ExperienceSection.tsx index 9041284..b9b2f71 100644 --- a/frontend/components/Home/ExperienceSection.tsx +++ b/frontend/components/Home/ExperienceSection.tsx @@ -1,18 +1,19 @@ -import { FadeContainer, popUp } from "../../content/FramerMotionVariants" -import { motion } from "framer-motion" -import React from "react" -import { TimelineItem } from "@components/TimelineItem" -import { TimelineList } from "@components/TimelineList" -import { ExperienceType } from "@lib/types" -import AnimatedHeading from "@components/FramerMotion/AnimatedHeading" -import { headingFromLeft } from "@content/FramerMotionVariants" +import { FadeContainer, popUp } from '../../content/FramerMotionVariants' +import { motion } from 'framer-motion' +import React from 'react' +import { TimelineItem } from '@components/TimelineItem' +import { TimelineList } from '@components/TimelineList' +import { ExperienceType } from '@lib/types' +import AnimatedHeading from '@components/FramerMotion/AnimatedHeading' +import { headingFromLeft } from '@content/FramerMotionVariants' import { useRouter } from 'next/router' - +import Link from 'next/link' export default function ExperienceSection({ experiences }: { experiences: ExperienceType[] }) { const router = useRouter() + const isHomePage = router.pathname === '/' // limit experiences to 1 if on home page otherwise show all - const experiencesToDisplay = router.pathname === '/' ? experiences.slice(0, 1) : experiences + const experiencesToDisplay = isHomePage ? experiences.slice(0, 1) : experiences // ******* Loader Starts ******* if (experiences.length === 0) { @@ -28,9 +29,7 @@ export default function ExperienceSection({ experiences }: { experiences: Experi variants={headingFromLeft} > Work Experiences - - {experiences.length} - + {experiences.length}
@@ -42,7 +41,11 @@ export default function ExperienceSection({ experiences }: { experiences: Experi className="grid grid-cols-1 mb-10" >
-

Here's a brief rundown of my professional experiences.

+

+ As an individual, I'm driven by a continuous desire for personal and professional growth. I'm always + seeking opportunities to learn and expand my skill set. Here's a brief rundown of my professional + experiences. +

{experiencesToDisplay ? ( {experiencesToDisplay.map((experience: ExperienceType, index) => ( @@ -67,6 +70,30 @@ export default function ExperienceSection({ experiences }: { experiences: Experi ) : null}
+ + {/* View all experiences link */} + {isHomePage && ( + + View all experiences + + + + + )}
) diff --git a/frontend/components/Home/SkillSection.tsx b/frontend/components/Home/SkillSection.tsx index d303117..e379bbb 100644 --- a/frontend/components/Home/SkillSection.tsx +++ b/frontend/components/Home/SkillSection.tsx @@ -36,7 +36,10 @@ export default function SkillSection() { viewport={{ once: true }} className="mt-12 space-y-6 mb-10" > -

Here are some of my top Skills.

+

+ I possess a diverse range of skills that contribute to my effectiveness in tech industry. + Through experience and continuous learning, I have developed proficiency in various areas. Here are some of my key skills. +

{skills.map((skill: SkillType, index) => { const level = Number(skill.level) || 0 // Convert level to a number or use 0 if it's null or invalid diff --git a/frontend/components/Interest.tsx b/frontend/components/Interest.tsx index 8b8d375..a782691 100644 --- a/frontend/components/Interest.tsx +++ b/frontend/components/Interest.tsx @@ -37,7 +37,10 @@ export default function InterestSection() { viewport={{ once: true }} className="mt-12 space-y-6 mb-10" > -

Here are some of my interests.

+

+ Beyond my professional pursuits, I have a diverse range of interests that fuel my creativity, enhance my problem-solving abilities, + and bring balance to my life. Here are a few of my passions outside of work. +

{interests.map((interest: InterestType, index) => { return ( diff --git a/frontend/components/ProjectSection.tsx b/frontend/components/ProjectSection.tsx index 11d9010..63e040a 100644 --- a/frontend/components/ProjectSection.tsx +++ b/frontend/components/ProjectSection.tsx @@ -6,9 +6,11 @@ import { ProjectType } from '@lib/types' import { motion } from 'framer-motion' import { popUp } from '../content/FramerMotionVariants' import { FadeContainer } from '../content/FramerMotionVariants' -import { HomeHeading } from '../pages' import React from 'react' import AnimatedDiv from '@components/FramerMotion/AnimatedDiv' +import AnimatedHeading from "@components/FramerMotion/AnimatedHeading" +import { headingFromLeft } from "@content/FramerMotionVariants" + export default function ProjectSection({ projects }: { projects: ProjectType[] }) { // ******* Loader Starts ******* @@ -19,7 +21,17 @@ export default function ProjectSection({ projects }: { projects: ProjectType[] } return (
- +
+ + Projects + + {projects.length} + + +
-

- I've been making various types of projects some of them were basics and some of them were complicated. So - far I've made {projects.length}+{' '} - projects. +

+ I believe that projects are not just tasks to be completed, but opportunities to bring ideas to life, solve problems, and make a meaningful impact. + In each project, I follow a meticulous approach, combining innovative thinking, strategic planning, and attention to detail. + Here I will showcase some of the exciting projects I have worked on.

{projects.map((project: ProjectType, index) => ( diff --git a/frontend/lib/backendAPI.ts b/frontend/lib/backendAPI.ts index f1f4c12..dc55278 100644 --- a/frontend/lib/backendAPI.ts +++ b/frontend/lib/backendAPI.ts @@ -6,7 +6,7 @@ const BACKEND_API_TOKEN = process.env.BACKEND_API_TOKEN // *** PROFILE *** // Profile URL const PROFILE_PATH = "users/get_portfolio_user/" -const PROFILE_ENDPOINT = "http://127.0.0.1:8000/api/" + PROFILE_PATH +const PROFILE_ENDPOINT = BACKEND_API_BASE_URL + PROFILE_PATH /** * Makes a request to the BACKEND API to retrieve Portfolio User Information. @@ -36,7 +36,7 @@ export const getProfileInfo = async () => { // Experience URL const EXPERIENCE_PATH = "professional-experiences/" -const EXPERIENCE_ENDPOINT = "http://127.0.0.1:8000/api/" + EXPERIENCE_PATH +const EXPERIENCE_ENDPOINT = BACKEND_API_BASE_URL + EXPERIENCE_PATH /** * Makes a request to the BACKEND API to retrieve all Experience Data. @@ -74,7 +74,7 @@ export const getAllExperiences = async (length?: number | undefined) => { // Skills URL const SKILLS_PATH = "skills/" -const SKILLS_ENDPOINT = "http://127.0.0.1:8000/api/" + SKILLS_PATH +const SKILLS_ENDPOINT = BACKEND_API_BASE_URL + SKILLS_PATH /** * Makes a request to the BACKEND API to retrieve all Skills Data. @@ -102,7 +102,7 @@ export const getAllSkills = async () => { // Educations URL const EDUCATIONS_PATH = "educations/" -const EDUCATIONS_ENDPOINT = "http://127.0.0.1:8000/api/" + EDUCATIONS_PATH +const EDUCATIONS_ENDPOINT = BACKEND_API_BASE_URL + EDUCATIONS_PATH /** * Makes a request to the BACKEND API to retrieve all Educations Data. @@ -130,7 +130,7 @@ export const getAllEducations = async () => { // Certificates URL const CERTIFICATES_PATH = "certifications/" -const CERTIFICATES_ENDPOINT = "http://127.0.0.1:8000/api/" + CERTIFICATES_PATH +const CERTIFICATES_ENDPOINT = BACKEND_API_BASE_URL + CERTIFICATES_PATH /** * Makes a request to the BACKEND API to retrieve all Certificates Data. @@ -158,7 +158,7 @@ export const getAllCertificates = async () => { // Projects URL const PROJECTS_PATH = "projects/" -const PROJECTS_ENDPOINT = "http://127.0.0.1:8000/api/" + PROJECTS_PATH +const PROJECTS_ENDPOINT = BACKEND_API_BASE_URL + PROJECTS_PATH /** * Makes a request to the BACKEND API to retrieve all Projects Data. @@ -206,7 +206,7 @@ export const getProjectDetails = async (slug: string) => { // Interests URL const INTERESTS_PATH = "interests/" -const INTERESTS_ENDPOINT = "http://127.0.0.1:8000/api/" + INTERESTS_PATH +const INTERESTS_ENDPOINT = BACKEND_API_BASE_URL + INTERESTS_PATH /** * Makes a request to the BACKEND API to retrieve all Interests Data. @@ -235,7 +235,7 @@ export const getAllInterests = async () => { // Movies URL const MOVIE_PATH = "movies/" -const MOVIE_ENDPOINT = "http://127.0.0.1:8000/api/" + MOVIE_PATH +const MOVIE_ENDPOINT = BACKEND_API_BASE_URL + MOVIE_PATH /** * Makes a request to the BACKEND API to retrieve all Movies Data. @@ -263,7 +263,7 @@ export const getAllMovies = async () => { // Blogs URL const BLOGS_PATH = "/posts" -const BLOGS_ENDPOINT = BACKEND_API_BASE_URL + BLOGS_PATH +const BLOGS_ENDPOINT = "https://jsonplaceholder.typicode.com" + BLOGS_PATH /** * Makes a request to the BACKEND API to retrieve all Blogs Data. diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts index a852bcd..8d55824 100644 --- a/frontend/lib/types.ts +++ b/frontend/lib/types.ts @@ -31,9 +31,12 @@ export type ProfileType = { website: string contact: string contact_email: string + linkedin: string + github: string address: string about: string is_portfolio_user: string + resume_link: string is_active: string is_staff: string is_superuser: string diff --git a/frontend/pages/about.tsx b/frontend/pages/about.tsx index a50f573..162f9ae 100644 --- a/frontend/pages/about.tsx +++ b/frontend/pages/about.tsx @@ -71,7 +71,7 @@ export default function About({ whileInView="visible" variants={FadeContainer} viewport={{ once: true }} - className="grid min-h-screen py-20 place-content-center" + className="grid min-h-screen py-7 place-content-center" >
diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index 8d664c3..38c2455 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -1,33 +1,28 @@ // Page Components START---------- -import BlogsSection from "@components/Home/BlogsSection" -import SkillSection from "@components/Home/SkillSection" -import ExperienceSection from "@components/Home/ExperienceSection" -import Image from "next/image" -import Metadata from "@components/MetaData" -import Contact from "@components/Contact" -import { - FadeContainer, - headingFromLeft, - opacityVariant, - popUp, -} from "@content/FramerMotionVariants" -import AnimatedHeading from "@components/FramerMotion/AnimatedHeading" -import { homeProfileImage, cvURL } from "@utils/utils" -import getRSS from "@lib/generateRSS" -import generateSitemap from "@lib/sitemap" -import { motion } from "framer-motion" -import { FiDownload } from "react-icons/fi" -import pageMeta from "@content/meta" -import staticData from "@content/StaticData" -import React from "react" -import Link from "next/link" +import BlogsSection from '@components/Home/BlogsSection' +import SkillSection from '@components/Home/SkillSection' +import ExperienceSection from '@components/Home/ExperienceSection' +import Image from 'next/image' +import Metadata from '@components/MetaData' +import Contact from '@components/Contact' +import { FadeContainer, headingFromLeft, opacityVariant, popUp } from '@content/FramerMotionVariants' +import AnimatedHeading from '@components/FramerMotion/AnimatedHeading' +import { homeProfileImage } from '@utils/utils' +import getRSS from '@lib/generateRSS' +import generateSitemap from '@lib/sitemap' +import { motion } from 'framer-motion' +import { FiDownload } from 'react-icons/fi' +import pageMeta from '@content/meta' +import staticData from '@content/StaticData' +import React from 'react' +import Link from 'next/link' import { useEffect, useState } from 'react' -import { getProfileInfo, getAllExperiences, getAllBlogs } from "@lib/backendAPI" -import { ProfileType, ExperienceType } from "@lib/types" - +import { getProfileInfo, getAllExperiences, getAllBlogs } from '@lib/backendAPI' +import { ProfileType, ExperienceType } from '@lib/types' +import { BsGithub, BsLinkedin } from 'react-icons/bs' export default function Home() { - const [profileInfo, setProfileInfo] = useState(null) + const [profileInfo, setProfileInfo] = useState() const [experiences, setExperiences] = useState([]) const [blogs, setBlogs] = useState([]) @@ -58,6 +53,8 @@ export default function Home() { // } // // ******* Loader Ends ******* + const latest_experience = experiences[0] + return ( <> -
+
- + {profileInfo?.name} {profileInfo?.nickname && ({profileInfo.nickname})} - {staticData.personal.profession} + {latest_experience?.designation} + at + {latest_experience?.company}
- {staticData.personal.current_position} - - {profileInfo?.about || staticData.personal.about} @@ -124,75 +114,78 @@ export default function Home() {
- - Address: {profileInfo?.address || "Dhaka, Bangladesh"} - - - Email: - - - {profileInfo?.contact_email || "numanibnmazid@gmail.com"} - - - - - Contact: - -
- {profileInfo?.contact || "+880 1685238317"} - - - + {/* Address */} +
Address: {profileInfo?.address || 'Dhaka, Bangladesh'}
+ {/* Email */} + + {/* Contact */} + + + + + {/* LinkedIn */} + {profileInfo?.linkedin && ( +
+ + + +
+ )} + + {/* Github */} + {profileInfo?.github && ( +
+ + + +
+ )} +
- - -

Resume

- + {/* Resume Download Button */} + {profileInfo?.resume_link && ( + + +

Resume

+ + )}
- {/* Experience Section */} - {/* View all experiences link */} - - View all experiences - - - - - {/* Skills Section */} @@ -204,7 +197,7 @@ export default function Home() {
- ); + ) } export function HomeHeading({ title }: { title: React.ReactNode | string }) { @@ -215,14 +208,14 @@ export function HomeHeading({ title }: { title: React.ReactNode | string }) { > {title} - ); + ) } export async function getStaticProps() { - await getRSS(); - await generateSitemap(); + await getRSS() + await generateSitemap() return { - props: { }, - }; + props: {}, + } } diff --git a/frontend/static_pages/about.mdx b/frontend/static_pages/about.mdx index 4f5127a..d2f749f 100644 --- a/frontend/static_pages/about.mdx +++ b/frontend/static_pages/about.mdx @@ -7,5 +7,6 @@ excerpt: "" image: https://imgur.com/C6GBjJt.png --- -Hey, I am Numan Ibn Mazid. -Experienced professional Software Engineer who enjoys developing innovative software solutions that are tailored to customer desirability and usability. +Thank you for taking the time to learn more about me. + +I am Numan Ibn Mazid. You can call me Numan. I'm excited to share a glimpse into my background, work experience, education, my projects, what I like etc. diff --git a/frontend/static_pages/privacy-policy.mdx b/frontend/static_pages/privacy-policy.mdx index e72ed5b..762ad17 100644 --- a/frontend/static_pages/privacy-policy.mdx +++ b/frontend/static_pages/privacy-policy.mdx @@ -1,20 +1,39 @@ --- slug: privacy title: Privacy Policy -date: 2023-01-17 +date: 2023-06-26 published: true image: https://imgur.com/ghlRutT.png --- -At [j471n.in](https://j471n.in), I value your privacy and take it seriously. I do not collect any personal information from users, including but not limited to names, email addresses, and other contact information. +At `numanibnmazid.com`, we value and respect your privacy. This Privacy Policy outlines how we collect, use, and protect your personal information when you visit our website. -I do keep track of visitor count for certain pages (blogs) on the website, but this information is not linked to any specific user and is used for internal analytics purposes only. +## Information Collection and Use -I use [Substack](https://substack.com/) to send newsletters to my subscribers. When you subscribe to my [newsletter](https://j471n.substack.com/), your email address is stored on Substack's servers and listed as part of my subscribers under my substack account. This email address is used solely for the purpose of sending future newsletters that I publish. +When you visit our website, we may collect certain information about you, such as your name, email address, and any other information you provide voluntarily. We use this information solely for the purpose of improving your experience on our website and providing you with the requested services. -I don't share any information collected through this website or newsletter with any third parties, except as required by law. +## Use of Cookies -I reserve the right to update this Privacy Policy at any time, and will notify you of any changes by posting the updated policy. +We may use cookies and similar technologies to enhance your browsing experience on our website. Cookies are small text files that are stored on your device to collect information about your visit. These cookies help us analyze website traffic, customize content, and remember your preferences. You have the option to disable cookies in your browser settings, but please note that some features of our website may not function properly without them. -If you have any questions or concerns about the Privacy Policy, please contact met at [me@j471n.in](mailto:me@j471n.in). +## Data Security +We take data security seriously and implement appropriate measures to protect your personal information from unauthorized access, disclosure, alteration, or destruction. However, please be aware that no method of transmission over the internet or electronic storage is 100% secure, and we cannot guarantee absolute security of your data. + +## Third-Party Links + +Our website may contain links to third-party websites or services. We are not responsible for the privacy practices or content of those websites. We encourage you to review the privacy policies of any third-party sites you visit. + +## Children's Privacy + +Our website is not intended for use by individuals under the age of 13. We do not knowingly collect personal information from children. If you believe we have unintentionally collected information from a child, please contact us immediately, and we will take steps to remove that information from our records. + +## Changes to This Privacy Policy + +We reserve the right to update or modify this Privacy Policy at any time. Any changes will be effective immediately upon posting on this page. We encourage you to review this Privacy Policy periodically to stay informed about how we collect, use, and protect your personal information. + +## Contact Us + +If you have any questions or concerns about this Privacy Policy or your personal information, please don't hesitate to contact us. We are here to address any queries and ensure your privacy is respected. + +Thank you for trusting us with your personal information. diff --git a/project/users/admin.py b/project/users/admin.py index 15e002e..556f882 100644 --- a/project/users/admin.py +++ b/project/users/admin.py @@ -22,6 +22,9 @@ class UserAdmin(BaseUserAdmin): "website", "contact", "contact_email", + "linkedin", + "github", + "resume_link", "address", "about", "last_login", diff --git a/project/users/api/serializers.py b/project/users/api/serializers.py index b995b96..d854536 100644 --- a/project/users/api/serializers.py +++ b/project/users/api/serializers.py @@ -7,29 +7,7 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = get_user_model() - fields = ( - "id", - "username", - "email", - "name", - "slug", - "nickname", - "gender", - "image", - "dob", - "website", - "contact", - "contact_email", - "address", - "about", - "is_portfolio_user", - "is_active", - "is_staff", - "is_superuser", - "date_joined", - "last_login", - "updated_at", - ) + fields = "__all__" read_only_fields = ( "id", "username", diff --git a/project/users/migrations/0004_user_github_user_linkedin_user_resume_link.py b/project/users/migrations/0004_user_github_user_linkedin_user_resume_link.py new file mode 100644 index 0000000..87241e0 --- /dev/null +++ b/project/users/migrations/0004_user_github_user_linkedin_user_resume_link.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.1 on 2023-06-26 17:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0003_rename_nick_name_user_nickname"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="github", + field=models.URLField(blank=True, null=True), + ), + migrations.AddField( + model_name="user", + name="linkedin", + field=models.URLField(blank=True, null=True), + ), + migrations.AddField( + model_name="user", + name="resume_link", + field=models.URLField(blank=True, null=True), + ), + ] diff --git a/project/users/models.py b/project/users/models.py index e0ce334..38b7b20 100644 --- a/project/users/models.py +++ b/project/users/models.py @@ -94,9 +94,12 @@ class Gender(models.TextChoices): website = models.URLField(null=True, blank=True) contact = models.CharField(max_length=30, null=True, blank=True) contact_email = models.EmailField(null=True, blank=True) + linkedin = models.URLField(null=True, blank=True) + github = models.URLField(null=True, blank=True) address = models.CharField(max_length=254, null=True, blank=True) about = models.TextField(null=True, blank=True) is_portfolio_user = models.BooleanField(default=False) + resume_link = models.URLField(null=True, blank=True) """ Additional Fields Ends """ is_staff = models.BooleanField(default=False) is_superuser = models.BooleanField(default=False) From 2cb8495e82d445934e54d7b17def453238bb6629 Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Tue, 27 Jun 2023 01:01:19 +0600 Subject: [PATCH 25/45] Error State --- frontend/lib/github.ts | 74 ++++++++++++++++++++-------------------- frontend/pages/index.tsx | 6 ---- frontend/pages/stats.tsx | 44 ++++++++++++------------ 3 files changed, 59 insertions(+), 65 deletions(-) diff --git a/frontend/lib/github.ts b/frontend/lib/github.ts index e3b8348..0cf1f04 100644 --- a/frontend/lib/github.ts +++ b/frontend/lib/github.ts @@ -1,45 +1,45 @@ import { GithubRepo } from "./types"; const tempData = { - login: "j471n", - id: 55713505, - node_id: "MDQ6VXNlcjU1NzEzNTA1", - avatar_url: "https://avatars.githubusercontent.com/u/55713505?v=4", - gravatar_id: "", - url: "https://api.github.com/users/j471n", - html_url: "https://github.com/j471n", - followers_url: "https://api.github.com/users/j471n/followers", - following_url: "https://api.github.com/users/j471n/following{/other_user}", - gists_url: "https://api.github.com/users/j471n/gists{/gist_id}", - starred_url: "https://api.github.com/users/j471n/starred{/owner}{/repo}", - subscriptions_url: "https://api.github.com/users/j471n/subscriptions", - organizations_url: "https://api.github.com/users/j471n/orgs", - repos_url: "https://api.github.com/users/j471n/repos", - events_url: "https://api.github.com/users/j471n/events{/privacy}", - received_events_url: "https://api.github.com/users/j471n/received_events", - type: "User", - site_admin: false, - name: "Jatin Sharma", - company: null, - blog: "j471n.in", - location: "India", - email: null, - hireable: true, - bio: "React Developer", - twitter_username: "j471n_", - public_repos: 31, - public_gists: 10, - followers: 8, - following: 1, - created_at: "2019-09-23T18:37:14Z", - updated_at: "2022-07-02T03:07:58Z", -}; + "login": "NumanIbnMazid", + "id": 38869177, + "node_id": "MDQ6VXNlcjM4ODY5MTc3", + "avatar_url": "https://avatars.githubusercontent.com/u/38869177?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/NumanIbnMazid", + "html_url": "https://github.com/NumanIbnMazid", + "followers_url": "https://api.github.com/users/NumanIbnMazid/followers", + "following_url": "https://api.github.com/users/NumanIbnMazid/following{/other_user}", + "gists_url": "https://api.github.com/users/NumanIbnMazid/gists{/gist_id}", + "starred_url": "https://api.github.com/users/NumanIbnMazid/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/NumanIbnMazid/subscriptions", + "organizations_url": "https://api.github.com/users/NumanIbnMazid/orgs", + "repos_url": "https://api.github.com/users/NumanIbnMazid/repos", + "events_url": "https://api.github.com/users/NumanIbnMazid/events{/privacy}", + "received_events_url": "https://api.github.com/users/NumanIbnMazid/received_events", + "type": "User", + "site_admin": false, + "name": "Numan Ibn Mazid", + "company": "SELISE DIGITAL PLATFORMS", + "blog": "https://www.linkedin.com/in/numanibnmazid/", + "location": "Dhaka, Bangladesh", + "email": "numanibnmazid@gmail.com", + "hireable": true, + "bio": "Experienced professional Software Engineer who enjoys developing innovative software solutions that are tailored to customer desirability and usability.", + "twitter_username": "NumanIbnMazid", + "public_repos": 84, + "public_gists": 0, + "followers": 13, + "following": 35, + "created_at": "2018-04-30T21:30:32Z", + "updated_at": "2023-06-24T11:38:55Z" +} // its for /api/stats/github export async function fetchGithub() { const fake = false; if (fake) return tempData; - return fetch("https://api.github.com/users/j471n").then((res) => res.json()); + return fetch("https://api.github.com/users/NumanIbnMazid").then((res) => res.json()); } // its for getting temporary old data @@ -51,7 +51,7 @@ export function getOldStats() { export async function getGithubStarsAndForks() { // Fetch user's repositories from the GitHub API const res = await fetch( - "https://api.github.com/users/j471n/repos?per_page=100" + "https://api.github.com/users/NumanIbnMazid/repos?per_page=100" ); const userRepos = await res.json(); @@ -61,8 +61,8 @@ export async function getGithubStarsAndForks() { "https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting") ) { return { - githubStars: 74, - forks: 33, + githubStars: 0, + forks: 0, }; } // filter those repos that are forked diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index 38c2455..4dfed64 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -47,12 +47,6 @@ export default function Home() { fetchBlogs() }, []) - // // ******* Loader Starts ******* - // if (blogs.length === 0) { - // return
Loading...
- // } - // // ******* Loader Ends ******* - const latest_experience = experiences[0] return ( diff --git a/frontend/pages/stats.tsx b/frontend/pages/stats.tsx index bc3b7f8..bdd96a1 100644 --- a/frontend/pages/stats.tsx +++ b/frontend/pages/stats.tsx @@ -21,30 +21,30 @@ type Stats = { export default function Stats() { const { data: topTracks } = useSWR("/api/stats/tracks", fetcher); const { data: artists } = useSWR("/api/stats/artists", fetcher); - const { data: devto } = useSWR("/api/stats/devto", fetcher); + // const { data: devto } = useSWR("/api/stats/devto", fetcher); const { data: github } = useSWR("/api/stats/github", fetcher); const stats: Stats[] = [ - { - title: "Total Posts", - value: devto?.posts.toLocaleString(), - }, - { - title: "Blog Followers", - value: devto?.followers.toLocaleString(), - }, - { - title: "Blog Reactions", - value: devto?.likes.toLocaleString(), - }, - { - title: "Blog Views", - value: devto?.views.toLocaleString(), - }, - { - title: "Blog Comments", - value: devto?.comments.toLocaleString(), - }, + // { + // title: "Total Posts", + // value: devto?.posts.toLocaleString(), + // }, + // { + // title: "Blog Followers", + // value: devto?.followers.toLocaleString(), + // }, + // { + // title: "Blog Reactions", + // value: devto?.likes.toLocaleString(), + // }, + // { + // title: "Blog Views", + // value: devto?.views.toLocaleString(), + // }, + // { + // title: "Blog Comments", + // value: devto?.comments.toLocaleString(), + // }, { title: "Github Repos", value: github?.repos, @@ -211,7 +211,7 @@ function LoadingArtists() { {Array.from(Array(5).keys()).map((item) => (
<>
From 7921082efda141629c71dd528ab239f4e66ccda1 Mon Sep 17 00:00:00 2001 From: Numan Ibn Mazid Date: Tue, 27 Jun 2023 04:13:45 +0600 Subject: [PATCH 26/45] Frontend Refined #35 #36 --- TODO.md | 7 + frontend/.env.example | 3 + frontend/components/Footer.tsx | 18 +- frontend/components/PDFViewer.tsx | 2 +- frontend/lib/github.ts | 49 +- frontend/next.config.js | 3 +- frontend/pages/stats.tsx | 177 +- ...css-resources-that-you-should-bookmark.mdx | 105 - .../10-git-commands-everybody-should-know.mdx | 208 - ...-chrome-edge-extensions-for-developers.mdx | 49 - frontend/posts/6-ways-to-center-a-div.mdx | 163 - frontend/posts/access-local-server.mdx | 51 - frontend/posts/active-navbar-next-js.mdx | 157 - frontend/posts/battery-api.mdx | 215 - .../before-and-after-in-tailwind-css.mdx | 42 - frontend/posts/ce-dec-2022.mdx | 132 - frontend/posts/ce-feb-2023.mdx | 100 - frontend/posts/ce-jan-2023.mdx | 131 - frontend/posts/ce-nov-2022.mdx | 117 - frontend/posts/chrome-extensions-april.mdx | 78 - frontend/posts/chrome-extensions-aug-2022.mdx | 200 - .../posts/chrome-extensions-july-2022.mdx | 213 - frontend/posts/chrome-extensions-june.mdx | 153 - frontend/posts/chrome-extensions-oct-2022.mdx | 88 - frontend/posts/chrome-extensions-sep-2022.mdx | 111 - frontend/posts/clean-loading-animation.mdx | 71 - frontend/posts/colorful-rain-with-js.mdx | 110 - frontend/posts/convert-nextjs-app-to-pwa.mdx | 189 - frontend/posts/countdown-loading-with-js.mdx | 116 - frontend/posts/countries-flags.mdx | 843 -- ...reate-bash-script-and-file-permissions.mdx | 110 - .../posts/creative-hover-menu-with-css.mdx | 142 - frontend/posts/css-battle-1.mdx | 114 - frontend/posts/css-battle-10.mdx | 98 - frontend/posts/css-battle-11.mdx | 101 - frontend/posts/css-battle-12.mdx | 102 - frontend/posts/css-battle-13.mdx | 90 - frontend/posts/css-battle-14.mdx | 108 - frontend/posts/css-battle-15.mdx | 90 - frontend/posts/css-battle-2.mdx | 78 - frontend/posts/css-battle-3.mdx | 136 - frontend/posts/css-battle-4.mdx | 138 - frontend/posts/css-battle-5.mdx | 184 - frontend/posts/css-battle-6.mdx | 154 - frontend/posts/css-battle-7.mdx | 87 - frontend/posts/css-battle-8.mdx | 102 - frontend/posts/css-battle-9.mdx | 85 - frontend/posts/css-flag-india.mdx | 321 - .../posts/css-gradient-loading-animation.mdx | 67 - frontend/posts/css-icon-android.mdx | 178 - frontend/posts/css-icon-google-photos.mdx | 91 - frontend/posts/css-icon-gpay.mdx | 219 - frontend/posts/css-icon-microsoft.mdx | 98 - frontend/posts/curved-timeline-in-css.mdx | 168 - ...hen-users-switch-tabs-using-javascript.mdx | 70 - frontend/posts/dev-to-api.mdx | 85 - frontend/posts/getting-started-with-bash.mdx | 75 - ...glassmorphism-circle-loading-animation.mdx | 77 - .../posts/glassmorphism-loading-animation.mdx | 75 - frontend/posts/google-analytics-data-api.mdx | 186 - .../posts/image-slider-with-vanila-js.mdx | 424 - frontend/posts/ip-address-and-classes.mdx | 244 - frontend/posts/js-cheatsheet.mdx | 2159 ----- frontend/posts/next-google-docs.mdx | 250 - frontend/posts/next-google.mdx | 66 - frontend/posts/notification-panel.mdx | 101 - frontend/posts/os-conepts.mdx | 172 - frontend/posts/paths-and-comments-in-bash.mdx | 120 - frontend/posts/pip-web-api.mdx | 137 - frontend/posts/portfolio-tutorial.mdx | 7773 ----------------- ...t-heroku-server-from-sleeping-for-free.mdx | 52 - frontend/posts/preview-file-in-react.mdx | 323 - frontend/posts/rss-feed-for-nextjs.mdx | 384 - frontend/posts/scroll-to-the-top-with-js.mdx | 222 - ...ome-javascript-methods-you-should-know.mdx | 166 - .../some-strange-concept-of-javascript.mdx | 191 - frontend/posts/spotify-api-nextjs.mdx | 349 - frontend/posts/timeline-with-css.mdx | 102 - frontend/posts/truncate-css.mdx | 66 - frontend/posts/ts.mdx | 2021 ----- frontend/posts/typing-effect-by-using-css.mdx | 69 - frontend/posts/typing-effect-with-typedjs.mdx | 54 - .../video-as-text-background-using-css.mdx | 72 - frontend/posts/vs-code-setup.mdx | 376 - frontend/posts/web-share-api.mdx | 79 - frontend/posts/web-storage-api.mdx | 240 - .../posts/why-i-ditched-chrome-for-edge.mdx | 136 - frontend/posts/windows-commands.mdx | 731 -- frontend/public/sitemap.xml | 324 - 89 files changed, 138 insertions(+), 24795 deletions(-) delete mode 100644 frontend/posts/10-css-resources-that-you-should-bookmark.mdx delete mode 100644 frontend/posts/10-git-commands-everybody-should-know.mdx delete mode 100644 frontend/posts/5-very-useful-chrome-edge-extensions-for-developers.mdx delete mode 100644 frontend/posts/6-ways-to-center-a-div.mdx delete mode 100644 frontend/posts/access-local-server.mdx delete mode 100644 frontend/posts/active-navbar-next-js.mdx delete mode 100644 frontend/posts/battery-api.mdx delete mode 100644 frontend/posts/before-and-after-in-tailwind-css.mdx delete mode 100644 frontend/posts/ce-dec-2022.mdx delete mode 100644 frontend/posts/ce-feb-2023.mdx delete mode 100644 frontend/posts/ce-jan-2023.mdx delete mode 100644 frontend/posts/ce-nov-2022.mdx delete mode 100644 frontend/posts/chrome-extensions-april.mdx delete mode 100644 frontend/posts/chrome-extensions-aug-2022.mdx delete mode 100644 frontend/posts/chrome-extensions-july-2022.mdx delete mode 100644 frontend/posts/chrome-extensions-june.mdx delete mode 100644 frontend/posts/chrome-extensions-oct-2022.mdx delete mode 100644 frontend/posts/chrome-extensions-sep-2022.mdx delete mode 100644 frontend/posts/clean-loading-animation.mdx delete mode 100644 frontend/posts/colorful-rain-with-js.mdx delete mode 100644 frontend/posts/convert-nextjs-app-to-pwa.mdx delete mode 100644 frontend/posts/countdown-loading-with-js.mdx delete mode 100644 frontend/posts/countries-flags.mdx delete mode 100644 frontend/posts/create-bash-script-and-file-permissions.mdx delete mode 100644 frontend/posts/creative-hover-menu-with-css.mdx delete mode 100644 frontend/posts/css-battle-1.mdx delete mode 100644 frontend/posts/css-battle-10.mdx delete mode 100644 frontend/posts/css-battle-11.mdx delete mode 100644 frontend/posts/css-battle-12.mdx delete mode 100644 frontend/posts/css-battle-13.mdx delete mode 100644 frontend/posts/css-battle-14.mdx delete mode 100644 frontend/posts/css-battle-15.mdx delete mode 100644 frontend/posts/css-battle-2.mdx delete mode 100644 frontend/posts/css-battle-3.mdx delete mode 100644 frontend/posts/css-battle-4.mdx delete mode 100644 frontend/posts/css-battle-5.mdx delete mode 100644 frontend/posts/css-battle-6.mdx delete mode 100644 frontend/posts/css-battle-7.mdx delete mode 100644 frontend/posts/css-battle-8.mdx delete mode 100644 frontend/posts/css-battle-9.mdx delete mode 100644 frontend/posts/css-flag-india.mdx delete mode 100644 frontend/posts/css-gradient-loading-animation.mdx delete mode 100644 frontend/posts/css-icon-android.mdx delete mode 100644 frontend/posts/css-icon-google-photos.mdx delete mode 100644 frontend/posts/css-icon-gpay.mdx delete mode 100644 frontend/posts/css-icon-microsoft.mdx delete mode 100644 frontend/posts/curved-timeline-in-css.mdx delete mode 100644 frontend/posts/detect-when-users-switch-tabs-using-javascript.mdx delete mode 100644 frontend/posts/dev-to-api.mdx delete mode 100644 frontend/posts/getting-started-with-bash.mdx delete mode 100644 frontend/posts/glassmorphism-circle-loading-animation.mdx delete mode 100644 frontend/posts/glassmorphism-loading-animation.mdx delete mode 100644 frontend/posts/google-analytics-data-api.mdx delete mode 100644 frontend/posts/image-slider-with-vanila-js.mdx delete mode 100644 frontend/posts/ip-address-and-classes.mdx delete mode 100644 frontend/posts/js-cheatsheet.mdx delete mode 100644 frontend/posts/next-google-docs.mdx delete mode 100644 frontend/posts/next-google.mdx delete mode 100644 frontend/posts/notification-panel.mdx delete mode 100644 frontend/posts/os-conepts.mdx delete mode 100644 frontend/posts/paths-and-comments-in-bash.mdx delete mode 100644 frontend/posts/pip-web-api.mdx delete mode 100644 frontend/posts/portfolio-tutorial.mdx delete mode 100644 frontend/posts/prevent-heroku-server-from-sleeping-for-free.mdx delete mode 100644 frontend/posts/preview-file-in-react.mdx delete mode 100644 frontend/posts/rss-feed-for-nextjs.mdx delete mode 100644 frontend/posts/scroll-to-the-top-with-js.mdx delete mode 100644 frontend/posts/some-javascript-methods-you-should-know.mdx delete mode 100644 frontend/posts/some-strange-concept-of-javascript.mdx delete mode 100644 frontend/posts/spotify-api-nextjs.mdx delete mode 100644 frontend/posts/timeline-with-css.mdx delete mode 100644 frontend/posts/truncate-css.mdx delete mode 100644 frontend/posts/ts.mdx delete mode 100644 frontend/posts/typing-effect-by-using-css.mdx delete mode 100644 frontend/posts/typing-effect-with-typedjs.mdx delete mode 100644 frontend/posts/video-as-text-background-using-css.mdx delete mode 100644 frontend/posts/vs-code-setup.mdx delete mode 100644 frontend/posts/web-share-api.mdx delete mode 100644 frontend/posts/web-storage-api.mdx delete mode 100644 frontend/posts/why-i-ditched-chrome-for-edge.mdx delete mode 100644 frontend/posts/windows-commands.mdx diff --git a/TODO.md b/TODO.md index 48f9db1..7ba5480 100644 --- a/TODO.md +++ b/TODO.md @@ -21,6 +21,13 @@ - Utilities - Configure EmailJS (Contact Form). Better if mail would send via backend api. + - Fix Utilities Page. + - Fix Every page's meta option. + - Fix Snippets, Newsletter, RSS, dev.to. Fix footer. + - Fix Visitor Count. + - Fix Bar code Scanner. + - Fix Spotify. + - Implement Youtube Stats. ## Bug Fix diff --git a/frontend/.env.example b/frontend/.env.example index f10fb68..a742eab 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -43,3 +43,6 @@ BACKEND_API_BASE_URL= # Backend API TOKEN BACKEND_API_TOKEN= + +# Github Access TOKEN +GITHUB_ACCESS_TOKEN= diff --git a/frontend/components/Footer.tsx b/frontend/components/Footer.tsx index f424c6b..8d228fe 100644 --- a/frontend/components/Footer.tsx +++ b/frontend/components/Footer.tsx @@ -100,28 +100,18 @@ export default function Footer({ - Powered by + Developed by - Next.js - - and - - Vercel + Numan Ibn Mazid diff --git a/frontend/components/PDFViewer.tsx b/frontend/components/PDFViewer.tsx index d21ad3d..0103062 100644 --- a/frontend/components/PDFViewer.tsx +++ b/frontend/components/PDFViewer.tsx @@ -39,7 +39,7 @@ const PDFViewer: React.FC = ({ base64String }) => { file={base64String} onLoadSuccess={onDocumentLoadSuccess} > - {Array.from(new Array(numPages || 0), (el, index) => ( + {Array.from(new Array(numPages || 0), (_, index) => (
res.json()); + const fake = false + if (fake) return tempData + + return fetch( + "https://api.github.com/users/NumanIbnMazid", + { + headers: { + Authorization: `Bearer ${GitHubAccessToken}`, + }, + } + ).then((res) => res.json()) } // its for getting temporary old data export function getOldStats() { - return tempData; + return tempData } /* Retrieves the number of stars and forks for the user's repositories on GitHub. */ export async function getGithubStarsAndForks() { // Fetch user's repositories from the GitHub API const res = await fetch( - "https://api.github.com/users/NumanIbnMazid/repos?per_page=100" - ); - const userRepos = await res.json(); + "https://api.github.com/users/NumanIbnMazid/repos?per_page=100", + { + headers: { + Authorization: `Bearer ${GitHubAccessToken}`, + }, + } + ) + const userRepos = await res.json() /* Default Static Data: If use exceeded the rate limit of api */ if ( @@ -61,30 +76,30 @@ export async function getGithubStarsAndForks() { "https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting") ) { return { - githubStars: 0, - forks: 0, - }; + githubStars: 7, + forks: 4, + } } // filter those repos that are forked const mineRepos: GithubRepo[] = userRepos?.filter( (repo: GithubRepo) => !repo.fork - ); + ) // Calculate the total number of stars for the user's repositories const githubStars = mineRepos.reduce( (accumulator: number, repository: GithubRepo) => { - return accumulator + repository["stargazers_count"]; + return accumulator + repository["stargazers_count"] }, 0 - ); + ) // Calculate the total number of forks for the user's repositories const forks = mineRepos.reduce( (accumulator: number, repository: GithubRepo) => { - return accumulator + repository["forks_count"]; + return accumulator + repository["forks_count"] }, 0 - ); + ) - return { githubStars, forks }; + return { githubStars, forks } } diff --git a/frontend/next.config.js b/frontend/next.config.js index 7f7f3d5..26e2c27 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -33,6 +33,7 @@ module.exports = withPWA({ }, env:{ BACKEND_API_BASE_URL : process.env.BACKEND_API_BASE_URL, - BACKEND_API_TOKEN : process.env.BACKEND_API_TOKEN + BACKEND_API_TOKEN : process.env.BACKEND_API_TOKEN, + GITHUB_ACCESS_TOKEN : process.env.GITHUB_ACCESS_TOKEN, } }); diff --git a/frontend/pages/stats.tsx b/frontend/pages/stats.tsx index bdd96a1..c273e43 100644 --- a/frontend/pages/stats.tsx +++ b/frontend/pages/stats.tsx @@ -1,28 +1,30 @@ -import React from "react"; -import useSWR from "swr"; -import { FadeContainer, opacityVariant } from "@content/FramerMotionVariants"; -import fetcher from "@lib/fetcher"; -import MetaData from "@components/MetaData"; -import PageTop from "@components/PageTop"; -import StatsCard from "@components/Stats/StatsCard"; -import Track from "@components/Stats/Track"; -import Artist from "@components/Stats/Artist"; -import AnimatedHeading from "@components/FramerMotion/AnimatedHeading"; -import AnimatedText from "@components/FramerMotion/AnimatedText"; -import pageMeta from "@content/meta"; -import { SpotifyArtist, SpotifyTrack } from "@lib/types"; -import AnimatedDiv from "@components/FramerMotion/AnimatedDiv"; +import React from "react" +import useSWR from "swr" +import { FadeContainer } from "@content/FramerMotionVariants" +import fetcher from "@lib/fetcher" +import MetaData from "@components/MetaData" +import StatsCard from "@components/Stats/StatsCard" +// import Track from "@components/Stats/Track"; +// import Artist from "@components/Stats/Artist"; +// import AnimatedHeading from "@components/FramerMotion/AnimatedHeading"; +// import AnimatedText from "@components/FramerMotion/AnimatedText"; +import pageMeta from "@content/meta" +// import { SpotifyArtist, SpotifyTrack } from "@lib/types"; +import AnimatedDiv from "@components/FramerMotion/AnimatedDiv" +import { HomeHeading } from '../pages' + type Stats = { - title: string; - value: string; + title: string + value: string }; export default function Stats() { - const { data: topTracks } = useSWR("/api/stats/tracks", fetcher); - const { data: artists } = useSWR("/api/stats/artists", fetcher); + + // const { data: topTracks } = useSWR("/api/stats/tracks", fetcher); + // const { data: artists } = useSWR("/api/stats/artists", fetcher); // const { data: devto } = useSWR("/api/stats/devto", fetcher); - const { data: github } = useSWR("/api/stats/github", fetcher); + const { data: github } = useSWR("/api/stats/github", fetcher) const stats: Stats[] = [ // { @@ -65,7 +67,7 @@ export default function Stats() { title: "Repositories Forked", value: github?.forks, }, - ]; + ] return ( <> @@ -77,25 +79,24 @@ export default function Stats() { />
- -

- These are my personal statistics about my Dev.to Blogs, Github and - Top Streamed Music on Spotify. -

-
+ + +

Here are some statistics about my personal github.

{/* Blogs and github stats */} - {stats.map((stat, index) => ( - - ))} +
+ {stats.map((stat, index) => ( + + ))} +
{/* Spotify top songs */} -
+ {/*
)}
-
+
*/} {/* Spotify top Artists */} -
+ {/*
)}
-
+
*/}
- ); + ) } // Loading Components -function LoadingSongs() { - return ( - <> - {Array.from(Array(10).keys()).map((item) => ( -
-
- #{item + 1} -
+// function LoadingSongs() { +// return ( +// <> +// {Array.from(Array(10).keys()).map((item) => ( +//
+//
+// #{item + 1} +//
-
-
-

-

-
-
- ))} - - ); -} +//
+//
+//

+//

+//
+//
+// ))} +// +// ); +// } -function LoadingArtists() { - return ( - <> - {Array.from(Array(5).keys()).map((item) => ( -
- <> -
- #{item + 1} -
-
-
-

-

-
- -
- ))} - - ); -} +// function LoadingArtists() { +// return ( +// <> +// {Array.from(Array(5).keys()).map((item) => ( +//
+// <> +//
+// #{item + 1} +//
+//
+//
+//

+//

+//
+// +//
+// ))} +// +// ); +// } diff --git a/frontend/posts/10-css-resources-that-you-should-bookmark.mdx b/frontend/posts/10-css-resources-that-you-should-bookmark.mdx deleted file mode 100644 index 67fafec..0000000 --- a/frontend/posts/10-css-resources-that-you-should-bookmark.mdx +++ /dev/null @@ -1,105 +0,0 @@ ---- -slug: 10-css-resources-that-you-should-bookmark -title: 10 CSS Resources that you should bookmark -date: 2021-10-24 -published: true -excerpt: In this article we are going to look at some awesome resource for you that can improve your productivity. -image: https://imgur.com/c5Ir06W.png ---- - -In this article we are going to look at some awesome resource for you that can improve your productivity, I can assure you that you will not regret on clicking this article, so Let's see what I have got for you. - -## 1. Neumorphism - -[Neumorphism](https://neumorphism.io/) generates the soft UI for your `section` or `div` and it can also customize `border-radius`, `box-shadow` and etc. - -![neumorphism](https://i.imgur.com/9Azvowr.png) - -## 2. Shadows Brumm - -[Shadows Brumm](https://shadows.brumm.af/) can generate multiple Layered shadow for you which gives very cool effect and you can customize the color from the curve. - -![shadows brumm](https://i.imgur.com/2wiFPyS.png) - -## 3. CSS Clip-path Maker - -[CSS Clip-path Maker](https://bennettfeely.com/clippy/) can generate beautiful clip-path with various different shapes it can be very handy if you use these king of shapes and properties. - -![clip-path](https://i.imgur.com/RHi4NE3.png) - -## 4. Fancy Border Shape Generator - -[Fancy Border Shape Generator](https://9elements.github.io/fancy-border-radius/) generates most awesome shapes by manipulating `border-radius` and you can use it anywhere in you project. You can also change the size of shape to check how it'll look with your project preference. - -![border-shape](https://i.imgur.com/Mi3hNOb.png) - -## 5. Cubic Curve - -[Cubic Curve](https://cubic-bezier.com/) basically generates the `cubic-bezier` for you animation in css. As we know we use `ease-in`, `ease-out` etc property for the animation to tell the browser what is the animation's flow. you can customize those properties here. - -![cubic-curve](https://i.imgur.com/RdpTDJl.png) - -## 6. CSS Gradient - -If you work with gradient then you will love [cssgradient.io](https://cssgradient.io/). because I am using this for a long time and it's just perfect. and also here you can also get some tools like _Gradient Button_ and many more. - -![gradient](https://i.imgur.com/GXQ30rQ.png) - -## 7.CSS Waves Generator - -According to me these three waves generators that are awesome to generate any kind of waves it could be for you footer or divider section etc. - -### CSS Waves - -[CSS Waves](https://getwaves.io/) generates simple waves with some customization. - -![css-waves-1](https://i.imgur.com/SIACH2I.png) - -### Gradient Multiple Waves - -[Gradient Multiple Waves](https://www.softr.io/tools/svg-wave-generator) can generate multiple gradient waves which is awesome. - -![css-waves-2](https://i.imgur.com/BKLsLsw.png) - -### Multiple Animated Waves - -[Multiple Animated Waves](https://svgwave.in/) can generate multiple gradients waves but the main feature is that it can also generate Live animation for that. - -![css-waves-3](https://i.imgur.com/Qj9Fcft.png) - -## 8. CSS Grid Generator - -### CSS grid - -[CSS grid](https://cssgrid-generator.netlify.app/) generates the awesome css For Grid and you can customize it with `div` and it will also create the child element for that -![css-grid](https://i.imgur.com/SkTnHrb.png) - -### CSS Grid Area - -[CSS Grid Area](https://grid.layoutit.com/) generates the `grid-area` for you. and you can name that and customize the area according to your need. - -![css-grid-area](https://i.imgur.com/Bl6WXfY.png) - -## 9. Loading Animated GIFs/SVGs - -On [loading.io](https://loading.io/) you can generate multiple loading animation and download that as SVG, GIFs, PNG and other formats but the best feature of it is that you can customize these animation to the next level. You should try this. - -![loading](https://i.imgur.com/qApN28O.png) - -## 10. Free Icon Library - -### Flaticons - -[Flaticons](https://www.flaticon.com/) Library have 5.7M+ vector icons. So you can find any possible icons here and you can use it. - -![flaticons](https://i.imgur.com/lm8Tz78.png) - -### icons8 - -[icons8](https://icons8.com/) library also has the vast collection of icons and you can customize them as well, also you can directly use that icon without downloading it. - -![icons8](https://i.imgur.com/8gyhKQb.png) - -## Wrapping up - -I hope you learned something from this article, if yes them thumbs up. There are unlimited resources but I've covered only ten in this article. I'll cover them in the future articles. So consider to Follow. diff --git a/frontend/posts/10-git-commands-everybody-should-know.mdx b/frontend/posts/10-git-commands-everybody-should-know.mdx deleted file mode 100644 index ec9742f..0000000 --- a/frontend/posts/10-git-commands-everybody-should-know.mdx +++ /dev/null @@ -1,208 +0,0 @@ ---- -slug: 10-git-commands-everybody-should-know -title: 10 Git Commands everybody should know -date: 2021-10-20 -published: true -excerpt: In this article there are 10 git commands which will help you in your projects -image: https://imgur.com/17ihNhB.png ---- - -Learning Git isn't easy. There are tons of confusing and complicated commands. Because of this, most people only learn the absolute basics of Git such as adding and committing files. This is only a small part of what you can do with Git. In this article, we'll be looking at 10 Git commands that everyone should know, or maybe you're already familiar with that. - -## 1. Add/Commit All - -One of the most common things you will do when working with Git is adding and committing a bunch of files at once. The standard way to do this is usually running an add command followed by a commit. - - -```bash -git add . -git commit -m "Message" -``` - -This is fine but you can actually combine them together into a one command - - -```bash -git commit -a -m "Message" -#or -git commit -am "Message" -``` - -By using the `-a` flag when committing you are telling Git to add all files that have been modified and then commit them with the message. This runs into issues with new files. Since the `-a` flag only adds modified files it will not add new files or deleted files. Unfortunately, you cannot use commit to add these types of files, but you can use the `-A` flag with add to add them all. - - -```bash -git add -A -git commit -m "Message" -``` - -This using this you can add all the files modified as well as new and deleted files `-A` flag will add all files within the repository. - -## 2. Aliases - -In the previous section, we see that how you can add and commit all files, but it requires two commands and if you wanna do it several times then it hurts so much doesn't it? That is where the Git aliases command comes in. With aliases, you can write your own Git commands that do anything you want. Let's take a look at how you would write an alias for the above add/commit command. - - -```bash -git config --global alias.ac "!git add -A && git commit -m" -``` - -With this simple line, we are modifying our global Git config and adding an alias called `ac` which will run the command `git add -A && git commit -m`. The code looks a little bit confusing, but the result is that I can now run `git ac "Message"` and it will do the full add and commit for me. - -Now, let's look at what's going on. The first part of the command is `git config --global`. This just says we are modifying global Git `config` because as we want this alias to be available in any Git repository after that you can use `git ac` anywhere. - -The next part is `alias.ac`. This says we want to create an alias called `ac` which is stands for auto-commit. you can name it dinosaur if you want. - -Finally, the last part is the full command `!git add - A && git commit -m`. This is just our normal Git command, but we have prefixed the command with an exclamation point `!`. The reason for this is that a Git alias by default assumes that you will be calling one single `git` command, but we want to run two commands. By prefixing our command with an exclamation (`!`) git will not assume we are running one simple command. To explain this further here is an example of creating an alias for `git commit -a -m "Message"` - - -```bash -git config --global alias.ac "commit -a -m" -``` - -This is the single git command via using alias. Git will assume that we are trying to call a single Git command and will add the git for us - -## 3. Revert - -The last commands have been pretty complex so let's look at a really simple command. The `revert` command simply allows us to undo any commit on the current branch. - - -```bash -git revert 486bdb2 -``` - -All you need to do is pass the commit you want to revert to the command and it will undo all changes from that commit. One important thing to note, though, is that this only undoes changes from that exact commit. - -Another important thing to note is that using `revert` does not actually remove the old commit. Instead it creates a new commit that undoes all the changes from the old commit. This is good since it will preserve the history of your repository. - -One useful command that revert the most recent commit - - - -```bash -git revert HEAD -``` - -## 4. Reflog - -Another simple, but useful command is reflog. This command lets you easily see the recent commits, pulls, resets, pushes, etc on your local machine. - -![reflog](https://imgur.com/aWAzDGN.png) - -## 5. Pretty Logs - -Another useful logging command in Git is the log command. This command combined with some special flags gives you the ability to print out a pretty log of your commits/branches. As you know the standard `git log` doesn't look pretty. - - -```bash -git log --graph --decorate --oneline -``` - -For more flags you can [visit here](https://rb.gy/qfnm9m) - -![git log](https://i.stack.imgur.com/39dMf.png) - -## 6. Searching Logs - -You can also use the log command to search for specific changes in the code. For example you can search for the text **A promise in JavaScript** is very similar as follows. - - -```bash -git log -S "A promise in JavaScript is very similar" -``` - -This command returns the commit where I added the article on JavaScript promises since that is the commit where I added this text. - -## 7. Stash - -How many times have you been working on a feature when an urgent bug report comes in and you have to put all your current code on hold. It is very tempting to do a simple add/commit. so you can switch branches to the main branch before fixing the bug. . Instead, the best thing you can do is use a `stash`. - - -```bash -git stash -``` - -This simple command will stash all your code changes, but does not actually commit them. Instead it stores them locally on your computer inside a stash which can be accessed later. - - -```bash -git stash pop -``` - -This command will take all the changes from the stash and apply them to your current branch and also remove the code from the stash. - - -```bash -$ git stash save "" -``` - -This will stash your changes with the message you entered. This can be helpful especially when you have several stashes. - - -```bash -git stash list -``` - -This might show the following list if you have multiple stashes - - -```bash -stash@{0}: On master: Stashed with message1 -stash@{1}: On master: Stashed with message2 -``` - -Now to use the particular stash you can use the following commands: - - -```bash -git stash apply stash@{1} -``` - -Now go and fix your bug without any headache. - -## 8. Remove Dead Branches - -If you are working on any decent sized project odds are your repository has tens or hundreds of branches from previous pull requests. Most of these branches have probably been merged already and are deleted from the remote repository, but they are still on your local machine. This can get annoying when you have hundreds of dead branches on your machine which is where this command comes in. - - -```bash -git remote update --prune -``` - -This command will delete all the tracking information for branches that are on your local machine that are not in the remote repository - -## 9. Git Merge - -Once you're done with development inside your feature branch and tested your code, you can merge your branch with the parent branch. - -_We must first switch to the parent branch using the checkout command._ - - -```bash -git checkout -``` - -Before merging, you must make sure that you update your local parent branch. This is important because your teammates might've merged into that branch while you were working on your feature. We do this by running the pull command - `git pull` - -If there are no conflicts while pulling the updates, you can finally merge your `feature1` branch into the `master` or parent branch. - - -```bash -git merge feature1 -``` - -This will merge the `feature1` branch to `master` if your parent branch is `master` - -## 10. Destroy Local Changes - -Sometimes you make changes and realize that you need to scrap everything you have done so far. This usually isn't a big deal if you haven't committed yet, but if you have made multiple commits it can be hard to exactly remove all changes. This is where the reset command comes in. By running the below command you can wipe out all changes on your local branch to exactly what is in the remote branch. - - -```bash -git reset --hard origin/main -``` - -It will pull the exact code from the remote repository and remove al the changes you have made locally. - -## Conclusion - -There are hundreds of other amazing Git commands I could have covered but these 10 commands are some of my favorites when it comes to really being a power user of Git. Hopefully, at least one of these commands can help you with mastering Git. diff --git a/frontend/posts/5-very-useful-chrome-edge-extensions-for-developers.mdx b/frontend/posts/5-very-useful-chrome-edge-extensions-for-developers.mdx deleted file mode 100644 index 5f2194b..0000000 --- a/frontend/posts/5-very-useful-chrome-edge-extensions-for-developers.mdx +++ /dev/null @@ -1,49 +0,0 @@ ---- -slug: 5-very-useful-chrome-edge-extensions-for-developers -title: 5 very useful chrome/edge extensions for developers -date: 2021-10-28 -published: true -excerpt: In this article we are going to look at some useful chrome extension which can be very handy as a developer. -image: https://imgur.com/VH9d6lS.png ---- - -In this article we are going to look at some useful chrome extension which can be very handy as a developer. So let's dive in. - - -**Note -** _Now you can add chrome extension to your microsoft edge browser_ - -## 1.Buster - -[Buster](https://cutt.ly/GRFxCrv) is a browser extension which helps you to solve difficult captchas by completing reCAPTCHA audio challenges using speech recognition. Challenges are solved by clicking on the extension button at the bottom of the reCAPTCHA widget. - -![buster](https://cutt.ly/wRFxHu3) - -## 2.CSS Peeper - -[2.CSS Peeper](https://cutt.ly/PRFcufb) is a CSS viewer tailored for Designers. Get access to the useful styles with our Chrome extension. Our mission is to let Designers focus on design, and spend as little time as possible digging in a code. - -![css-peeper](https://cutt.ly/sRFcfpx) - -## 3.Coder's Calendar - -[Coder's Calendar](https://cutt.ly/dRFcmYM) Shows a list of live & upcoming coding Contests happening on various popular competitive programming websites with the feature to add these events to your Google Calendar. - -It currently support Codechef, HackerEarth, Hackerrank, Topcoder, Codeforces, CSAcademy, AtCoder, LeetCode, Kaggle, etc. - -![calender](https://cutt.ly/pRFcDxD) - -## 4.Gitako - GitHub file tree - -[Gitako](https://cutt.ly/ERFvr7m) is a file tree for GitHub. It can do more than just that, it has many features such as Instant file search and navigation, Fold source code etc. - -![gitako](https://cutt.ly/sRFvc6r) - -## 5.OneTab - -Whenever you find yourself with too many tabs, click the [OneTab](https://cutt.ly/NRFbqob) icon to convert all of your tabs into a list. When you need to access the tabs again, you can either restore them individually or all at once. - -![one-tab](https://cutt.ly/LRFbyXD) - -## Wrapping up - -So these are some cool and awesome chrome/edge extension that can be very useful to you in daily life. This is gonna be a serious in which I'll provide more useful extension, so follow for more. diff --git a/frontend/posts/6-ways-to-center-a-div.mdx b/frontend/posts/6-ways-to-center-a-div.mdx deleted file mode 100644 index 0f48f24..0000000 --- a/frontend/posts/6-ways-to-center-a-div.mdx +++ /dev/null @@ -1,163 +0,0 @@ ---- -slug: 6-ways-to-center-a-div -title: 6 ways to center a div -date: 2022-01-20 -published: true -excerpt: This article show how you can make Center a child div vertically and horizontally respect to it's parent. There are many ways to do it. These ways are the best to use. -image: https://imgur.com/urN98S0.png ---- - -Yeah, I know we all have struggled with this situation. We want to center a `div` or `child` inside the parent element, but sometimes it won't work or it's hard to do. So now let me introduce to you 6 ways by which you can center a `div` mostly in every situation. - -## Problem - -We have 2 divs `parent` and `child` and we need to center `child` with respect to the `parent`. - - -```html -
-
-
-``` - -
- -Now we know what we want to achieve. So let's see what are the possible solutions for this problem. - -## 1. Using Flexbox - -The Flexible Box Layout Module, makes it easier to design flexible responsive layout structure. - -Apply the following properties to `.parent` will center `.child` horizontally and vertically. - - -```css -.parent { - display: flex; - justify-content: center; - align-items: center; -} -``` - - - -## 2. Using Position - -The `position` property specifies the type of positioning method used for an element (static, relative, fixed, absolute or sticky). We only need relative and absolute. - -Apply following properties to `.parent` and `.child` will center `.child` horizontally and vertically. - - -```css -.parent { - position: relative; -} -.child { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} -``` - - - -## 3. Using CSS grid - -The CSS Grid Layout Module offers a grid-based layout system, with rows and columns. we can center the `child` element with this as well. - -Apply following properties to `.parent` will center `.child` horizontally and vertically. - - -```css -.parent { - display: grid; - justify-content: center; /* Horizontal */ - align-content: center; /* Vertical */ -} -``` - - - -Also there's one other way to use the Grid you can apply the following properties to `.parent`. - - -```css -/* Another Approach */ -.parent { - display: grid; - place-items: center; -} -``` - - - -## 4. Using `margin: auto` on a `flex` item - -Flexbox introduced a pretty awesome behavior for `auto` margins. Now, it not only horizontally centers an element as it did in block layouts, but it also centers it in the vertical axis. - -Apply following properties to `.parent` will center `.child` horizontally and vertically. - - - -```css -.parent { - display: flex; -} - -.child { -margin: auto; -} -``` - - - -## 5. Pseudo-elements on a `flex` container - -Not the most practical approach in the world, but we can also use flexible, empty pseudo-elements to push an element to the center. - -Apply following properties to `.parent` will center `.child` horizontally and vertically. - - - -```css -.parent { - display: flex; - flex-direction: column; -} -.parent::before, -.parent::after { - content: ""; - flex: 1; -} -.child { - /* ...other CSS */ - margin: 0 auto; -} -``` - - - -## 6. `margin: auto` on a `grid` item - -Similarly to Flexbox, applying `margin:` `auto` on a `grid` item centers it on both axes. - - -```css -.parent { - display: grid; -} -.child { - margin: auto; -} -``` - - - -## Wrapping up - -These are not the only solution or the ways to center a child. There are many other ways to achieve the same thing, But I know only these so I shared them with you. diff --git a/frontend/posts/access-local-server.mdx b/frontend/posts/access-local-server.mdx deleted file mode 100644 index 73d64d5..0000000 --- a/frontend/posts/access-local-server.mdx +++ /dev/null @@ -1,51 +0,0 @@ ---- -slug: access-local-server -title: How to Access Local Sever on Other Devices -date: 2022-11-26 -published: true -keywords: web development, javascript, react, localhost, server, access local sever on other devices -excerpt: In this article, I am going to show you how you can access your local/localhost server on any other device (Android or iOS). -image: https://imgur.com/xRR2iOO.png ---- - - - -In this article, I am going to show you how you can access your local/localhost server on any other device (Android or iOS). - -To access your local server, you only one thing **IP Address** of the device where the server is running. Now there could be two scenarios where you are using Windows or Mac. You just need to get the IP Address. Let me show you how: - -Open windows terminal and type `ipconfig` in the command line and you will get the following output: - -![windows IP](https://i.imgur.com/VSkhF2w.png) - -You only need an IPv4 address and as you get it not type this in your mobile browser followed by `PORT`. For instance- - -```bash -# IPv4:PORT -192.168.64.201:3000 -``` -And if you are running any web app on `localhost:3000` then you will be able to see that on your mobile devices also. - -For **Mac users** you have to do the same- -- Get the IPv4 (use `ifconfig` command) -- Put that on your mobile device followed by `PORT` - -And hurray!!! - -If you are unable to fine the mac IP address then you can take a look the following article: - -[How to Find Your IP Address on a Mac](https://www.wikihow.com/Find-Your-IP-Address-on-a-Mac) - -Following is the Video demo of how it works: -![Demo](https://imgur.com/QEAyiwr.gif) - - -**🌐 Connect with me:** - -[Twitter](https://twitter.com/j471n_) -[Github](https://github.com/j471n) -[Instagram](https://www.instagram.com/j471n_/) -[Newsletter](https://www.getrevue.co/profile/j471n) -[LinkedIn](https://www.linkedin.com/in/j471n/) -[Website](https://j471n.in/) -[Buy me a Coffee](https://buymeacoffee.com/j471n) diff --git a/frontend/posts/active-navbar-next-js.mdx b/frontend/posts/active-navbar-next-js.mdx deleted file mode 100644 index b051245..0000000 --- a/frontend/posts/active-navbar-next-js.mdx +++ /dev/null @@ -1,157 +0,0 @@ ---- -slug: active-navbar-next-js -title: Active Navbar with Next.js Routes -date: 2022-05-26 -published: true -excerpt: This is the walkthrough of how you can make an active navigation bar by using Next.js router and it'll highlight the current page user visiting in the navbar. -image: https://imgur.com/Cb2pzWn.png ---- - -Active Navbar means that when the user is on the `about` page, the `about` link should be highlighted in the navbar. and when the user is on a different page and its link is available in the navbar, that should be highlighted. - -If you don't get it then the following is the demo of what I am talking about and how it's gonna turn out- - -## Preview - -![preview](https://imgur.com/ewfEjcH.png) - -As you can see in the above demo when the user clicks any of the navbar sections the page route changes and the active page name is highlighted on the navbar. That's what are we going to build. - -## Creating Layout - -If you don't have a Layout then you need to create a layout first. and Add that `Layout` component to the `_app.js`. - - -```jsx {1,5} -import Navbar from "./Navbar"; -export default function Layout({ children }) { - return ( - <> - - {children} - {/* Footer */} - {/* You can add more things here */} - - ); -} -``` -In the above code, you can see that we are importing `Navbar` and then we are rendering the `Navbar` inside the `Layout`. - -Now, after creating Layout we need to wrap our whole app with Layout. It should look something like this- - - -```jsx {2,5,7} -import "../styles/global.css"; -import Layout from "./../components/Layout"; -function MyApp({ Component, pageProps }) { - return ( - - - - ); -} -export default MyApp; -``` - -## Creating Navbar - -Create a file name `Navbar.jsx` in the components folder. and first, import the `Link` and `useRouter` as we need those to check the current page route. - - -```jsx {1.2} -import { useRouter } from "next/router"; -import Link from "next/link"; -``` - -Then we need to define all the routes which we have and want to show on the navbar. - - -```jsx {4} -import { useRouter } from "next/router"; -import Link from "next/link"; - -const navigationRoutes = ["home", "about", "pricing", "contact"]; -``` - -Then we need to loop through `navigationRoutes` and render the elements. - - - -```jsx {6-22} -import { useRouter } from "next/router"; -import Link from "next/link"; - -const navigationRoutes = ["home", "about", "pricing", "contact"]; - -export default function Navbar() { - const router = useRouter(); - return ( - - ); -} -``` - - -In the above code, we are defining the `router` and then we are creating a `nav` container. After that, I am mapping `navigationRoutes` and for each route, we are returning `NavigationLink` which we will create in a minute. -**Props: ** -- `href`: route link -- `text`: text that will be displayed on the navigation bar -- `router`: verify the current route - - -```jsx {24-36} -import { useRouter } from "next/router"; -import Link from "next/link"; - -const navigationRoutes = ["home", "about", "pricing", "contact"]; - -export default function Navbar() { - const router = useRouter(); - return ( - - ); -} - -function NavigationLink({ href, text, router }) { - const isActive = router.asPath === (href === "/home" ? "/" : href); - return ( - - - {text} - - - ); -} -``` - - -In this, we check if the current router path is the same as the `href` then return `true` for `isActive` and if the current route is active then apply the `nav_item_active` class. - -This is all we need to create an active navigation bar and it works flawlessly. You can check the live demo on the following sandbox. - - diff --git a/frontend/posts/battery-api.mdx b/frontend/posts/battery-api.mdx deleted file mode 100644 index d6d3de3..0000000 --- a/frontend/posts/battery-api.mdx +++ /dev/null @@ -1,215 +0,0 @@ ---- -slug: battery-api -title: How to use Battery Status API? -date: 2021-12-06 -published: true -excerpt: In this article, we are gonna build a Battery Informer which will display the battery status and the other information along with it such as the charging status, charging level and the discharging time. -image: https://imgur.com/jodmfWd.png ---- - -In this article, we are gonna build a Battery Informer which will display the battery status and the other information along with it such as the charging status, charging level and the discharging time. Let's first look at what are we building - - -## Preview -![preview](https://i.imgur.com/vG6yTvB.gif) - - - -Now you know how it will look like, So let's look at the code now - - -## HTML - - -```html -
-
- -
-

- charging -
- -
- -
-

Discharging :

-
-
-
-
-``` - -In the HTML code, the `battery` class is the main container and it has three different section - -- `charging_info` : it shows the battery level and the charging icon -- `charging_bar` : it is the bar to represent the battery level -- `other_info` : it shows the `discharging_time` - -Now let's look at the CSS - - -## CSS - - -```css -/* Battery main Container */ -.battery { - display: flex; - align-items: center; -} - -/* Battery main Container */ -.main_container { - position: relative; - background: #fff; - width: 300px; - height: 150px; - padding: 4px; - border-radius: 15px; -} -.right_bar { - width: 10px; - height: 75px; - border-radius: 15px; - background: white; - margin-left: 1px; -} - -/* main charging bar */ -.main_container > .charging_bar { - position: relative; - background: limegreen; - border-radius: 15px; - width: 0; - height: 100%; - z-index: 9; - animation: animate 2s linear; -} - -/* the charging animation from the left */ -@keyframes animate { - 0% { - width: 0; - } -} - -/* Charging information such as battery % and charging Icon */ -.main_container > .charging_info { - position: absolute; - content: ""; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - z-index: 10; - font-size: 60px; - width: 100%; -} - -/* Charging Icon */ -.charging_info > img { - width: 35%; - display: none; -} - -/* Other information such as discharging time */ -.other_info { - position: absolute; - inset: 12px; - z-index: 10; - display: none; -} -``` - -Now the main part is the javascript in order to run this properly. - -## Javascript - - -```js -// All the containers we need to update the battery information -const chargingIcon = document.querySelector(".charging_icon"); -const batteryLevel = document.querySelector(".battery_level"); -const chargingBar = document.querySelector(".charging_bar"); -const dischargingTime = document.querySelector(".discharging_time"); -const otherInfo = document.querySelector(".other_info"); - -// Getting battery it returns a propmise -navigator.getBattery().then((battery) => { - /* Update all the battery information which is a combination of multiple functions */ - function updateAllBatteryInfo() { - updateChargeInfo(); - updateLevelInfo(); - updateDischargingInfo(); - } - - // Running as the promise returns battery - updateAllBatteryInfo(); - - // Event Listener, when the charging status changes - // it checks that does your device is plugged in or not - battery.addEventListener("chargingchange", function () { - updateAllBatteryInfo(); - }); - - // Event Listener, when the Battery Level Changes - battery.addEventListener("levelchange", function () { - updateAllBatteryInfo(); - }); - - // Event Listener, when the discharging Time Change - // it checks that does your device is plugged in or not - battery.addEventListener("dischargingtimechange", function () { - updateAllBatteryInfo(); - }); - - // Updating the battery Level container and the charging bar width - function updateLevelInfo() { - batteryLevel.textContent = `${parseInt(battery.level * 100)}%`; - chargingBar.style.width = `${parseInt(battery.level * 100)}%`; - } - - function updateChargeInfo() { - /* - if the device is plugged in - - changing the Animation Iteration Count to infinite - - showing the charging Icon - - Hiding the other information - else - - changing the Animation Iteration Count to initial - - hiding the charging Icon - - showing the other information - */ - - battery.charging - ? ((chargingBar.style.animationIterationCount = "infinite"), - (chargingIcon.style.display = "inline-flex"), - (otherInfo.style.display = "none")) - : ((chargingIcon.style.display = "none"), - (otherInfo.style.display = "inline-flex"), - (chargingBar.style.animationIterationCount = "initial")); - } - - // updating the Discharging Information - function updateDischargingInfo() { - const dischargeTime = parseInt(battery.dischargingTime / 60) ? true : false; - dischargeTime - ? ((dischargingTime.textContent = `${parseInt( - battery.dischargingTime / 60 - )} minutes`), - (otherInfo.style.display = "flex")) - : (otherInfo.style.display = "none"); - } -}); -``` - -**Note -** `dischargeTime` will not show if it is null/infinity, and in mobile devices it mostly infinity so to view that in action you should use laptop/desktop. - - - -## Wrapping up - -This shows the battery information of your device. you can use this on your website to show the battery status of the users. diff --git a/frontend/posts/before-and-after-in-tailwind-css.mdx b/frontend/posts/before-and-after-in-tailwind-css.mdx deleted file mode 100644 index edf514f..0000000 --- a/frontend/posts/before-and-after-in-tailwind-css.mdx +++ /dev/null @@ -1,42 +0,0 @@ ---- -slug: before-and-after-in-tailwind-css -title: How to use ::before and ::after in Tailwind CSS -date: 2021-10-22 -published: true -excerpt: This article explains how you can use `::before` and `::after` selectors in Tailwind CSS -image: https://imgur.com/hKvmXEn.png ---- - -In this article we are going to learn that how you can use `::before` and `::after` selectors in Tailwind CSS. If you don't know what [Tailwind CSS](https://tailwindcss.com/) is then you learn it because it is just awesome. And trust me If you got used to it then you could not leave. - -## Introduction -Now let's continue with this article. This feature is only available in [Just-in-Time (JIT)](https://tailwindcss.com/docs/just-in-time-mode) mode. Tailwind has first-party support for styling pseudo-elements like `before` and `after`: - - -```html -
-``` - -## Use -It has `content: ""` by default so you don't need to worry about that. Any time you use a `before` or `after` variant to make sure the elements are rendered, but you can override it using the content utilities which have full arbitrary value support: - - -```html -
-``` - -You can even grab the content from an attribute using the CSS `attr()` function: - - -```html -
-``` - -This can be super helpful when your content has spaces in it since spaces can't be used in CSS class names. You can use mostly any property with `before` and `after` - -## Wrapping up - -Tailwind is the super awesome framework of CSS. According to me, it's the best so far. Everyone should learn it and it is very easy to learn and use. For more such articles consider a Follow. diff --git a/frontend/posts/ce-dec-2022.mdx b/frontend/posts/ce-dec-2022.mdx deleted file mode 100644 index 3cfccd2..0000000 --- a/frontend/posts/ce-dec-2022.mdx +++ /dev/null @@ -1,132 +0,0 @@ ---- -slug: ce-dec-2022 -title: Chrome Extensions of the Month - December 2022 -date: 2022-12-25 -keywords: css, web dev elopment, productivity, chrome, chrome extensions, chrome extensions of the month, december -published: true -excerpt: In this article, we are going to look at some of the best extensions that you need to install for better productivity. So without further due, let's get into it. -image: https://imgur.com/3idQhwF.png ---- - -In this article, I will suggest to you some of the best extensions you need to install for better productivity that can come in very handy. So without further due, let's get into it. - - -## NOVA (new tab) - -[Nova](https://chrome.google.com/webstore/detail/nova-new-tab-with-theme/cmfhopmhaagcfnjflfppceclmkenjkpc?hl=en) is one of the best new tab extensions for Chrome with a minimalistic, light and dark theme, and a fully customizable New Tab page with a built-in to-do list, clock, calendar, Pomodoro, notes, RSS reader, weather and news. - -![Nova](https://lh3.googleusercontent.com/N4k6E3gMxvNjoD7sVc-Ol_xom1pu9EskMQkqjEaU0jxCc2Y61e_A4LHAY3Scb0C9YTxYGwMJTLLPkIm9ZPFGXQ413A=w640-h400-e365-rj-sc0x00ffffff) - -## DeepL Translate - -[DeepL Translate](https://chrome.google.com/webstore/detail/deepl-translate-reading-w/cofdbpoegempjloogbagkncekinflcnj?hl=en) lets you translate from one language to another. Sounds similar? Yes. It sounds like Google translate. The main difference is that it has tons of features such as- - -* Translate while you are writing. -* Translate while you are reading. -* Full-page translation (For PRO users). -* It supports 26 languages. -* If you visit their [website](https://www.deepl.com/translator/files) then you can even translate the whole document (.pdf, .docx, .ppt) - - -![DeepL](https://lh3.googleusercontent.com/dRjn1llyJu3mnxLmswS_45_vPUP294BM26ntwWjlV7FRDg6U0imp_H7Da3FLRYyeUQQIww2ZGMvL4RGyO0Lwc_Z5ng=w640-h400-e365-rj-sc0x00ffffff) - -## StayFree - -[StayFree](https://chrome.google.com/webstore/detail/stayfree-website-blocker/elfaihghhjjoknimpccccmkioofjjfkf) - Web Analytics & Screen Time Tracker is analytics, self-control, productivity, and web addiction controller extension. StayFree provides analytics to help you understand how you are using the internet (daily website usage statistics), and factors leading to distractions (such as advertisements you have seen), and focus your time by restricting the usage of distracting websites. - -![StayFree](https://lh3.googleusercontent.com/zh-YyATWuiMVr_DnbzsTMnMxADm2KKhej9BPrqOjWjZxl6m4FbWFUVdiXjCfzxg2SyZNxkyI6Ag3hbQsO8M0w8vi8A=w640-h400-e365-rj-sc0x00ffffff) - -## Listly - -[Listly](https://chrome.google.com/webstore/detail/listly-free-data-scraper/ihljmnfgkkmoikgkdkjejbkpdpbmcgeh/related) converts any website to Excel in just a few clicks. Speed up your data collection and put your time and effort into what really matters to you. You can also Schedule a daily extraction. - -![Listly](https://lh3.googleusercontent.com/Q21Y01w6-20bwGnC-dhXTuDm0if_vaiyjbVh1q4g1rRXNRx-9OzlYGaGT52tiEX36JCIDNkf6AR6iLIp80ujswoasso=w640-h400-e365-rj-sc0x00ffffff) - -![Listly](https://lh3.googleusercontent.com/JWOsf3-HJmbDqTWySRFqkik_5z5CyzJGfjA3uuv2yCZLbJLB2n5NWC13kh57TKCRp2hfQz_MD0NBMlavUnqi-TmN0Jo=w640-h400-e365-rj-sc0x00ffffff) - -## Minimal Theme for Twitter - -[Minimal Theme for Twitter](https://chrome.google.com/webstore/detail/minimal-theme-for-twitter/pobhoodpcipjmedfenaigbeloiidbflp) refines and cleans up the Twitter interface, and adds useful features and customizations: - -* Remove the distracting trends sidebar -* Customize your Timeline width -* Remove Timeline and Tweet borders for a more minimalistic look -* Customize the left navigation -* Hide vanity counts under tweets -* Remove promoted posts -* Remove "Who to Follow" and other suggestions -* Hide the Search Bar -* Hide the Tweet button -* Hide View Counts - - -![Minimal](https://lh3.googleusercontent.com/DcJtUQTRtVNJ_fjfLehL9L2QYO9Ny2jTdeir8T9OfPX8gECS4h5aA_onLsA75-grngfwRN7VTcWWEVtWjbFpSDfdHQQ=w640-h400-e365-rj-sc0x00ffffff) - -![Minimal](https://lh3.googleusercontent.com/1I--BWRsXOeaoXM9F313pPJSzlyQZVXx6FxhjlDMZHXgpXkVnPzn5SByIUQ6h8RWwcFDXLz5bZPjDmbKUAyjlRYPK1s=w640-h400-e365-rj-sc0x00ffffff) - -## Robots Exclusion Checker - -[Robots Exclusion Checker](https://chrome.google.com/webstore/detail/robots-exclusion-checker/lnadekhdikcpjfnlhnbingbkhkfkddkl) is designed to visually indicate whether any robots exclusions are preventing your page from being crawled or indexed by Search Engines - -**The extension reports on 5 elements:** - -* Robots.txt -* Meta Robots tag -* X-robots-tag -* Rel=Canonical -* UGC, Sponsored and Nofollow attribute values - - -![Robots](https://lh3.googleusercontent.com/56vQxRGPdh8ITUV2pjfowShuPqyMiVh_VZNgQI7jjP28KXksrZWMmTno3fxdPdLmW4VVLdcJe_b9FKZs2iHJQJHX4w=w640-h400-e365-rj-sc0x00ffffff) - -## Motion DevTools - -[Motion DevTools](https://chrome.google.com/webstore/detail/motion-devtools/mnbliiaiiflhmnndmoidhddombbmgcdk) is a browser extension to inspect, edit and export animations made with CSS and Motion One. I highly recommend you use this extension if you work with animations. - -![Motion](https://lh3.googleusercontent.com/v1xDwsqk7daYdyuMiWDWXb5U8Y14WjdaH_3NY_0pF0PPdDl1hAcSwqeThLYTX3-83YX59IWnBxDQibbptZcxzB9S6w=w640-h400-e365-rj-sc0x00ffffff) - -## Chroma - -[Chroma](https://chrome.google.com/webstore/detail/chroma-ultimate-eyedroppe/pkgejkfioihnchalojepdkefnpejomgn) is a free, user-friendly color tool that allows you to easily sample colors from your screen while browsing the internet. It is highly intuitive and simple to use. - -![Chroma](https://lh3.googleusercontent.com/gUo_414NWXSYgLxXTqa2qUW-Mpys4wtGZwW9Gdx_lR25gNZAfMZLQs73jtIWhp1xsOSlzcTD1z5DM8odvfuR9B_bDt0=w640-h400-e365-rj-sc0x00ffffff) - -## Monaco Markdown Editor For GitHub - -[Monaco Markdown Editor For GitHub](https://chrome.google.com/webstore/detail/monaco-markdown-editor-fo/mmpbdjdnmhgkpligeniippcgfmkgkpnf?hl=en) is an open-source extension that replaces all GitHub text areas for authoring markdowns with a monaco editor. - -**Features:** - -* Syntax Highlighting of Markdown and Code Snippets -* Tab to indent and Shift+Tab to outdent entire selections -* Multi-Cursor Editing - - -![Monaco](https://lh3.googleusercontent.com/WhtCPBpnXxFfSIOBKSWPePYuPLqnVITYJ7l0a_wCZf-6j2s0HQHN_lpM5EL-HVzJVvqhqLVOLk67VSQl5JhIGoyJ8w=w640-h400-e365-rj-sc0x00ffffff) - -## Dopely Colors - -[Dopely Colors](https://chrome.google.com/webstore/detail/dopely-colors/likjnedfkpkabglldlnelmochinjpbjm?hl=en) is a package of advanced color tools that we believed to be essential for every creator. - -**Features:** - -* Color Picker -* Color Gradient Generator -* Color Tab -* Color Contrast Checker -* Color Blindness Simulator -* Color Toner -* Save & Export your colors - - -![Dopely Color](https://lh3.googleusercontent.com/KY37hcI_qnH4M1ukGF2sw6dDJOEhZgYLScy71XoZ8DDsnz7yxFSEnfWJxs6HtGTXCpwcmLgFQpFTwcIqNxHH7H-P=w640-h400-e365-rj-sc0x00ffffff) - -![Dopely Color](https://lh3.googleusercontent.com/XS9LCeYkrQ8jy_D_P7cjeawB9_m44Cl1M-uqk2-caQVYaOmt6jkkVyxNruVkF8EM0Bfsd-_f5CKHa8bAwuqd-hK3iVo=w640-h400-e365-rj-sc0x00ffffff) - -![Dopely Color](https://lh3.googleusercontent.com/VUFq_7ZmnyomOVKyc4w7wYLX7i1wBCcOShErhiwptsLXbT_9uI6zhxTAPvrwcPWUUl5V5Ap9vvj_KGcygWfh1mEpnw=w640-h400-e365-rj-sc0x00ffffff) - -*** - -**Wrapping up** - -These were some extensions for this month (November 2022). I have personally used all the above extensions and from my experience, every extension is worth installing. \ No newline at end of file diff --git a/frontend/posts/ce-feb-2023.mdx b/frontend/posts/ce-feb-2023.mdx deleted file mode 100644 index 5661e48..0000000 --- a/frontend/posts/ce-feb-2023.mdx +++ /dev/null @@ -1,100 +0,0 @@ ---- -slug: ce-feb-2023 -title: Chrome Extensions of the Month - February 2022 -date: 2028-02-26 -keywords: css, web dev elopment, productivity, chrome, chrome extensions, chrome extensions of the month, february -published: true -excerpt: In this article, we are going to look at some of the best extensions that you need to install for better productivity. So without further due, let's get into it. -image: https://imgur.com/YnvREmY.png ---- - -In this article, I will suggest to you some of the best extensions you need to install for better productivity that can come in very handy. So without further due, let's get into it. - - -## OSlash - -[OSlash](https://chrome.google.com/webstore/detail/oslash-text-expander-and/gpljeioabgadbidbkcbhjjglpinfhmal) is a free, top-rated Text Expander and Custom Keyboard Shortcut for you and your team. Using OSlash Text Expander, insert text templates everywhere you work in apps such as Gmail, LinkedIn, Intercom, and Google Docs. - -![](https://lh3.googleusercontent.com/ZaX5XnMfQqOhUKCv70fasHxHhyWb45kbyWugVSkQS-fzHul8AhFZa3v_hqt6lTmDzSMV-zhHI510uNGm58pTkntjUg=w640-h400-e365-rj-sc0x00ffffff) - -![](https://lh3.googleusercontent.com/gI4cs3csNFDpwF4Xw-q6-41QjCZgUFkbigVygaKRl8Ikt5uKE03HS3nHwtJl02Dr2HRdhIOUiG2gGaPuZ8kY5jnq=w640-h400-e365-rj-sc0x00ffffff) - -## Auto Group Tabs - -[Auto Group Tabs](https://chrome.google.com/webstore/detail/auto-group-tabs/ekjmngjikhikadcbbidpfdaocifddaip) Keeps your tab bar tidy with auto grouping without the need to group them manually. Group tabs automatically based on URL. - -![](https://lh3.googleusercontent.com/A2VGNAqOiXNevP_7140NiW4IsOzTM3axTVQ1E6WTwa13w9MI7TBGfW6jAR8fKcmnwY8HxKBrJJ2Qa2roFAWnbd7ZRw=w640-h400-e365-rj-sc0x00ffffff) - -## Validity - -[Validity](https://chrome.google.com/webstore/detail/validity/bbicmjjbohdfglopkidebfccilipgeif) can be used to quickly validate your HTML documents from the toolbar. Just click the icon in the toolbar to validate the current document without leaving the page. The number of validation errors can be seen in the tool tip and the detail can be seen in the console tab of Chrome's developer tools. - -![](https://lh3.googleusercontent.com/CMxzRA-jU3mckPzZpF5DWkhhzLjHR3KEleh_5fBKIckD7QMGJ9z6qNME65Bl1h2qQrtFzfal1O9cEX6qHWiTn3wg4w=w640-h400-e365-rj-sc0x00ffffff) - -## Find+ - -[find+](https://chrome.google.com/webstore/detail/find%2B-regex-find-in-page/fddffkdncgkkdjobemgbpojjeffmmofb) is a powerful Find-in-Page extension for Google Chrome allowing you to search a web page or document by regular expression. It has been designed to look and behave much like the native CTRL+F tool, but extended with various useful features. - -![](https://lh3.googleusercontent.com/IjiJrnzn44W01uIHsNmUP1s8FxqueTAAAxR-PlgDGgbDwuE_qskZ9RI98vHo5x1Sry70mziQ90B4qCTM74c-5_z2fw=w640-h400-e365-rj-sc0x00ffffff) - -## Gimli Tailwind - -[Gimli Tailwind](https://chrome.google.com/webstore/detail/gimli-tailwind/fojckembkmaoehhmkiomebhkcengcljl) lets developers debug and work with Tailwind CSS more intuitively. This is a DevTools extension enabling smart tools for Tailwind CSS. - -![](https://lh3.googleusercontent.com/ADqozONu5ski6YRXrRQCrD-bxMg8dESx_uBn4c-APAe9DNs_xjl9PQlgDT87CGZbntGuuPVxuLsh_z-MEO88MTZGSA=w640-h400-e365-rj-sc0x00ffffff) - -## Eesel - -All your Google Docs, Notion pages and other work documents, right in your new tab. Your team creates many work docs in many different apps. A project brief in Google Docs, a timeline in Notion, a mockup in Figma. It can be an exhausting game of trial and error to find the links you need, and that's where [eesel](https://chrome.google.com/webstore/detail/eesel-the-new-tab-for-wor/jffaiidojfhflballoapgofphkadiono) comes in. - -![](https://lh3.googleusercontent.com/iMe0G5-irvmBhAQ0YK3Pbs5YkqU4CkGmLzL27rvN0TROoMjyuqkAcNMiZHgffvtoK_1D-z25-jOqXVkpSp9MG_oJcQ=w640-h400-e365-rj-sc0x00ffffff) - -## Buffer - -[Buffer](https://chrome.google.com/webstore/detail/buffer/noojglkidnpfjbincgijbaiedldjfbhh?hl=en) is the best way to share great content to Social Networks from anywhere on the web. The Buffer Chrome extension allows you to schedule posts to Buffer ([https://buffer.com](https://buffer.com)). By using this tool, you will be able to create and schedule your social media content faster from anywhere on the web. As we add updates to [Buffer.com](http://Buffer.com), you’ll automatically get the new features here, too! - -![](https://lh3.googleusercontent.com/uIGzRAPt9IhTfNXzOPtGp74N9iCaJv6lrXH1lo-t2tMTXt9EZzQ2u2epTsWKZseMStTbv5TQRLXCKTCwNhfXHkfc6n8=w640-h400-e365-rj-sc0x00ffffff) - -## DuckDuckGo Privacy Essentials - -[DuckDuckGo Privacy Essentials](https://chrome.google.com/webstore/detail/duckduckgo-privacy-essent/bkdgflcldnnnapblkhphbgpggdiikppg?hl=en) is used for protecting your privacy online. Locking the front door won’t stop the most determined folks from getting inside, especially if you’ve left the back door and windows unlocked and an extra key under the doormat. That’s why we offer multiple types of privacy protection, all with the single purpose of better protecting your privacy in Chrome. - -![](https://lh3.googleusercontent.com/4ejTSepN79f8uaOChkHup6-J-7Vb3J7p2MFGXe1gjUifym-uc36MdEBjCFmiyZEUTnzrXXV7zacSHPyAEM_OPYoLug=w640-h400-e365-rj-sc0x00ffffff) - -![](https://lh3.googleusercontent.com/iYtWe5HNmWszwt-SCjvgKodWwnF4WBuvMkn2tDH5k-chANe6MJRB98Wh7RY73hDGQyQVaI2htZkx104aSF2U_IpD=w640-h400-e365-rj-sc0x00ffffff) - -## Save image as Type - -[Save image](https://chrome.google.com/webstore/detail/save-image-as-type/gabfmnliflodkdafenbcpjdlppllnemd?hl=en) as PNG, JPG or WebP by context menu on image. Add context menu for images to save image as PNG, JPG or WebP format. - -![](https://lh3.googleusercontent.com/GxY1qe6ENSpwtiw9-Bp0mtLQ38uQQQWgXntYWlIjT2aW3cLBYS2OF2_Xdggi92HmSoAkD8eSFhKxV-HdM7qjFhifZg=w640-h400-e365-rj-sc0x00ffffff) - -## Awesome Screenshot and Screen Recorder - -[Awesome Screenshot and Screen Recorder](https://chrome.google.com/webstore/detail/awesome-screenshot-and-sc/nlipoenfbbikpbjkfpfillcgkoblgpmj) is the best screen recorder and screen capture & screenshot tool to record screens. - -**Features** - -* Provide stable service for more than 10 years - -* Loved by more than 3 million users across different platforms 👍 - -* ️Local Screen Recorder & Cloud Screen recorder 2 in 1 - -* Screenshot / Screen capture & Screen recorder 2 in 1 - -* Instant sharing of your screenshots and screen recordings - - -![](https://lh3.googleusercontent.com/5_RfvhY7bsBguxe9N4pHb55YllfHCIgIPBSPLhlScHF-Q9Jy5yjsy30k6FxOPjrDLeJuXChIWo8R3L2EtrP60Eso=w640-h400-e365-rj-sc0x00ffffff) - -![](https://lh3.googleusercontent.com/GqtBc7wJxq7yBsJ-kO6tTSRct6hypcgclj_m7CkepgZUMgAZh4LPk_XsSyxauEIJGYcRE7tIOTmun_wNBU_yaEtNig=w640-h400-e365-rj-sc0x00ffffff) - -![](https://lh3.googleusercontent.com/q6WVCMxriY4MRj6RYUd72TOfwc6T8dJ-1eHMYEPBNnxeAUyB--ApWQbEX3e2UF7JhfNngn4494IigX9Aeu_rIueA-uQ=w640-h400-e365-rj-sc0x00ffffff) - - -*** - -**Wrapping up** - -These were some extensions for this month (February 2023). I have personally used all the above extensions and from my experience, every extension is worth installing. diff --git a/frontend/posts/ce-jan-2023.mdx b/frontend/posts/ce-jan-2023.mdx deleted file mode 100644 index 8046b91..0000000 --- a/frontend/posts/ce-jan-2023.mdx +++ /dev/null @@ -1,131 +0,0 @@ ---- -slug: ce-jan-2023 -title: Chrome Extensions of the Month - January 2023 -date: 2023-01-28 -keywords: css, web development, productivity, chrome, chrome extensions, chrome extensions of the month, January, 2023 -published: true -excerpt: In this article, we are going to look at some of the best extensions that you need to install for better productivity. So without further due, let's get into it. -image: https://imgur.com/2ZmhZAx.png ---- - - - -In this article, I will suggest to you some of the best extensions you need to install for better productivity that can come in very handy. So without further due, let's get into it. - - - - -## ChatGPT for Google - -[ChatGPT for Google](https://chrome.google.com/webstore/detail/chatgpt-for-google/jgjaeacdkonaoafenlfkkkmbaopkbilf) displays ChatGPT response alongside search engine results This is an open-source extension that shows the response from ChatGPT alongside Google, Bing, DuckDuckGo and other search engines - -**Features:** - -* Supports all popular search engines - -* Access ChatGPT from the extension popup - -* Markdown rendering - -* Code highlights - -* Dark mode - -* Feedback to improve ChatGPT - -* Custom trigger mode - -* Copy to clipboard - - -![](https://i.imgur.com/XuvoSvm.png) - -## Summarize - -With the [Summarize](https://chrome.google.com/webstore/detail/summarize/lmhkmibdclhibdooglianggbnhcbcjeh) extension, you can get the main ideas of any page in just one click, without leaving the page. Whether you're reading news, articles, blogs, or research reports, Summarize have you covered. - -![](https://lh3.googleusercontent.com/B0l-_JbAcYsmMqLSmffyrr0DTpn7I-0-T_0A01uyIU-ofN3YSxt86NNrJecOmxqvMHwaM7igxQBxNEuTn45H6QrwuHY=w640-h400-e365-rj-sc0x00ffffff) - -## Flonnect - -[Flonnect](https://chrome.google.com/webstore/detail/screen-webcam-recorder-fl/lkeokcighogdliiajgbbdjibidaaeang) extensions allow Screen Recording from your Webcam and Desktop Record your screen or camera along with your audio. This extension supports all types of screen recordings for free with unlimited usage. - -* Free to Use - -* No Watermark - -* No Lag screen recording - -* Unlimited Usage - -* Includes audio recording with Webcam - - -![](https://lh3.googleusercontent.com/5Pe5nDOoCp5LMQlPOpuJph5cpYTi_H2WwLZ3_jSO4tBGrbXlw77XmfadEg9mqqPrznVzEs1XSIkSMs8g-rcJLsBSkg=w640-h400-e365-rj-sc0x00ffffff) - -## Hackertab - -[Hackertab](https://chrome.google.com/webstore/detail/hackertabdev-developer-ne/ocoipcahhaedjhnpoanfflhbdcpmalmp) allows you to customize your default tab page that includes news, tools and events from top sources such as GitHub Trendings, Hacker News, DevTo, Medium, and Product Hunt. No matter what type of developer you are, you'll find valuable and relevant information here. - -![](https://lh3.googleusercontent.com/rQiOXCi1evWhjOOOCaoM5hWmE3RUMbKqaqcV70Jf0VCAzH5pkAUsYcvRqFMzdNjg8UsJP9P0f9VYQ32eppTtTHo8YQ=w640-h400-e365-rj-sc0x00ffffff) - -## SuperDev - -[SuperDev](https://chrome.google.com/webstore/detail/superdev-design-and-dev-t/jlkikimlceonbmfjieipbonnglnlchhl) is a browser toolbox for designers, developers and techies that minimizes the development/designing time and provides various tools to debug the web without any hassle. It contains 18 tools for developers so you won't have to install a separate extension for each one. - -![](https://lh3.googleusercontent.com/ALEstpr8U-EHKVwsvkkPW6Qz5CU5s61gVaICjoTQf8gcCHYiPpmEg4CrnTmB9CxIshkOUwMXkly_JIMnH6eJ-OIyV2o=w640-h400-e365-rj-sc0x00ffffff) - -![](https://lh3.googleusercontent.com/X35Vh8GIh3SQ6dkgEDpRLHPbWrMx3vA7MGXrhUuBVKrQ56a-Kum9Xye8tMINePOram94F73yJYuie84QHszQ8EQ7=w640-h400-e365-rj-sc0x00ffffff) - -## GitHub Writer - -[GitHub Writer](https://chrome.google.com/webstore/detail/github-writer/diilnnhpcdjhhkjcbdljaonhmhapadap) provides all the features available in the GitHub plain-text editor, including Markdown input. For features like tables, it offers a much easier experience in comparison to plain-text Markdown and allows users to be more productive. No more switching to the Preview tab to see what you write! - -![](https://lh3.googleusercontent.com/f1pqeSNUeStwkah579AzvJqU6T9TxtaUGePWtSdMVrSSN-08dZdJrog10t3ZLJmX_-8OcDUTDrtyoEgvhX7HKJPL=w640-h400-e365-rj-sc0x00ffffff) - -## Querio - -[Querio](https://chrome.google.com/webstore/detail/querio-graphql-and-xhr-re/ojealanebldmhejpndmaenfcdpengbac) adds the Missing DevTools network inspector for GraphQL and XHR requests. - -* Querio intercepts and displays GraphQL and XHR requests in a nicer and more dev-friendly way than the built-in Network panel - -* Each request made on the page is beautifully formatted and highlighted - -* Search data in request/response - -* Filtering by request type and searching by GraphQL query/mutation name - -* Dark and Light themes - -![](https://lh3.googleusercontent.com/Pb-yfu0uUl3jQs6KWY8ccVQyy0u-VRt0xcsPgTdoLcciRnIzMmVGto7XeWHatXRNeb1vKVXptK1YDjYO4CIfY9EdOg=w640-h400-e365-rj-sc0x00ffffff) - - -![](https://lh3.googleusercontent.com/xovxnw6CzIso_AZ5RfvNYJosiEYtF5lpqXQOy0KW8KofQoLOhxPOSH5IgrAx1Y16XwLl5VJipDEuPrfbi4yq3cXmRro=w640-h400-e365-rj-sc0x00ffffff) - -## Little Star - -[Little Star](https://chrome.google.com/webstore/detail/little-star-github-stars/kmjfdonmflchjdlmeoecbmebfpnafpec) comes from the idea of categorizing projects directly when clicking the GitHub Star button, and I want to manage and local my GitHub stars quickly. So building a browser extension with a comfortable UI comes to my mind. - -![](https://lh3.googleusercontent.com/3-RkCO-KK-QDEio1zGeok03P7d9BuN7tTdfwDVZfdTwOm3lmj3qgODOkIFEvpGPU-X7BtwBCK3i9ko9z-An63sw00A=w640-h400-e365-rj-sc0x00ffffff) - -![](https://lh3.googleusercontent.com/bgytldKRmvrJ3r7fVmfd1C1ZVGVFLJZUtE-ZGsV9yO-UAUnRl2vrM-9x3dqCscu-rIHuZeeVBSbSWS6Zkm19-Y-Bdg=w640-h400-e365-rj-sc0x00ffffff) - -## You - -Search less, find and create more with generative AI apps supercharging your search and helping you write better, code better, and design better. [You.com](https://chrome.google.com/webstore/detail/youcom-search-chat-and-cr/fhplnehgjpmohhldfnjhibanpbiedofi) has a beautiful and modern UI. Quickly access generative AI apps powered by OpenAI models (such as GPT-3). - -![](https://lh3.googleusercontent.com/7iFUe_ac5FxWhSqBptLVgSDhHt4TebpzJ40rUgoQMUEH-Oi9Lnan9s1AOLHAEvMPZc2khiVGfej84qsaYBr3z3j-1tY=w640-h400-e365-rj-sc0x00ffffff) - -![](https://lh3.googleusercontent.com/5QQnng5TESoRGefTWxhF6ENSIcdBO8LYjJZeB7_wVkxLXqRv31UIptebAzU5lY8a5dQD1VBX67ewMlpaALM4qVtQ5g=w640-h400-e365-rj-sc0x00ffffff) - -## OCR Editor - -[OCR text extractor](https://chrome.google.com/webstore/detail/ocr-editor-text-from-imag/ihnfmldlpdipgnliolhfffenpcmjgnif/related), or image-to-text converter extracts text from images and videos. It uses optical character recognition (OCR) technology to accurately convert images into text. It supports text extraction from all the images. - -![](https://lh3.googleusercontent.com/NZFyEnSuY8wMNUlTAoD3gPIyRjYPkXbsCkUq2UbhU4vRi5ofz6FkAVcf42lKR98TwCPA-hysPG73toYbe3bQeCMZLZ0=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## Wrapping up - -These were some extensions for this month (January 2023). I have personally used all the above extensions and from my experience, every extension is worth installing. diff --git a/frontend/posts/ce-nov-2022.mdx b/frontend/posts/ce-nov-2022.mdx deleted file mode 100644 index d1069a0..0000000 --- a/frontend/posts/ce-nov-2022.mdx +++ /dev/null @@ -1,117 +0,0 @@ ---- -slug: ce-nov-2022 -title: Chrome Extensions of the Month - November 2022 -date: 2022-11-29 -keywords: css, web development, productivity, chrome, chrome extensions, chrome extensions of the month, november -published: true -excerpt: In this article, we are going to look at some of the best extensions that you need to install for better productivity. So without further due, let's get into it. -image: https://imgur.com/qK15kjE.png ---- - - -In this article, I will suggest to you some of the best extensions you need to install for better productivity that can come in very handy. So without further due, let's get into it. - - -## 1. Simplify Gmail - -Brought to you by the co-founder and design lead for Google Inbox, [Simplify Gmail](https://chrome.google.com/webstore/detail/simplify-gmail/pbmlfaiicoikhdbjagjbglnbfcbcojpj) is a browser extension for desktop Gmail that boosts productivity, strengthens privacy, and reduces️ stress. It makes the Gmail very simple in UI and more productive. - - -![Simplify Gmail](https://lh3.googleusercontent.com/MS5EZsoGs_Jqi_j00TX8FnFi_Tdghne4dfpDNLvoVbNKOVE3gz2LTRMHbD7wXPi26oBeMlACYzS1eYwAk3arNHSXeA=w640-h400-e365-rj-sc0x00ffffff) - -## 2. Bardeen - -[Bardeen](https://chrome.google.com/webstore/detail/bardeen-automate-workflow/ihhkmalpkhkoedlmcnilbbhhbhnicjga) is a no-code workflow automation app that replaces your repetitive tasks with a single shortcut. Connect your favorite web apps and build custom automations in minutes. - -Suggested By - [@dochan](https://dev.to/dochan) - -![Bardeen](https://lh3.googleusercontent.com/TVBzwPMLMxn_vND09BUKjCERsa6-qL4TbCJVUetk9yGOuM_GvsXebePz6z-cgHf_EGpjAls25y9OAQ936pNk0bMyRA=w640-h400-e365-rj-sc0x00ffffff) - -## 3. Let Me Read That Article - -Do you hate it when you want to read an article ad free, but the website says, "looks like you're using an ad-blocker"? Or when you have a limited number of reads on a news website until you have to subscribe? Same here, so here's an extension that will intercept those annoying popups and let you read that article. - -**[Download](https://chrome.google.com/webstore/detail/let-me-read-that-article/bmdnpacffafhifoibkeajbacaapgcoih)** - -![Let Me Read That Article](https://lh3.googleusercontent.com/FXQ_ZyUmuT7TItRnmiNOEBBlFl46ZG9REqIobYyWFuAAOjv6MMukSn08Hkrch3bcpPUKXtNVtKUceY5fj0PrYrJr=w640-h400-e365-rj-sc0x00ffffff) - - -![lemme read](https://lh3.googleusercontent.com/qkinNy_ss__MgFaKQNhXhBQeXIJeGno2l6Dntodvet4WW_IZAi1JXZ_jme1HMWTQLBUiim5oOa2ZftuBl01M9_Y3KyU=w640-h400-e365-rj-sc0x00ffffff) - -## 4. Image download center - -[Image Download Center](https://chrome.google.com/webstore/detail/image-download-center/deebfeldnfhemlnidojiiidadkgnglpi) - saves your time when you want to download images from a webpage. - -Features: -- Download all images -- Supports many formats including PNG, JPEG, SVG, WEBP -- Filter by size (you can filter out website logos and design elements) -- Preserves file name - - -![Image download center](https://lh3.googleusercontent.com/K9kZUfAxrnaadvGZ50BxsEb1SVtz0RvCo6PNWGIzoHkOExs2zcTeoQUFkyv0r4PZSTT8Ov2tsvp0_i5k8hZlr3JYXyQ=w640-h400-e365-rj-sc0x00ffffff) - -## 5. Paint Tool - -[Paint Tool](https://chrome.google.com/webstore/detail/paint-tool-marker-for-chr/nadohmjilefnhjobhhlnnddplaklmnnp) - is a simple to use free extension that allows you to create and save quick and fun drawings while using Chrome! It can also be used as a Full Page screenshot tool. - -![Paint Tool ](https://lh3.googleusercontent.com/KN2SRMkwOB0bmtY-N3tloAdcKaSBY4Gp3CxDnKfH7qeKRS3jdRurroPz-yzbUAjYYtTxnCTO9jxjdnoPMdnTR0Lvfw=w640-h400-e365-rj-sc0x00ffffff) - -## 6. Selenium IDE - -The [Selenium IDE](https://chrome.google.com/webstore/detail/selenium-ide/mooikfkahbdckldjjndioackbalphokd) is designed to record your interactions with websites to help you generate and maintain site automation, tests, and remove the need to manually step through repetitive takes. - -![Selenium IDE](https://lh3.googleusercontent.com/8TvMgoN4USfvq_AMf_QWoExfjNQv-j3OLdFMD8OfWgMluDyHyVc492A6ZbqWIO52qlMBeOz_HgkCM9ePG9Ul6I7xYtQ=w640-h400-e365-rj-sc0x00ffffff) - -## 7. Color by Fardos - -Pick colors from websites, save colors & gradients, get matching shades and tints and create beautiful gradients. It's my favorite color picker. - -**[Download](https://chrome.google.com/webstore/detail/color-by-fardos-color-pic/iibpgpkhpfggipbacjfeijkloidhmiei)** - - -![Color by Fardos](https://lh3.googleusercontent.com/RquFxAKv5ve0Jzeqt9ttsxWPZTPlScaMW4qM1VIxLmsPaC94BhcMJso6NbQKrTT4gGUD8JhCQB-KatExJBZLFMEzkW0=w640-h400-e365-rj-sc0x00ffffff) - -![Color picker](https://lh3.googleusercontent.com/IhSwctg2_czBu29vq9w0NLpQ3rDKCEtiiGR2pKZmmJqjCvZDIy2GxAxVe2N2v6r771nRtGBhuRR7kGCBNCNbip6c=w640-h400-e365-rj-sc0x00ffffff) - - -## 8. PixelZoomer - -[PixelZoomer](https://chrome.google.com/webstore/detail/pixelzoomer/fogkjckfkdcnmnnfmbieljpkmmihhpao) takes a screenshot of the current website and provides various tools for pixel analysis. You can zoom into websites (up to 3200%), measure distances and pick colors with an eye dropper. - -![PixelZoomer](https://lh3.googleusercontent.com/rscXfBzRikjz0CQwOYU5cwUB77Ja9WwY9a6alF1Fjh8fgA9MAQUzmqqEVXWeVW4nstgMHeNm1TpPitZrQerVn7XOmg=w640-h400-e365-rj-sc0x00ffffff) - -## 9. SpanTree - -[SpanTree](https://chrome.google.com/webstore/detail/spantree-gitlab-tree/gcjikeldobhnaglcoaejmdlmbienoocg) makes navigating a GitLab repository feel like a breeze by providing a familiar tree structure. - -**Features:** -- Easy to navigate tree structure -- Resize the tree to your convenience -- Supports self-hosted GitLab instances (Along with compatibility mode for GitLab v12 and less) -- Lazy loaded file structure for a fast responsive user interface -- Inbuilt Dark Theme for GitLab -- Quick Search your Repository (using Ctrl/⌘ + P) - - -![SpanTree](https://lh3.googleusercontent.com/tVJm8rdusGtdVbWT6d3qnbE0ilLWWqQ2XfjDbCAr8lPVEqhb548mw8nOjwYoD4_Z7vVkTbkQHFYyNHmy-jYB2YNzxg=w640-h400-e365-rj-sc0x00ffffff) - -![spantree -1 ](https://lh3.googleusercontent.com/3s2vRE54FhLfdGQjDQ9eFhoM6eff4QoGO7cFRKtxVkrfhYOI4Vg6PLB8CiX91cw4uHHJiOhiZ3wvCjDuUiXdfXJZ=w640-h400-e365-rj-sc0x00ffffff) - - -## 10. Briskine - -Write emails faster! Increase your productivity with templates and keyboard shortcuts on Gmail, Outlook, or LinkedIn. - -**[Download](https://chrome.google.com/webstore/detail/briskine-email-templates/lmcngpkjkplipamgflhioabnhnopeabf)** - -![Briskine](https://lh3.googleusercontent.com/6qc8rP4r7JknInSxSxo9ZEcyLde5uls_uW8ZY6zyyggteTANS4fA88J-ng9ggFgHCUoUyEatRUsRqxvwQSmyuiZY=w640-h400-e365-rj-sc0x00ffffff) - - - - -*** - -## Wrapping up - -These were some extensions for this month (November 2022). I have personally used all the above extensions and from my experience, every extension is worth installing. diff --git a/frontend/posts/chrome-extensions-april.mdx b/frontend/posts/chrome-extensions-april.mdx deleted file mode 100644 index fdc3d2b..0000000 --- a/frontend/posts/chrome-extensions-april.mdx +++ /dev/null @@ -1,78 +0,0 @@ ---- -slug: chrome-extensions-april -title: Chrome Extensions of the Month - April -date: 2022-04-26 -published: true -excerpt: In this article, we are going to look at some of the best extensions that you need to install for better productivity. So without further due, let's get into it. -image: https://imgur.com/FwgXV49.png ---- - -In this article, we are going to look at some of the best extensions that you need to install for better productivity. So without further due, let's get into it. - - -## 1. Grepper - -The [Grepper](https://cutt.ly/vGj0rWd) is the ultimate RAM upgrade for the software developer's brain. Easily snag code examples from around the web, then access your code examples without having to think. - -![grepper](https://cutt.ly/QGj0scV) - -## 2. Fake Filler - -[Fake Filler](https://cutt.ly/aGj0bSa) is the form filler to fill all input fields on a page with randomly generated fake data. This productivity boosting extension is a must for developers and testers who work with forms as it eliminates the need for manually entering values in fields. - -![fake filler](https://cutt.ly/PGj0WMm) - -## 3. Extensity - -[Extensity](https://cutt.ly/EGj0Ol6) lets you manage your extension. You can turn on/off an extension with just one click. Keep your browser lean and fast. You can turn off all extensions with just one click. It allows your most important extensions to be always enabled. -![extensity](https://cutt.ly/6Gj0SQQ) - -## 4. daily.dev - -[daily.dev](https://cutt.ly/xGj2yl4) is the fastest growing online community for developers to stay updated on the best developer news. Get all the content you love in one place collected from +400 sources. - -![dailydev](https://cutt.ly/OGj2pVl) - -![features](https://cutt.ly/vGj2s4g) - -## 5. Dimensions - -[Dimensions](https://cutt.ly/aGj2E8T) extension measures the dimensions from your mouse pointer up/down and left/right until it hits a border. So if you want to measure distances between elements on a website this is perfect. It doesn't really work with images because there the colors change a lot pixel to pixel. - -![dimensions](https://cutt.ly/gGj2U15) - -## 6. Mailtrack - -Free and unlimited tracking for Gmail. [Mailtrack](https://cutt.ly/hGj2ZPh) is a personal email marketing tool with Campaigns, Mail Merge and PDF document tracking from Gmail. - -![mailtrack](https://cutt.ly/KGj2VNb) - -## 7. GitHub Code Folding - -[GitHub Code Folding](https://cutt.ly/jGj24In) - the ability to selectively hide and display sections of a code - is an invaluable feature in many text editors and IDEs. Now, developers can utilize that same style code-folding while poring over source code on the web in GitHub. Works for any type of indentation, spaces or tabs. - -![codefolding](https://cutt.ly/XGj26vQ) - -## 8. Hide Scrollbar - -Sometimes I hate the scrollbar in the browser. That's where this [Hide Scrollbar](https://cutt.ly/SGj9sxR) extension comes into play. This extension allows you to hide the scrollbars on any page while still allowing you to use the scroll functionality. It helps provide a minimal style for any webpage. - -![scrollbar](https://cutt.ly/4Gj9cup) - -## 9. I don't care about cookies - -[I don't care about cookies](https://cutt.ly/zGj9RFh) extension removes cookie warnings from almost all websites. In most cases, it just blocks or hides cookie related pop-ups. When it's needed for the website to work properly, it will automatically accept the cookie policy for you (sometimes it will accept all and sometimes only necessary cookie categories, depending on what's easier to do). It doesn't delete cookies. - -![I don't care about cookies](https://imgur.com/u1cSLan.png) - -## 10. Material Icons for GitHub - -Replace the file/folder icons on the GitHub file browser with icons representing the file's type and which tool it is used by. -Replace GitHub's default icons with icons from Visual Studio Code's Material Icon Theme icons. Use the same icons on your code editor and on github.com, and quickly identify file types, configuration files and project scaffolding at a glance. -[Download](https://cutt.ly/4Gj3YCL) - -![materialIcons](https://cutt.ly/RGj3P8G) - -## Wrapping Up - -These were some extensions for this month (April). I have personally used all the above extensions and from my experience, every extension is worth installing. diff --git a/frontend/posts/chrome-extensions-aug-2022.mdx b/frontend/posts/chrome-extensions-aug-2022.mdx deleted file mode 100644 index 33ae0bd..0000000 --- a/frontend/posts/chrome-extensions-aug-2022.mdx +++ /dev/null @@ -1,200 +0,0 @@ ---- -slug: chrome-extensions-aug-2022 -title: Chrome Extensions of the Month - August 2022 -date: 2022-08-31 -published: true -keywords: css, web development, productivity, chrome, chrome extensions, chrome extensions of the month , august -excerpt: In this article, we are going to look at some of the best extensions of August that you need to install for better productivity. So without further due, let's get into it. -image: https://imgur.com/rnrLy9v.png ---- - - -In this article, I will suggest to you some of the best extensions you need to install for better productivity that can come in very handy. So without further due, let's get into it. - -## Clipt - -Clipt creates a link between all of your devices to increase productivity. Seamlessly and safely send text, photos, and files over the cloud by synchronizing your clipboard. It’s as easy as copying on one device and pasting on another and works in the background. - -**Key Features:** -- Seamlessly link multiple devices across Ecosystems for increased productivity -- Transfer data with confidence knowing it’s on your Google Drive -- Transfer text, images, and even files -- Search the recent history of your shared clipboard - -> Clipt doesn’t see what you send as it uses your Google Drive and only transfers a way to identify the file. Keeping your information safe within the Google Cloud. - -**Download:** [Clipt](https://chrome.google.com/webstore/detail/clipt/ngpicahlgepngcpigiiebnheihgbaenh/) - -![clipt](https://lh3.googleusercontent.com/Fp0ie0ful9gffqeK6N_p8RW6BKJmrvGwSLdhLZqAVLFQk0kFCntNdPSBUnCyDkEd5-7Pa5_efdhyfYjGQl73AXXOB-0=w640-h400-e365-rj-sc0x00ffffff) - - - - -*** - -## I'm Feeling Lucky - Skip search - -This extension allows you to skip the search result page and take you directly to the most relevant result. It also offers some shortcuts to navigate the Search Results page. You just need to type "go" and then enter the word you want to search such as "LinkedIn". It will take you directly to linkedin.com (Skipping Search Results). - -**Key Features:** - -- Takes you the most relevant result directly -- Keyboard shortcut: Press '1' - '9' on the Google search result page to go directly to the result link (1 is the top result) -- Deep integration with Google search to provide the highest quality result -- Autocomplete suggestions with personalized query suggestions powered by Google search - -**Download:** [I'm Feeling Lucky](https://chrome.google.com/webstore/detail/im-feeling-lucky-skip-sea/fpenjoflgejmljiniakaonpgfcamcooc/related?hl=en) - - - - -*** - -## Checkbot - -Checkbot is a powerful website testing tool that tells you how to improve the SEO, page speed, and security of your website. Checkbot crawls 100s of pages at the same time checking for 50+ common website problems based on web best practices recommended by Google and Mozilla. - -**Key Features:** -- 📊 SEO TESTS -- 🚀 PAGE SPEED TESTS -- 🔒 WEB SECURITY TESTS - -**Download:** [Checkbot](https://chrome.google.com/webstore/detail/checkbot-seo-web-speed-se/dagohlmlhagincbfilmkadjgmdnkjinl?hl=en) - -![CHECKBOT](https://lh3.googleusercontent.com/9U9MpGIVZx4FfZ4cf6wImdpAVNSnAhuu4_ERJsDhMGjlR3XFpVpTEZofL1qx-zJzsYN30dVDnDdS2wEgSK0BzhkvhgE=w640-h400-e365-rj-sc0x00ffffff) - - -*** - -## ModHeader - -This extension allows you to Add, modify, and remove request and response headers. You can also use ModHeader to set X-Forwarded-For, Authorization, Access-Control-Allow-Origin, etc. - - -**Key Features:** - -- Add, modify, and remove request and response headers -- Use ModHeader to set X-Forwarded-For, Authorization, Access-Control-Allow-Origin, etc. -- Modify cookies in the request/response header -- Redirect URL with another -- Enable header modification by URLs -- Advanced filtering by tab, tab group, or window - -**Download:** [ModHeader](https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj?hl=en) - -![mod header](https://lh3.googleusercontent.com/WHDldHBTubkSkUH859odR2NpQZT4BMzvPbFIUkHUReFgRolJtOv6xYrV4LO9DVehXikdqVI3kia2BjH3TQ_ECXEaLQ=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## Modern scrollbar - -A modern look for Chromium's scrollbar. Compatible with light and dark modes. Have a nice, thin, modern scrollbar. The modern scrollbar works with the dark mode of most websites. This extension is extremely lightweight, it does not slow down your browser. - -**Key Features:** - -- Light Weight -- Give a modern look to your scrollbar -- Compatible with light and dark modes - -**Download: ** [Modern scrollbar](https://chrome.google.com/webstore/detail/modern-scrollbar/bgfofngpplpmpijncjegfdgilpgamhdk?hl=en) - -![modern Scrollbar](https://lh3.googleusercontent.com/TaHbis9bDWpBYm7LzUFyTkj_diG2SKJ3H3ucZSeg9zMuqy6AfD4kZTFMOIq7O887l5QMbQ3nslJzXDETjsL-wbkzXiQ=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## Google Dictionary (by Google) - -This extension helps you to view definitions easily as you browse the web. You just need to double-click any word to view its definition in a small pop-up bubble. It also keeps track of the words that you've looked up, so you can practice them later. - -**Key Features:** - -- Double-click any word to view its definition in a small pop-up bubble. -- View the complete definition of any word or phrase using the toolbar dictionary. -- Store a history of words you've looked up, so you can practice them later. - -**Download: ** [Google Dictionary](https://chrome.google.com/webstore/detail/google-dictionary-by-goog/mgijmajocgfcbeboacabfgobmjgjcoja?hl=en) - - -![google dict](https://lh3.googleusercontent.com/Sm2s-DpuE4NpOsIsCdgvoNVPWtAR55dNKnqn7xWjjPsg0C6HkfOkmiZhaode7sERFDGXFoTDMNLVKtL-V2kEahgmnw=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## Inspect CSS - -I have used many extensions that allow you to inspect CSS. So far this one is my favorite. It has a very simple, clean, and modern UI. It allows you to inspect the CSS of any element in the webpage along with `hover` styles. You can download Images from the webpage, Inspect attributes, color palettes, etc. - -**Key Features:** -- Get CSS Properties from any element by selecting it -- Get and edit element attributes -- Add your custom CSS to the website -- Get the color palette of the website -- Download images from the webpage - -**Download:** [Inspect CSS](https://chrome.google.com/webstore/detail/inspect-css/fbopfffegfehobgoommphghohinpkego/related?hl=en) - - -![Inspect CSS](https://lh3.googleusercontent.com/MM_jTyiaIMUrsVFokYM_ARoFOJj9tmfsRZrxFuNchM2dbuQf6Muu8rjQeKTN2R6aW9QYJe9nGv2shbkTSnItNs30AC4=w640-h400-e365-rj-sc0x00ffffff) - -![inspectcss](https://lh3.googleusercontent.com/wgiuo-ImhILERr8G7VuyKugxApZO2TMGcxh0RHxoSSUzbqrIm4MfDV8cCZ6f8jFi-oXorsBdGaJhIZavYbNhHPL-SA=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## Smart TOC - -Sometimes you visit a website and you be like... man it should have a TOC (Table of Contents) but unfortunately, it doesn't. That's where this extension comes in. This extension displays a table of contents for every website, making it easier to read/navigate long articles, documentation, and online books. - - -**Key Features:** -- Accurate article/headings detection -- Clean user interface -- Highlight the current heading -- Click to jump to the heading -- Panel can be dragged to the preferred position -- Only runs when you actually use it - -**Download:** [Smart TOC](https://chrome.google.com/webstore/detail/smart-toc/lifgeihcfpkmmlfjbailfpfhbahhibba/related?hl=en) - -![smart toc](https://lh3.googleusercontent.com/NhJI3YHU8I1F3cEL_AQct8np0SpmrJG6ddAK65qU975-Sj4CKX7P_JQDlaABm6OBm_YRN5N1_1gy0P2hZIIfVBOA=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## Smart Mute - -Tired of having multiple tabs play audio simultaneously? Take back control of your audio experience with Smart Mute and listen to audio one tab at a time. - -**Key Features:** -- Mute / Unmute Tab -- Add websites to your whitelist to ensure your listening experience never gets interrupted again. -- Add websites to the blacklist to block audio (some website have too many ads that plays automatically) -- Silent Mode prevents any audio from playing in your browser. - - -**Download:** [Smart Mute](https://chrome.google.com/webstore/detail/smart-mute/apadglapdamclpaedknbefnbcajfebgh?hl=en) - -![Smart Mute](https://lh3.googleusercontent.com/HrNGFGgarrG1QvJxHAP0rEqo4ZpKMkqXjnoklP8imS7ITog768UE_VzhpiEpe-rcrcXuev6e5FcZgaYIeuYFD9CnSF4=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## Listly - Free Data Scraper, Extractor - -This extension allows you to turn Web pages into Excel data in seconds with just one click. -It automatically extracts clean data and arranges them into rows and columns. - -**Key Features:** - -- Schedule a daily extraction -- Export Multiple pages into an Excel spreadsheet on Databoard -- Upload .html files to Fileboard -- Auto-Save while scrolling -- Select Proxy Server to change the IP address -- Extract Hyperlinks over the content - -**Download:** [Listly](https://chrome.google.com/webstore/detail/listly-free-data-scraper/ihljmnfgkkmoikgkdkjejbkpdpbmcgeh) - - - -*** - -## Wrapping up - -These were some extensions for this month (July 2022). I have personally used all the above extensions and from my experience, every extension is worth installing. diff --git a/frontend/posts/chrome-extensions-july-2022.mdx b/frontend/posts/chrome-extensions-july-2022.mdx deleted file mode 100644 index 285e1ab..0000000 --- a/frontend/posts/chrome-extensions-july-2022.mdx +++ /dev/null @@ -1,213 +0,0 @@ ---- -slug: chrome-extensions-july-2022 -title: Chrome Extensions of the Month - July 2022 -date: 2022-07-31 -published: true -keywords: css, web development, productivity, chrome, chrome extensions, chrome extensions of the month -excerpt: In this article, we are going to look at some of the best extensions of July that you need to install for better productivity. So without further due, let's get into it. -image: https://imgur.com/Fa1m1T7.png ---- - -In this article, I will suggest you some of the best extensions you need to install for better productivity. So without further due, let's get into it. - - -## Manganum - -It is a new tab with a sidebar that turns Chrome from just a browser into a productivity workspace. It can really boost your productivity. It has tons of features. - -**Key Features:** - -- Quick access to your favorite and most visited sites without opening a new tab -- Translate text instantly across more than 100 languages -- See your schedule from Google Calendar on any browser tab -- You can access your Google Tasks -- Simple notes that are always at your fingertips -- Customizable Backgrounds -- Clock and Weather widgets -- Motivation Quotes - -**Download: [Manganum](https://chrome.google.com/webstore/detail/manganum-1-new-tab-for-ch/jbfeongihppeenfnaofmdeikahaefljd)** - -**Size: 21.48MiB** - -![mg1](https://lh3.googleusercontent.com/3JmPUjaoj4kMOV8chuz5q6p_-fVIcoZbj1Lj8VuvGALmokvigvj37X4DeIUdL-rSMpTyZoBL3d7cZ8m_4eSMq91mEw=w640-h400-e365-rj-sc0x00ffffff) -![mg2](https://lh3.googleusercontent.com/h1pIsFmmUreduOnoQGMh7-TeEGiDY4ACArYr38uxRm9U7ZwLzxa8Keb7KlXqLZ1iJrBVPWDpzHJ-HY9nKxxs5cFN=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## Wordtune - - -Wordtune is an AI-powered writing companion that understands what you’re trying to say and suggests ways to make your writing more clear, compelling, and authentic. - - -**Key Features:** - -- Write a better, clearer message and deliver it the way you intend to -- Make brilliant vocabulary choices -- Spend less time editing and perfecting your text -- Write with confidence -- Sound more fluent -- Expand your English vocabulary - - -**Download: [Wordtune](https://chrome.google.com/webstore/detail/wordtune-ai-powered-writi/nllcnknpjnininklegdoijpljgdjkijc)** - -**Size: 1.42MiB** - -![wordtune](https://lh3.googleusercontent.com/SK-x_LSckiR6HDHUKEoCVJ_rHsEzV_FkHHTQ5OsTg7nc-ax2KxvACb8QMcQVHJt5uPFSzIY-LRBaDb4q7ObctlcTJQ=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## The Great Suspender - - -It suspends unused tabs to save memory usage. The flexible settings menu allows setting shortcuts, a periodical of suspending, and many other advantages. - - -**Key Features:** - -- suspend opened tabs by schedule or with a single click to save memory -- restore tab all at one or only one needed -- create a group of tabs and save it for then using -- suspend and unsuspend the selected tab or all at once - -**Download: [The Great Suspender](https://chrome.google.com/webstore/detail/the-great-suspender-origi/ahmkjjgdligadogjedmnogbpbcpofeeo)** - -**Size: 301KiB** - - -![suspender](https://lh3.googleusercontent.com/LDZLDIrFuGIAq4r73h_5tRV2xHBsJ8XybPHxsEIolVAMRxdLyakjtV4hKOKTwqIKajiutkOkh3RhMChvTZltefjq=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## Papier - -People who don't like too much chaos and prefer simple things as I do. Then this extension is for you. Sometimes you had a thought and you don't know where to write it or even if you write it somewhere then you could lose it. This extension allows you to create a simple New Tab with only just Markdown and nothing else. - -**Key Features:** -- Simple and Minimalistic -- Clean and distraction-free -- Uses Markdown Format -- Print the Content -- Multiple Themes and Fonts - - -**Download: [Papier](https://chrome.google.com/webstore/detail/papier/hhjeaokafplhjoogdemakihhdhffacia)** - -**Size: 1.02MiB** - -![papier](https://lh3.googleusercontent.com/pgpdqHqKKKc9tAcf7qFB1xpK86G4QSTWw--KGOUgu5YrtcaVrnRuTs5_Bi8hfSxWUENg61ngd5URsONmhT8Pai7t=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## Button for Google Calendar - -It is a quick overview of your *Google Calendar* with one-click access. It helps you to create, and customize your google calendar instead of vising the website by yourself. It can also notify you about upcoming events. - - -**Key Features:** -- Constantly reminds you of an upcoming event from your Google Calendar™. -- Sends push notifications following the meeting settings. -- Provides quick access to conference links from your events. Google Meet, Zoom, Microsoft Teams, and Skype are supported. -- Allows you to create new events quickly. -- Displays meetings from selected calendars only. - -**Download: [Button for Google Calendar](https://chrome.google.com/webstore/detail/button-for-google-calenda/lfjnmopldodmmdhddmeacgjnjeakjpki?hl=en)** - -**Size: 184KiB** - - -![calendar](https://lh3.googleusercontent.com/dGwxGUMR3WjiQco40QjBHHKzzuTsEYTqIFoJQcZcdKXiIz8kJ3YVrPjDuCjX-l-VTUJwfdkWKG0zIZ4dOHRJ57qyT3Q=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## LINER - -It helps you to discover the highlights of the web, filtered by millions of intelligent people around the world. - -**Key Features:** - -- Search Assistant -- Show the result that is Trusted by LINER users -- Highlights Preview (sneak peek at a page’s key contents. See if it contains the information you need) -- Popular Highlights by LINER users -- Web & PDF Highlighter -- Image Highlights -- Add a comment, and leave your instant thoughts on your highlight. -- It also comes with LINER New Tab - - -**Download: [LINER](https://chrome.google.com/webstore/detail/liner-search-faster-highl/bmhcbmnbenmcecpmpepghooflbehcack)** - -**Size: 1.29MiB** - - -![liner](https://lh3.googleusercontent.com/PMSe1LzBoaua4C_bCMWXN7WLXU50x5uDqCIk0iquyyGUzjQbXRc3JfsWNkXMwMVHpihJrRUqXBzlB40R5Owcf_K2Sw=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## Disconnect - -Disconnect lets you visualize and block the invisible websites that track you. Load the pages you go to 44% faster. Stop tracking by thousands of third-party sites. - - -**Download: [Disconnect](https://chrome.google.com/webstore/detail/disconnect/jeoacafpbcihiomhlakheieifhpjdfeo)** - -**Size: 1017KiB** - -![disconnect](https://lh3.googleusercontent.com/zScj_Xh3uu7XFn1JjFJBk8dt2XYrIjzpwkVr8NQeroqmrfcv2QWOaTIdon45CQYVMtudznO_tI_c_GniYXk7RqheZA=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## npmhub - -It lets you explore the npm dependencies of any repository within Github itself. On every GitHub repository or folder with a `package.json` file, scroll to the bottom of the page to see a list of its npm dependencies and their descriptions. - -**Download: [npmhub](https://chrome.google.com/webstore/detail/npmhub/kbbbjimdjbjclaebffknlabpogocablj/related) -** - -**Size: 29.61KiB** - -![npmhub](https://lh3.googleusercontent.com/hHPmjpRrD3sD2BfKTcjZIXERzXKT9HmCiBAnQV0L8bNRQkFJbUwVz-9eSHoOJ1P87ix9K9yh4Em0VcAD7qvwZkhRbg=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## Toast - Save Tabs for Later - -The extension helps you control open tabs by saving & closing the ones that aren't needed at this exact moment, so you can be more efficient working on one thing at a time. - -**Key Features:** -- Organize your Tabs in folders -- One Click to open all tabs in the folder -- Live synchronization of Tabs and Folder -- Share the Folder as a link - - -**Download: [Toast](https://chrome.google.com/webstore/detail/toast-save-tabs-for-later/pejhbjnfifdecpkgcjhgmcaphdobmiie)** - -**Size: 1.6MiB** - - -![toast](https://lh3.googleusercontent.com/U6PlL15uaEHveGPCyqnJ565-wfwSLUdXAcmJnq_Pk18HPI3_HddVMBexyMpyS6KuZMjCMaPg8dMLKjqnTGOxZ2sxiA=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## Email Finder by Snov.io - -Find email addresses on any website. This is an easy-to-use email finder with an inbuilt email verifier, email drip campaigns, and a lot of free tools. Snov.io also provides a campaign service in which you can use these fetched emails to send the campaign in bulk. - - -**Download: [Email Finder by Snov.io](https://chrome.google.com/webstore/detail/email-finder-by-snovio/einnffiilpmgldkapbikhkeicohlaapj/related)** - -**Size: 211KiB** - -![email](https://lh3.googleusercontent.com/5waP6uFHAFW3DMSyacXNaeaStY_Puklfcqy2RzMNSYDZpVtdt1i5GmanVfUAlnMhCsnyLzEn7KgqzSXFP8oj9xhL-6g=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## Wrapping up - -These were some extensions for this month (July 2022). I have personally used all the above extensions and from my experience, every extension is worth installing. - - diff --git a/frontend/posts/chrome-extensions-june.mdx b/frontend/posts/chrome-extensions-june.mdx deleted file mode 100644 index 92513ba..0000000 --- a/frontend/posts/chrome-extensions-june.mdx +++ /dev/null @@ -1,153 +0,0 @@ ---- -slug: chrome-extensions-june -title: Chrome Extensions of the Month - June -date: 2022-06-30 -published: true -excerpt: In this article, we are going to look at some of the best extensions of June that you need to install for better productivity. So without further due, let's get into it. -image: https://imgur.com/BDLbnWC.png ---- - - -In this article, we are going to look at some of the best extensions of June that you need to install for better productivity. So without further due, let's get into it. - - -## 1. SVG Export - -[SVG Export](https://chrome.google.com/webstore/detail/svg-export/naeaaedieihlkmdajjefioajbbdbdjgp) lets you download SVGs from websites as SVGs, PNGs, and JPEGs. You can export them in bulk - -**Key Features:** -- Bulk export -- Export as PNG, JPEG, or SVG from any website -- Resize images - -![SVG export](https://lh3.googleusercontent.com/8o74-dBaQb5Uda1tFt6Eye5q_nl5sY1n0HrODIvQIU-3Q8NQoatcw2fKAwnTHXWtVew-SoSaBMjPEZowUFUka3RO=w640-h400-e365-rj-sc0x00ffffff) - -*** - - -## 2. Bionic Reading - -[Bionic Reading](https://chrome.google.com/webstore/detail/bionic-reading/kdfkejelgkdjgfoolngegkhkiecmlflj) extensions play with `font-weight`. It increases the `font weight` of some of the characters in the text which increases the readability of the text. - -**Key Features:** -- Increase the readability of text -- Bold the initial letters for faster reading -- Focused Reading - -![bionic reading](https://lh3.googleusercontent.com/9PH2ZwiLHoauM46XaOviouBVYERlZQky7elTbnn2X6C4eKYa8VxOBcGCEfF0cTmPw4AJy-Tn81FAATcc5CAjejrN=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## 3. Mobile Simulator - -[Mobile Simulator](https://chrome.google.com/webstore/detail/mobile-simulator-responsi/ckejmhbmlajgoklhgbapkiccekfoccmk) lets you check the responsiveness of your website or web app. You can do that via chrome dev tools but it has support for 40+ devices. - -**Key Features:** -- Test the Responsiveness -- 16 models of Android smartphones -- 15 models of Apple smartphones -- 5 models of tablets -- 7 special devices -- Take a screenshot of the smartphone in transparent PNG - -![mobile simulator](https://lh3.googleusercontent.com/4ga5aWnod0W6o_3bYR6OOdWcXvbtW-7D6yRwgQD-FeMICJG-k_-WjdXeIAQdkc5M4w2dUHgfJEKYLfx786UO9lC-kA=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## 4. Speechify Text-to-Speech (TTS) - -[Speechify](https://chrome.google.com/webstore/detail/speechify-text-to-speech/ljflmlehinmoeknoonhibbjpldiijjmm) allows you to listen to docs, articles, PDFs, email — anything you read — with our leading text-to-speech reader. - -**Key Features:** -- Listen to Docs, Articles, PDFs, Emails, etc. -- Turn any text into speech -- Listen with the highest quality natural-sounding voices in over 20+ languages. - -![speechify](https://lh3.googleusercontent.com/gxuSR_dSLNYwKPK-XeZ44coeKZCz1-vOVWB9z4JkBEjrIZSKmeneFKat4JqUjs6MKxNts5ewMdLm4vn4qKctuEbNgv0=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## 5. Hover Zoom+ - -[Hover Zoom+](https://chrome.google.com/webstore/detail/hover-zoom%2B/pccckmaobkjjboncdfnnofkonhgpceea) allows you to hover over any image on the supported websites and it will automatically enlarge the image to its full size, making sure that it still fits into the browser window. - -**Key Features:** -- Enlarge the image to its full size -- Make sure that the image fits into the browser window - - -![hover zoom+](https://lh3.googleusercontent.com/Dgk59trR55ciTAkrHTTTNiVFahl0X5Q41XPab41uwDSZbmdqgkCLk-FDmX-BfAylR9CZPtxWIfRDHdatUy2GhfOa2A=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## 6. Scribe - -[Sribe](https://chrome.google.com/webstore/detail/scribe-%E2%80%94-documentation-so/okfkdaglfjjjfefdcppliegebpoegaii) auto-generate step-by-step guides, just by clicking record. All you have to do is click “record” and go through the process you want to share. Scribe monitors your clicks and keystrokes to instantly create your guide. - -**Key Features:** -- Automatically generated step-by-step guides -- Customizable text, steps, and images -- One-click sharing -- Easy embed in any knowledge base or CMS -- Auto-redaction for sensitive information -- Enterprise-grade security and controls - -![scribe](https://lh3.googleusercontent.com/CN8n-PRqK3TiLPkzwh0FJ9Uv2j54gpdPuf7T6gTlL8B7BR7ZFGA14X0ZNGBB6ORU9HDTLwgd2atO3MPLNPAQ15rk=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## 7. Visual CSS Editor - -[Visual CSS Editor](https://chrome.google.com/webstore/detail/visual-css-editor/cibffnhhlfippmhdmdkcfecncoaegdkh/related) allows you to customize any website without coding. Click on an element and start visual editing. Adjust colors, fonts, sizes, positions, and a lot more. Take full control over your website's design with more than 60 style properties. - -**Key Features:** -- Visual Inspector -- Customize any website visually -- Automatic CSS selectors -- 60+ CSS properties -- Visual Dragging -- Visual margin & padding editing -- Undo/redo history -- Export CSS Styles - -![visual css editor](https://lh3.googleusercontent.com/TTEAGbufQo-RJE81GYbcY_H8fIPGz-on-frsWdACMxMCLpldJRNGJKaJAivs_peJRZie5swxLKSU92Sj7KS7I1a6zZo=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## 8. Social Share Preview - -With [Social Share Preview](https://chrome.google.com/webstore/detail/social-share-preview/ggnikicjfklimmffbkhknndafpdlabib/related) plugin, you can check how the link previews of a website look like on Facebook, Twitter, LinkedIn and Pinterest. Just activate the plugin to see the preview snippets of every page you browse. - - -![social preview](https://lh3.googleusercontent.com/-mc69tCw-Pw-sbd8XqFPpVRDUngtFTw2OqchJV2Xq-YGxDxMkBP3e_F6KgSasf4Zans-ZkqRricbs7n4PSqLZOp4Qw=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## 9. Wappalyzer - -[Wappalyzer](https://chrome.google.com/webstore/detail/wappalyzer-technology-pro/gppongmhjkpfnbhagpmjfkannfbllamg) is a technology profiler that shows you what websites are built with. Wappalyzer is more than a CMS detector or framework detector: it uncovers more than a thousand technologies in dozens of categories such as programming languages, analytics, marketing tools, payment processors, CRM, CDN, and others. - -![wappalyzer](https://lh3.googleusercontent.com/TE5cGjbTbj_mqLFn1_IljQ8NkX8lZZNDJApijpuoug4FMd8g5EsoWjW8ZUcHnlclzo1KknI21_KUmckFNHUE3JCO0w=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## 10. Raindrop.io - -[Raindrop.io](https://chrome.google.com/webstore/detail/raindropio/ldgfbffkinooeloadekpmfoklnobpien) is all-in-one bookmark manager. You can save anything from the Web such as Clip articles, photos, videos, PDFs and pages from the web and apps. - -**Key Features:** -- Save Anything from the Web -- All bookmarks are organized -- Full-Text Search & Permanent Library -- Access your bookmark anywhere (Cross Platform) -- Bookmarks are Private by Default -- No tracking or intrusive advertising. - -![raindrop.io](https://lh3.googleusercontent.com/AO5W5bGoyq298U8eawQ5JqBbox1W9Jug2LGeD7qFcx48oroRnUfY2zttHwFl1FlToamZSNnu-Z48e_GJdO8AKFpCNhQ=w640-h400-e365-rj-sc0x00ffffff) - -*** - -## Wrapping Up -These were some extensions for this month (June). You might have heard some before, but if you like anyone then don't forget to press ❤️ and Bookmark this article for later use. I have personally used all the above extensions and from my experience, every extension is worth installing. - - diff --git a/frontend/posts/chrome-extensions-oct-2022.mdx b/frontend/posts/chrome-extensions-oct-2022.mdx deleted file mode 100644 index 37e5a3d..0000000 --- a/frontend/posts/chrome-extensions-oct-2022.mdx +++ /dev/null @@ -1,88 +0,0 @@ ---- -slug: chrome-extensions-oct-2022 -title: Chrome Extensions of the Month - October 2022 -date: 2022-10-28 -published: true -keywords: css, web development, productivity, chrome, chrome extensions, chrome extensions of the month, october -excerpt: In this article, we are going to look at some of the best extensions of September that you need to install for better productivity. So without further due, let's get into it. -image: https://imgur.com/9Zsnx6D.png ---- - - -In this article, I will suggest to you some of the best extensions you need to install for better productivity that can come in very handy. So without further due, let's get into it - -## Text Blaze - -[Text Blaze](https://chrome.google.com/webstore/detail/text-blaze/idgadaccgipmpannjkmfddolnnhmeklj) lets you insert text templates anywhere using keyboard shortcuts. Save hours and avoid mistakes by eliminating repetitive typing using customizable templates. - -![text blaze](https://lh3.googleusercontent.com/-OFu7owaWRf5JT2fRfAXYMqhskZpEI9AhE-FwMKrw2fPAEvezu5O7YzON_PvroMQnzFueDVG0bL3J-xgPQQ_f5M8ng=w640-h400-e365-rj-sc0x00ffffff) - -## Default Account for Google products - -With the [Default Account Manager for Google™ products](https://chrome.google.com/webstore/detail/default-account-for-googl/bnocikekcdchphdpfffcgikiblhnkdad/related), you can, for example, set up work accounts for Google Meet™ and personal for Gmail™, etc. - -![gafgp](https://lh3.googleusercontent.com/8hQKwsd9Jev9PZaQLklJ0vLTDQyFT1jD6bMJYpXSvlQ3O2Kv-htBcni2rZY7TtSeduLO-0FQ3N_uoRG8X-YIEML3oA=w640-h400-e365-rj-sc0x00ffffff) - -## Weava Highlighter - -[Weava](https://chrome.google.com/webstore/detail/weava-highlighter-pdf-web/cbnaodkpfinfiipjblikofhlhlcickei) can highlight websites and PDFs with multiple colors, and make annotations. Revisit them with a single click. It also Organizes your highlights into folders and sub-folders. You can access your highlights anywhere. - - -![weava](https://lh3.googleusercontent.com/li3XsJaA1g1u5mL9N9qDuahjujO6Q8Cg91TWgB8kqX_DU08AHotRICFztPHTn0U0QC3ACN4GJEl8cUdb4J7NuhkYqQ=w640-h400-e365-rj-sc0x00ffffff) -![weava2](https://lh3.googleusercontent.com/av8nK40cKbgDzWg8ETlbf1HcM17OwnV980NkCFvjo9yYRy7LbWuZMBYQ0SsKn34OqBQrjY-QAGJ1n5y0E5rF6lSj_0M=w640-h400-e365-rj-sc0x00ffffff) - -## Ghostery - -[Ghostery](https://chrome.google.com/webstore/detail/ghostery-%E2%80%93-privacy-ad-blo/mlomiejdfkolichcflejclcbmpeaniij) is a powerful privacy extension. Block ads, stop trackers, and speed up websites. Ghostery’s built-in ad blocker removes advertisements from a webpage to eliminate clutter so you can focus on the content you want. Ghostery allows you to view and block trackers on websites you browse to control who collects your data. - -![ghostery](https://lh3.googleusercontent.com/GOe98qBzHwlyPc1GWLmyFBUbAj6-9HIxCWFtBBRvOGtQ4IpOEHcduBxawc0ajWmCSgbKFm_NZVRxl3-tbvapuiWX1w=w640-h400-e365-rj-sc0x00ffffff) - -## Tab for a Cause - -Raise money for charity with every tab you open in Chrome! Just by surfing the web, you can build libraries, plant trees, send emergency aid, give clean water, and more. It's not a productive extension but it deserves a try to help people. - -[Download](https://chrome.google.com/webstore/detail/tab-for-a-cause/gibkoahgjfhphbmeiphbcnhehbfdlcgo) - -![Tab for a Cause](https://lh3.googleusercontent.com/ay5bIBCOoNXTg8dfThsivNGEOeZ8kxwdY3t-hAvyZnsB2AIkpNa30j-lnIva4FHGrvn02ikZIqDrqMSCfBSh_QU=w640-h400-e365-rj-sc0x00ffffff) - -## Initab - -[Initab](https://chrome.google.com/webstore/detail/initab/igmbdimmfbpdplpahpapkploofmgaipl) is a new tab extension that replaces Chrome's new tab page with a dashboard of useful tools to make your job as a programmer easier. Get a quick view of your GitHub/GitLab Issue activity, Stack Overflow activity, and much more.. - -![initab](https://lh3.googleusercontent.com/_3KTVnrcKw5lJ9A6ghbLbxB_7OWOLWw4kgzybUTG70EsR3h47JWFDtUCjW4rIwi-9LcKe5uRyIZ8X4WmULWUON7c9A=w640-h400-e365-rj-sc0x00ffffff) - -## GitHub Downloader - -GitHub Downloader is used to download a single file in the GitHub repository by just clicking the file icon. It's very useful when you just need only one file rather than the whole repository. - -![downloader](https://lh3.googleusercontent.com/Z1mC4LqLDEW3KDSxSGJi7ImMcBNSbgvWI_vbhRkwLS6Et_uuNl2YAgPzhgoHLcQ4vS7OdLs6jugn7DIzpyJJBnNG=w640-h400-e365-rj-sc0x00ffffff) - -## 30 Seconds of Knowledge - -Gain new developer skills every time you open a New Tab! Choose which programming languages you want to get better at in extension options and get smarter every time you open a New Tab. All you need is 30 seconds to read and understand snippets of code and improve your knowledge - -[Download](https://chrome.google.com/webstore/detail/30-seconds-of-knowledge/mmgplondnjekobonklacmemikcnhklla) - -![30s](https://lh3.googleusercontent.com/gweVLrLpahu1S7k434r3qlWYm4hgZ_1_q21mrxKfoC5JaUifXh73s_lnWvBUch3e50JnEWTGcvYODGi1OI2y2gDr=w640-h400-e365-rj-sc0x00ffffff) - -## Clip Art Search - -[ClipArt Search](https://chrome.google.com/webstore/detail/clip-art-search/ofjmbhciaojnbhmfhecmjjpfiikfhgpd) – A Best Free Vector Art and Stock Vector Photos Search Engine Over 100 Million Free Vector Images and stock photos, illustrations, vectors. - -![clip art](https://lh3.googleusercontent.com/VL_3fQBlzZ0mLCzOkgnNu4j1NNts-E_lVOuISEKGOemx7cCpdwNg0ZLCZKU5xwGNlfO-EPyUfKNMxVS6Ci6OhRUvDSw=w640-h400-e365-rj-sc0x00ffffff) - -## Devtools for Tailwind CSS - -The [DevTools for Tailwind CSS](https://chrome.google.com/webstore/detail/devtools-for-tailwind-css/mihalpimkkhhigoielhempcamoffhfij) extension allows you to easily work with Tailwind CSS classes on your websites by adding the full power of the JIT engine to your browser. No more missing classes while trying to debug any Tailwind classes on your website. - - - -![tailwind](https://lh3.googleusercontent.com/0A00ltgxAzLlKZLD3HfdTut3rSd8UFSbTZaxs3vPFpeQETV6qADsigp2aYfYUIZthpPZgFapzf08A7XZTkBXAnXb=w640-h400-e365-rj-sc0x00ffffff) - - - -*** - -## Wrapping up - -These were some extensions for this month (October 2022). I have personally used all the above extensions and from my experience, every extension is worth installing. diff --git a/frontend/posts/chrome-extensions-sep-2022.mdx b/frontend/posts/chrome-extensions-sep-2022.mdx deleted file mode 100644 index babc7f4..0000000 --- a/frontend/posts/chrome-extensions-sep-2022.mdx +++ /dev/null @@ -1,111 +0,0 @@ ---- -slug: chrome-extensions-sep-2022 -title: Chrome Extensions of the Month - September 2022 -date: 2022-09-30 -published: true -keywords: css, web development, productivity, chrome, chrome extensions, chrome extensions of the month, september -excerpt: In this article, we are going to look at some of the best extensions of September that you need to install for better productivity. So without further due, let's get into it. -image: https://imgur.com/LJI6AGa.png ---- - - - -In this article, I will suggest to you some of the best extensions you need to install for better productivity that can come in very handy. So without further due, let's get into it. - - -## Fake Data - -It is the most advanced tool for filling forms with fake and random data. Fake Data will help you insert random values in any form field. Generate random names, emails, addresses, phone numbers and many more types of data. Create your own types of data using JavaScript code, which opens unlimited possibilities. You can fill single fields or entire form at once. - -**Download:** [Fake Data](https://chrome.google.com/webstore/detail/fake-data-a-form-filler-y/gchcfdihakkhjgfmokemfeembfokkajj) - -![fake data](https://lh3.googleusercontent.com/yp4wFjod6P3O82_Yb3E1YATftPvIiuBC6WbjXEaMZbrTMUjWhQs37EnuUMxlZ6k9wPpDzvsygobuqGmbShGM0BSfImM=w640-h400-e365-rj-sc0x00ffffff) - - -## GoFullPage - -Capture a screenshot of your current page in entirety and reliably—without requesting any extra permissions! The simplest way to take a full page screenshot of your current browser window. - -**Download:** [GoFullPage](https://chrome.google.com/webstore/detail/gofullpage-full-page-scre/fdpohaocaechififmbbbbbknoalclacl) - -![go full page](https://lh3.googleusercontent.com/Bf0tVoBHMJJd7GoZlcikNS8Sv7qt6I7ZhEfroiQNMOxK5DGjow9tNCDtnUlTYEAeMiQhEYiL2X-mzfr7roiZdX1M=w640-h400-e365-rj-sc0x00ffffff) - - -## JSON Viewer Pro - -A completely free extension to visualize JSON response in awesome Tree and Chart view with great user experience and options. You can also import import local JSON file. - -**Download:** [JSON Viewer Pro](https://chrome.google.com/webstore/detail/json-viewer-pro/eifflpmocdbdmepbjaopkkhbfmdgijcc) - -![json viewer pro](https://lh3.googleusercontent.com/ZBnQqTWxMKL0jmeA851z8p961NSSLullhJbOUW5LVxCDeCSpLLFVrtwmoFMHxN7xD8QyRJ0Spi4ERikx_zwq2fKXhw=w640-h400-e365-rj-sc0x00ffffff) - - -## Project Naptha - -It applies state-of-the-art computer vision algorithms on every image you see while browsing the web. So, you can highlight as well as copy and paste and even edit and translate the text formerly trapped within an image. - -**Download:** [Project Naptha](https://chrome.google.com/webstore/category/ext/38-search-tools) - -![Project Naptha](https://lh3.googleusercontent.com/d-XBvGriwLLTSpLMXpR2MJ4CdBDAOgBoXksBiTRQRQUUsqYefha5CpNng46Ca23XNJjPYVq7gqV-EnpOqEXWDvxmJQ=w640-h400-e365-rj-sc0x00ffffff) - - -## SEO META in 1 CLICK - -It is a tool that displays all meta tags/data and main SEO information clearly. By using this tool, we hope you can better manage and improve your SEO and visibility on Internet - -**Download:** [SEO META in 1 CLICK](https://chrome.google.com/webstore/detail/seo-meta-in-1-click/bjogjfinolnhfhkbipphpdlldadpnmhc) - -![SEO META in 1 CLICK](https://lh3.googleusercontent.com/eK8Xo-x36M3V6yUKXQN9JVnNCKa7DlrKUjKGHOuBaa5c9R-DapEu3x_TS8weYmQ_u9DUj_anqomL6-ND_5qOxgix=w640-h400-e365-rj-sc0x00ffffff) - - -## Minimal Theme for Twitter - -Minimal Theme for Twitter adds an opinionated base layer of styles to clean up the Twitter UI. In addition, it provides extra customization on top to allow you to personalize your Twitter experience. - -**Download:** [Minimal Theme for Twitter](https://chrome.google.com/webstore/detail/minimal-theme-for-twitter/pobhoodpcipjmedfenaigbeloiidbflp) - -![Minimal Theme for Twitter](https://lh3.googleusercontent.com/nDDBP9JBzFMN7Xm1Fbz889u2TZWsPWFTDe_6x4ihvqe7Y1_f2lwHmC-NgQWvb0gxP8vn729Stu762OoHNCK5CWzt=w640-h400-e365-rj-sc0x00ffffff) - - -## Responsive Viewer - -A Chrome extension to show multiple screens in one view. the extension will help front-end developers to test multiple screens while developing responsive websites/applications. - -**Download:** [Responsive Viewer](https://chrome.google.com/webstore/detail/responsive-viewer/inmopeiepgfljkpkidclfgbgbmfcennb) - -![Responsive Viewer](https://cdn.hashnode.com/res/hashnode/image/upload/v1664596378461/kNPyXW6J0.png) - - -## SwiftRead - -Use SwiftRead (formerly known as Spreed), the highest rated and most popular speed reading extension of its kind, to speed read through text in your browser! SwiftRead works on news articles, blog posts, and emails. - -**Download:** [SwiftRead](https://chrome.google.com/webstore/detail/swiftread-read-faster-lea/ipikiaejjblmdopojhpejjmbedhlibno) - -![SwiftRead](https://lh3.googleusercontent.com/s40SpCy_UtCbCX5Dasg4F4xvUhjddTMP0TnLS0RIlKUbrOEzmFUCe3JRXy6c_dn0u74Y5EhWBDFXiakp43rkBtmoew=w640-h400-e365-rj-sc0x00ffffff) - - -## Tabox - -Tabox lets you save all open tabs in the current window to a "Collection". You can click on the collection to open all tabs instantly! The extension's export and import feature is especially useful for working teams that need to share list of work URLs. - -**Download:** [Tabox](https://chrome.google.com/webstore/detail/tabox-save-and-share-tab/bdbliblipiempfdkkkjohnecmeknnpoa) - -![Tabox](https://lh3.googleusercontent.com/H6UqV-KK_jOwLw5Pq7gtMaoT4XGP7AbNbh1yh-aZzoiF1_fXvAOCHxOxOtvajM1QMglBkXRpC9nAUimJQYkdhLkMpw=w640-h400-e365-rj-sc0x00ffffff) - - -## tabExtend - -It can manage your tabs. You can also create notes and to-dos that will be sync between devices. You can also import bookmarks and top sites. Save selected text-snippets. It also supports dark theme. You can also share categories via public link. - - -**Download:** [tabExtend](https://chrome.google.com/webstore/detail/tabextend/ffikidnnejmibopbgbelephlpigeniph) - - - - -*** - -## Wrapping up - -These were some extensions for this month (September 2022). I have personally used all the above extensions and from my experience, every extension is worth installing. diff --git a/frontend/posts/clean-loading-animation.mdx b/frontend/posts/clean-loading-animation.mdx deleted file mode 100644 index 6e2796f..0000000 --- a/frontend/posts/clean-loading-animation.mdx +++ /dev/null @@ -1,71 +0,0 @@ ---- -slug: clean-loading-animation -title: Clean Loading Animation -date: 2021-12-02 -published: true -excerpt: In this article, we are going to build another loading animation with pure CSS. -image: https://imgur.com/NSItFcR.png ---- - -In this article, we are going to build another loading animation with pure CSS. First, let's look at what are we building - - -## Preview -![preview](https://i.imgur.com/npSZAhG.gif) - -Now let's look at the code now - - -## HTML - -```html -
-
-

loading...

-
-``` - -We have the main `div` with class `loading_container` and it has two children `loading` and `h3`. - -- `loading` - It is the main loader for this animation -- `h3` : it is the text which you can see in the preview - -## CSS - - -```css -/* Outer Loading Container */ -.loading_container { - position: relative; - width: 200px; - height: 200px; - border-radius: 150px; -} - -/* Loader */ -.loading { - width: 100%; - height: 100%; - border-radius: 150px; - border-right: 0.3rem solid white; - animation: animate 2s linear infinite; -} - -/* Animation */ -@keyframes animate { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } - -/* loading text */ -.loading_container > h3 { - color: #fff; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); -} -``` - - diff --git a/frontend/posts/colorful-rain-with-js.mdx b/frontend/posts/colorful-rain-with-js.mdx deleted file mode 100644 index 490ffe4..0000000 --- a/frontend/posts/colorful-rain-with-js.mdx +++ /dev/null @@ -1,110 +0,0 @@ ---- -slug: colorful-rain-with-js -title: Colorful Rain with JS -date: 2021-12-15 -published: true -excerpt: In this article, we are going to build a container that generates rain by using JS. And it is a colorful rain with random colors. You can play with colors however you want. -image: https://imgur.com/7KkAOaz.png ---- - -In this article, we are going to build a container that generates rain by using JS. And it is a colorful rain with random colors. You can play with colors however you want. So first, let's see what are we building. - -## Preview - -![preview](https://i.imgur.com/pt4KC8x.gif) - -Now let's look at the code, how we can make that work. - -## HTML - - -```html -
-``` - -## CSS - - -```css -* { - margin: 0; - padding: 0; -} - -.rain-container { - position: relative; - background: #000; - width: 100vw; - height: 100vh; - overflow: hidden; -} - -i { - position: absolute; - height: 120px; - border-radius: 0 0 999px 999px; - animation: animate 5s linear infinite; -} - -@keyframes animate { - 0% { - transform: translateY(-120px); - } - 100% { - transform: translateY(calc(100vh + 120px)); - } -} -``` - -## Javascript - - -```js -const rainContainer = document.querySelector(".rain-container"); - -// background Colors for the raindrop -const background = [ - "linear-gradient(transparent, aqua)", - "linear-gradient(transparent, red)", - "linear-gradient(transparent, limegreen)", - "linear-gradient(transparent, white)", - "linear-gradient(transparent, yellow)", -]; - -const amount = 100; // amount of raindops -let i = 0; - -// Looping and creating the raindrop then adding to the rainContainer -while (i < amount) { - // Creating and Element - const drop = document.createElement("i"); - - // CSS Properties for raindrop - const raindropProperties = { - width: Math.random() * 5 + "px", - positionX: Math.floor(Math.random() * window.innerWidth) + "px", - delay: Math.random() * -20 + "s", - duration: Math.random() * 5 + "s", - bg: background[Math.floor(Math.random() * background.length)], - opacity: Math.random() + 0.2, - }; - - // Setting Styles for raindrop - drop.style.width = raindropProperties.width; - drop.style.left = raindropProperties.positionX; - drop.style.animationDelay = raindropProperties.delay; - drop.style.animationDuration = raindropProperties.duration; - drop.style.background = raindropProperties.bg; - drop.style.opacity = raindropProperties.opacity; - - // Appending the raindrop in the raindrop container - rainContainer.appendChild(drop); - i++; -} -``` - - - -## Wrapping Up - -This is it. You can simply do that with this and you can also take this to the next level. diff --git a/frontend/posts/convert-nextjs-app-to-pwa.mdx b/frontend/posts/convert-nextjs-app-to-pwa.mdx deleted file mode 100644 index bb48f65..0000000 --- a/frontend/posts/convert-nextjs-app-to-pwa.mdx +++ /dev/null @@ -1,189 +0,0 @@ ---- -slug: convert-nextjs-app-to-pwa -title: Convert Next.js app to PWA -date: 2021-10-19 -published: true -excerpt: In this article it is explained that how you can turn your next.js web app to Progressive Web App. -image: https://imgur.com/XfpHK4N.png ---- - -To make Next.js app into PWA, we need the given things - - -- `next-pwa` package -- Service Worker -- Manifest & Icons -- Maskable Icon -- Meta Tags - -## 1\. `next-pwa` package - -To convert your nextjs app into PWA you need to install this package via `npm` or `yarn` -to install this run - - - -```bash -npm i next-pwa # npm -yarn add next-pwa # yarn -``` - -After installation go to your next `next.config.js` as update it as follows - - - -```js -const withPWA = require("next-pwa"); -module.exports = withPWA({ - //...before - pwa: { - dest: "public", - register: true, - skipWaiting: true, - }, - //...after -}); -``` - -## 2\. Service Worker - -We don't need to add external service worker the `next-pwa` will take care of that and it will auto generate the `sw.js` for us so we don't need to do anything in that - - -``` -├── public -| ├── sw.js -``` - -## 3\. Manifest and Icons - -To gernerate Icon and Manifest Go to [PWA Manifest](https://www.simicart.com/manifest-generator.html/) - -![manifest](https://i.imgur.com/ILwfs0f.png) - -Fill all the details and attach the `icon` in 512x512 it will generate the icons and manifest for you and you can download the zip file. - -Go to your public directory and create a folder `icons` and put all the icons in that folder like this - - -``` -├── public -| ├── icons -| | ├── icons.png -``` - -after that create a `manifest.json` in you `public/` which should be look like this - - - -```json -{ - "theme_color": "#000", - "background_color": "#fff", - "display": "standalone", - "scope": "/", - "start_url": "/", - "name": "pwa", - "short_name": "pwa", - "description": "pwa", - "icons": [ - { - "src": "icons/icon-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/icon-256x256.png", - "sizes": "256x256", - "type": "image/png" - }, - { - "src": "icons/icon-384x384.png", - "sizes": "384x384", - "type": "image/png" - }, - { - "src": "icons/icon-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ] -} -``` - -After that we need the `favicon` to get that go to [Favicon Generator](https://favicon.io/favicon-converter/) and upload your main icon and it will generate the rest of icon for you and download the zip after that from that we need only need two icon which is `favicon.ico` and `apple-touch-icon` put them into your `public/` - -Here is the path - - - -``` -├── public -| ├── apple-touch-icon.png -| ├── favicon.ico -| ├── icons -| | ├── icon-192x192.png -| | ├── icon-256x256.png -| | ├── icon-384x384.png -| | ├── icon-512x512.png -| | └── maskable.png -| ├── manifest.json -``` - -## 4. Maskable Icon - -To make the maskabel icon we need to visit [Maskable Editor](https://maskable.app/editor) and upload your icon and edit it -![masakable](https://i.imgur.com/WBk9a7y.png) - -after editing export the icon but be care full with the ratio -always choose the square ration and remember the ratio because we will need it in the `manifest` - -![sizes](https://i.imgur.com/iAIctjQ.png) - -After downloading the `icon` put it into the `public/icons/` - - -``` -├── public -| ├── icons -| | └── maskable.png -``` - -and add that to the `manifest.json` - - -```json - -"icons": [ - // ... - { - "src": "maskable.png", - "sizes": "48x48", - "type": "image/x-icon", - "purpose": "maskable" - }, - //... -] -``` - -Here you need to specify the size of the maskable image if the image size is `512x512` then in the `json` it should be `"sizes": "512x512"` - -## 5. Meta Tags - -Now to get all this work we need some meta tags put them wher is the `Head` of your application, which are given below - - -```html - - - - - - - Title of the project - - - -; -``` - -After all that Go to the Developer Console and Generate Resport for PWA in Lighthouse you will see the PWA and installable badge. - -![pwa](https://i.imgur.com/Txni6L6.png) - -You need to push your website with `https` you can use [Vercel](https://vercel.com/) or [Netlify](https://www.netlify.com/) diff --git a/frontend/posts/countdown-loading-with-js.mdx b/frontend/posts/countdown-loading-with-js.mdx deleted file mode 100644 index 7309a7e..0000000 --- a/frontend/posts/countdown-loading-with-js.mdx +++ /dev/null @@ -1,116 +0,0 @@ ---- -slug: countdown-loading-with-js -title: Countdown Loading with JS -date: 2021-12-07 -published: true -excerpt: In this article, we are building a countdown with the help of Javascript and CSS. It can also be used as the loading countdown. -image: https://imgur.com/X07dL4j.png ---- - -In this article, we are building a countdown with the help of Javascript and CSS. It can also be used as the loading countdown. Let's first look at what are we building - - -## Preview -![preview](https://i.imgur.com/tYadwyQ.gif) - -Now you know how it will look like, So let's look at the code now - - -## HTML - - -```html -
-
-
-``` - -In the HTML code, the `card` class is the main container and it has one section as child - -- `number` : it is the main countdown number or value - -## CSS - -```css -:root { - --background-color: #0e1538; - --text-color: #fff; - --font: sans-serif; -} - -* { - margin: 0; - padding: 0; -} - -body { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - background-color: var(--background-color); - font-family: var(--font); -} - -/* main Card */ -.card { - width: 200px; - height: 275px; - position: relative; - user-select: none; - cursor: pointer; - transition: 0.3s ease-in-out; -} - -/* Linear Background by using ::before */ -.card::before { - content: ""; - position: absolute; - top: -4px; - left: -4px; - bottom: -4px; - right: -4px; - transform: skew(2deg, 4deg); - background: linear-gradient(315deg, #00ccff, #0e1538, #d400d4); -} - -/* countdown number */ -.card > .number { - width: 100%; - height: 100%; - position: absolute; - z-index: 10; - font-size: 8em; - display: grid; - place-items: center; - background-color: var(--background-color); - color: var(--text-color); -} - -.card:hover { - transform: scale(1.1); - box-shadow: 0 0 200px rgba(225, 225, 225, 0.3); - transform: rotate(720deg); -} -``` - -Now the main part is the javascript in order to run this properly. - -## Javascript - - -```js -var number = document.querySelector(".number"); -var count = 10; - -// Countdown Interval which runs on every 1s -var countdownInterval = setInterval(() => { - // if count is less than or equal to 1 then clear the Interval - count <= 1 && clearInterval(countdownInterval); - number.textContent = `0${--count}`; -}, 1000); -``` - - - -## Wrapping up - -This is the countdown made by using Javascript and CSS you can use this in your project however you want. diff --git a/frontend/posts/countries-flags.mdx b/frontend/posts/countries-flags.mdx deleted file mode 100644 index 709861e..0000000 --- a/frontend/posts/countries-flags.mdx +++ /dev/null @@ -1,843 +0,0 @@ ---- -slug: countries-flags -title: "I made all countries' flags using HTML & CSS" -date: 2022-08-28 -published: true -keywords: css, webdev, countries flags, css3, flags, html -excerpt: In this article, I have made all (195) countries' flags by just using HTML and CSS. At the end of the article. You will have a special Bonus (you have to reach the end of the article to see it). -image: https://imgur.com/YND4dfM.png ---- - - - - -This article is special, you may ask why. It is because, in my last article [CSS Flag: India](https://dev.to/j471n/css-flag-india-1bg3), I said- - ->I am starting a new series called **CSS Flag**. Where I'll be making different countries' flags starting with India. - -I asked myself "Why don't I make an article which will have all 195 countries' flags instead of creating 195 articles?". Well, Here I am. - -In this article, I have made all countries' flags by just using HTML and CSS. At the end of the article. You will have a special Bonus (you have to reach the end of the article to see it). - - - - - - The idea of this series came up in my mind on 15 August as I mention in my previous article "CSS Flag: India". - - I don't want to hurt anyone's feelings or sentiments through this article. As It could offend some people if their country's flag is slightly off point. I respect every country's flag and its people. If you find any mistakes in any of the flags then please let me know. I'll fix it as soon as possible. - - If there are some issues with Codepen in mobile devices try to switch to the desktop mode or visit through desktop/laptop browsers. - - I have used images (PNG) in some of the flags as well because it was kind of complex to create them. - - The size of each flag is 300x200 pixels. Some of the countries' flags use different sizes. So It could be a little bit different due to the size difference. - - - -## Afghanistan - - - -## Albania - - - -## Algeria - - - -## Andorra - - - -## Angola - - - -## Antigua and Barbuda - - - -## Argentina - - - -## Armenia - - - -## Australia - - - -## Austria - - - -## Azerbaijan - - - -## Bahamas - - - -## Bahrain - - - -## Bangladesh - - - -## Barbados - - - -## Belarus - - - -## Belgium - - - -## Belize - - - -## Benin - - - -## Bhutan - - - -## Bolivia - - - -## Bosnia and Herzegovina - - - -## Botswana - - - -## Brazil - - - -## Brunei - - - -## Bulgaria - - - -## Burkina Faso - - - -## Burundi - - - -## Cambodia - - - -## Cameroon - - - -## Canada - - - -## Cape Verde - - - -## Central African - - - -## Chad - - - -## Chile - - - -## China - - - -## Colombia - - - -## Comoros - - - -## Democratic Republic of the Congo - - - -## Republic of the Congo - - - -## Costa Rica - - - -## Croatia - - - -## Cuban - - - -## Cyprus - - - -## Czechia - - - -## Denmark - - - -## Djibouti - - - -## Dominica - - - -## Dominican - - - -## East Timor - - - -## Ecuador - - - -## Egypt - - - -## El Salvador - - - -## Equatorial Guinea - - - -## Eritrea - - - -## Estonia - - - -## Eswatini - - - -## Ethiopia - - - -## Fiji - - - -## Finland - - - -## France - - - -## Gabon - - - -## Gambia - - - -## Georgia - - - -## Germany - - - -## Ghana - - - -## Greece - - - -## Grenada - - - -## Guatemala - - - -## Guinea - - - -## Guinea-Bissau - - - -## Guyana - - - -## Haiti - - - -## Honduras - - - -## Hungary - - - -## Iceland - - - -## India - - - -## Indonesia - - - -## Iran - - - -## Iraq - - - -## Ireland - - - -## Israel - - - -## Italy - - - -## Ivory Coast - - - -## Jamaica - - - -## Japan - - - -## Jordan - - - -## Kazakhstan - - - -## Kenya - - - -## Kiribati - - - -## North Korea - - - -## South Korea - - - -## Kuwait - - - -## Kyrgyzstan - - - -## Laos - - - -## Latvia - - - -## Lebanon - - - -## Lesotho - - - -## Liberia - - - -## Libya - - - -## Liechtenstein - - - -## Lithuania - - - -## Luxembourg - - - -## Madagascar - - - -## Malawi - - - -## Malaysia - - - -## Maldives - - - -## Mali - - - -## Malta - - - -## Marshall Islands - - - -## Mauritania - - - -## Mauritius - - - -## Mexico - - - -## Micronesia - - - -## Moldova - - - -## Monaco - - - -## Mongolia - - - -## Montenegro - - - -## Morocco - - - -## Mozambique - - - -## Myanmar - - - -## Namibia - - - -## Nauru - - - -## Nepal - - - -## Netherland - - - -## New Zealand - - - -## Nicaragua - - - -## Niger - - - -## Nigeria - - - -## North Macedonia - - - -## Norway - - - -## Oman - - - -## Pakistan - - - -## Palau - - - -## Palestine - - - -## Panama - - - -## Papua New Guinea - - - -## Paraguay - - - -## Peru - - - -## Philippines - - - -## Poland - - - -## Portugal - - - -## Qatar - - - -## Romania - - - -## Russia - - - -## Rwanda - - - -## Saint Kitts and Nevis - - - -## Saint Lucia - - - -## Saint Vincent and the Grenadines - - - -## Samoa - - - -## San Marino - - - -## São Tomé and Príncipe - - - -## Saudi Arabia - - - -## Senegal - - - -## Serbia - - - -## Seychelles - - - -## Sierra Leone - - - -## Singapore - - - -## Slovakia - - - -## Slovenia - - - -## Solomon Islands - - - -## Somalia - - - -## South Africa - - - -## South Sudan - - - -## Spain - - - -## Sri Lanka - - - -## Sudan - - - -## Suriname - - - -## Sweden - - - -## Switzerland - - - -## Syria - - - -## Taiwan - - - -## Tajikistan - - - -## Tanzania - - - -## Thailand - - - -## Togo - - - -## Tonga - - - -## Trinidad and Tobago - - - -## Tunisia - - - -## Turkey - - - -## Turkmenistan - - - -## Tuvalu - - - -## Uganda - - - -## Ukraine - - - -## United Arab Emirates - - - -## United Kingdom - - - -## United States - - - -## Uruguay - - - -## Uzbekistan - - - -## Vanuatu - - - -## Vatican City - - - -## Venezuela - - - -## Vietnam - - - -## Yemen - - - -## Zambia - - - -## Zimbabwe - - - - - -## Bonus - -As I told you at the start of the article I'll attach a bonus section. Here it is. You can find all countries' flags in the following Codepen (All in One): - - - - -## Wrapping up - -I have used images in some of the flags as well because it was kind of complex to create them. You can extend your support by [Buying me a Coffee](https://buymeacoffee.com/j471n). I'll see you in the next one. - - -## References - -- [Wikipedia](https://en.wikipedia.org/wiki/Gallery_of_sovereign_state_flags) -- [seekflag.com](https://seekflag.com/) - - -Thumbnail image by [@krisetya](https://unsplash.com/@krisetya) at [Unsplash](https://unsplash.com/photos/k0Jo8m6DO6k) - - - - - - diff --git a/frontend/posts/create-bash-script-and-file-permissions.mdx b/frontend/posts/create-bash-script-and-file-permissions.mdx deleted file mode 100644 index 04b46a5..0000000 --- a/frontend/posts/create-bash-script-and-file-permissions.mdx +++ /dev/null @@ -1,110 +0,0 @@ ---- -slug: create-bash-script-and-file-permissions -title: Create Bash Script & File Permissions -date: 2022-04-22 -published: true -excerpt: In this article, I have talked about how you can create a bash script and what are the permission for the files, and how you can manipulate those permissions. -image: https://imgur.com/4Dpq4wW.png ---- - -## Let's Recap - -In the previous [article](/blogs/getting-started-with-bash) we have learned what Bash is, How to install bash and how it functions on the command line. In this article, we are going to learn the bash scripting, and how we can create a bash script and how we can run it on our system. - -## Let's Create Bash Script - -### Step-1: Create the Script - -- To create an empty bash script, first, change the directory in which you want to save your script using **cd** command. -- Use **touch** command to create a bash script as shown below. - - -```bash -touch first.sh -``` - -**Note-** Her `.sh` is an extension that you have to provide for execution. - -### Step-2: Edit the Script - -- To edit the content of this file you can run `nano first.sh` it will open command line editor and you can edit your bash file. - -![nano first.sh](https://i.imgur.com/bLIZ2se.gif) - -- Now we need to add some stuff in this file first put `#! /bin/bash` in the top of the file. - - `#!` - is referred to as the **_shebang_** and re st of the line is the path to the interpreter specifying the location of bash shell in our operating system. -- Now we add `echo Hello World` in the file in next line so your bash script will look like- - - -```bash -#! /bin/bash -echo Hello World -``` - -- After you are done then just save the file, to save the file in windows follow these steps- - - Press `Ctrl + X` first. - - Then press `Y` - - Then press `Enter` - - Now your changes have been saved. - ![save a file](https://i.imgur.com/99B5757.gif) - -### Step-3: Run the Script - -To do the type the following in the command line- - - -```bash -./.sh -``` - -After runnnig this your screen will look like this- -![permission](https://i.imgur.com/6zQdAwV.gif) -It shows _permission denied_. It is because user don't have permission to execute the file right now. For that run the following command in the terminal- - - -```bash -chmod +x first.sh -``` - -**Note-** We will talk about the permissions in a second. For now it just changes the execution permission to true. `+` stands to add the permission and `x` stands for running or execution permission. - -Now after authourizing execution run the previous command again which was `./.sh` in our case it is `./first.sh`. - -![runfirst.sh](https://i.imgur.com/GuZTZOU.png) -As you can see in the above image we have successfully printed `Hello World`. - -## File Permissions - -Linux-based Operating System requires file permissions to secure its filesystem. There are three types of permissions associated with the files as follows- - -- **Read -** Permission to view the content of the file. represented as (`r`). -- **Write -** Permission to modify the file content. represented as (`w`). -- **Execute -** Permission to run the file or script. represented as (`x`). - -### Changing Permissions - -You can change the permission of the file by using `chmod` command. Syntax of `chmod` is - - - -```bash -chmod [class][operator][permission] file_name -``` - -- **Class** is represented by the indicators - **u, g, o,** and **a**, where- - - - `u = user` - - `g = group` - - `o = other` - - `a = all the classes` - -- **Operator** ( `+` or `-` ) is used to add or remove the permission. -- **Permissions** have three types as follows- - - `r = reading the script` - - `w = modifying the script` - - `x = running the script` - -Now I guess everything makes sense when we give execution permission (`+x`) to the `first.sh` script. - -## Wrapping Up - -Firstly, we learn how to create the bash script, how to edit, and how to run the script. Also, we learned about the different file permission in the Bash and how to modify them. diff --git a/frontend/posts/creative-hover-menu-with-css.mdx b/frontend/posts/creative-hover-menu-with-css.mdx deleted file mode 100644 index 457cf08..0000000 --- a/frontend/posts/creative-hover-menu-with-css.mdx +++ /dev/null @@ -1,142 +0,0 @@ ---- -slug: creative-hover-menu-with-css -title: Creative Hover Menu with CSS -date: 2021-12-11 -published: true -excerpt: In this article, we are going to make the navigation menu, but it will be in the verticle form, I'll recommend you to use this as the full page menu.first-letter -image: https://imgur.com/8ilaL1R.png ---- - -In this article, we are going to make the navigation menu, but it will be in the verticle form, I'll recommend you to use this as the full page menu, we are not going to talk about how to toggle the hamburger and that stuff if you want me to explain that, then tell me in the comment section. I can cover that in a separate article. First Let's see what are we building- - -## Preview - -![preview](https://i.imgur.com/qeWj4ly.gif) - -Before we look at the whole code first let me give you an overview of some effects. - -**Glow Effect** -As you can see the glow effect in the text when you hover on it. this can be achieved by the following CSS property- - - -```css -text-shadow: 0 0 7px #fff, 0 0 10px #fff, 0 0 21px #fff, 0 0 42px #0fa, - 0 0 82px #0fa, 0 0 92px #0fa, 0 0 102px #0fa, 0 0 151px #0fa; -``` - -**Text Spaceing Effect** -I've used the animation with the letter-spacing property. you can achieve that by the following code- - - -```css -@keyframes animate { - from { - opacity: 0; - letter-spacing: 50px; - } - to { - opacity: 1; - letter-spacing: 5px; - } -} -``` - -## HTML - - -```html - -``` - -In HTML we have the `.navbar` which wraps the whole navigation menu then we have the unordered list in which we have the `li` and inside that we have the anchor (`a`) tag which also contains the two paragraphs (`p`) tag one is the bigger one (`.link`) and the other one is hidden (`.hidden_link`) which will only be visible on `hover`. - -## CSS - - -```css -/* Default values */ -* { - margin: 0; - padding: 0; -} -ul > li { - list-style: none; -} - -a { - text-decoration: none; -} - -/* Relative navigation list item */ -.nav_list > li { - position: relative; - margin: 8px 0; -} - -.nav_list > li > a { - color: #fff; - text-align: center; -} - -.nav_list > li > a p { - text-transform: uppercase; -} - -.nav_list > li > a > .link { - font-size: 2rem; - transition: opacity 300ms ease-in-out; -} - -.nav_list > li > a .hidden_link { - position: absolute; - z-index: 10; - left: 50%; - top: 50%; - transform: translate(-50%, -40%); - color: #fff; - background: transparent; - text-align: center; - text-shadow: 0 0 7px #fff, 0 0 10px #fff, 0 0 21px #fff, 0 0 42px #0fa, - 0 0 82px #0fa, 0 0 92px #0fa, 0 0 102px #0fa, 0 0 151px #0fa; - - /* animation "from" */ - opacity: 0; - letter-spacing: 50px; - pointer-events: none; -} - -/* Low opacity of main Link */ -.nav_list > li > a:hover > .link { - opacity: 0.3; -} - -/* Show the Hidden link with animation */ -.nav_list > li > a:hover > .hidden_link { - animation: show-link 400ms ease-in-out forwards; -} - -@keyframes show-link { - to { - opacity: 1; - letter-spacing: 5px; - pointer-events: all; - } -} -``` - - - -## Conclusion - -Now you can make this by yourself as well. you should now make the hamburger or the navigation toggle button and display this navbar. with some sliding animation maybe. \ No newline at end of file diff --git a/frontend/posts/css-battle-1.mdx b/frontend/posts/css-battle-1.mdx deleted file mode 100644 index 9d9aa9e..0000000 --- a/frontend/posts/css-battle-1.mdx +++ /dev/null @@ -1,114 +0,0 @@ ---- -slug: css-battle-1 -title: "CSS Battle: #1 - Simply Square" -date: 2022-06-25 -published: true -excerpt: In this article, I will solve a `Simply Square` CSS Challenge on CSS Battle. -image: https://imgur.com/gCpXLrC.png ---- - - -In this article, I will solve a [Simply Square](https://cssbattle.dev/play/1) CSS Challenge on [CSS Battle](https://cssbattle.dev/). Let's look at the [problem](https://cssbattle.dev/play/1) first. - -## Problem - -We need to create the following container by using CSS Properties only: -![Simply Square](https://cssbattle.dev/targets/1.png) - -## Solution - -So now look at the Solution and how we are going to achieve this. - -### HTML - -First, create an HTML for this, I've used the two containers to do that. you can use [Pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes) as well. - - -```html -
-
-
-``` - - -### CSS - -Now let's style the containers. - - -```css -.box { - width: 400px; - height: 300px; - background: #5d3a3a; -} -``` -After applying the CSS to `.box` it will look like this: - -![first](https://imgur.com/H2t0ugm.png) - -Now we style the child `div` of `.box`. - - -```css {7-11} -.box { - width: 400px; - height: 300px; - background: #5d3a3a; -} - -.box div { - background: #b5e0ba; - width: 200px; - height: 200px; -} -``` - -After applying the CSS, here's the result: - -![result](https://imgur.com/IyYXGkR.png) - -Now the problem is solved by simply the above styles. - - - -## Alternate Solution - -This is one more way you can do this even though there are many ways, I like the following: - -### HTML - -```html -
-``` - -### CSS - - -```css -body{ - background: #5d3a3a; - margin:0; - -} -div { - width: 200px; - height: 200px; - background: #b5e0ba; -} -``` - - - Minify the code or CSS by using any [CSS Minifier](https://www.minifier.org/). - It helps you to reduce the characters in the code that will increase score. - - -## Wrapping up - -If you like this then don't forget to ❤️ it. And I'll see you in the next article. See you soon. - - - \ No newline at end of file diff --git a/frontend/posts/css-battle-10.mdx b/frontend/posts/css-battle-10.mdx deleted file mode 100644 index c601435..0000000 --- a/frontend/posts/css-battle-10.mdx +++ /dev/null @@ -1,98 +0,0 @@ ---- -slug: css-battle-10 -title: "CSS Battle: #10 - Cloaked Spirits" -date: 2022-07-27 -published: true -excerpt: In this article, I will solve a `Cloaked Spirits` CSS Challenge on CSS Battle. -image: https://imgur.com/JyZLTob.png ---- - - -In this article, I will solve a [Cloaked Spirits](https://cssbattle.dev/play/10) CSS Challenge on [CSS Battle](https://cssbattle.dev/). Let's look at the problem first. - -## Problem - -We need to create the following container by using CSS Properties only: -![Cloaked Spirits](https://cssbattle.dev/targets/10.png) - -## Solution - -So now look at the Solution and how we are going to achieve this. - -*Video's Code is a little bit different because in the following CSS code I've used `aspect-ratio` just to reduce character and to match the width and height which is not mentioned in the video. (Video code also works fine)* - -### HTML - - -```html -

-

-``` - - -### CSS - -Now let's style the containers. - - -```css -* { - margin: 0; - background: #62306d; -} -body { - display: grid; - place-items: center; -} -p { - position: fixed; - aspect-ratio: 1; -} -[b] { - width: 100; - background: #f7ec7d; - box-shadow: -100px 100px #f7ec7d, - 100px 100px #f7ec7d, - 0 100px #f7ec7d; -} -[c] { - width: 60; - background: #aa445f; - border-radius: 1in; - bottom: 170; - box-shadow: 0 0 0 20px #e38f66, - 100px 100px #e38f66, - 100px 100px 0 20px #aa445f, - -100px 100px #e38f66, - -100px 100px 0 20px #aa445f; -} -``` - - - - - Note: In CSS Battle you can use `100` instead of `100px`. You don't need to define `px` in CSS. However, if you are using `rem` or `%`, you need to pass them separately. That's why in the above CSS code there are no units mostly. For more info [visit here](https://cssbattle.dev/tips). - - - - Minify the code or CSS by using any [CSS Minifier](https://www.minifier.org/). It helps you to reduce the characters in the code which will increase the score. - - - -**Minified Version:** -```text -