diff --git a/accounts/api/serializers.py b/accounts/api/serializers.py index 44c7f0e..38368fe 100644 --- a/accounts/api/serializers.py +++ b/accounts/api/serializers.py @@ -149,6 +149,7 @@ class ProfileSerializer(serializers.ModelSerializer): "principal_type", "principal_type_name", "profile_photo", + "player_id", "first_name", "last_name", "email", @@ -284,3 +285,7 @@ class ProfilePhotoSerializer(serializers.ModelSerializer): class Meta: model = IAmPrincipal fields = ("email", "profile_photo", "first_name") + + +class PlayerIDSerializer(serializers.Serializer): + player_id = serializers.CharField(max_length=255) diff --git a/accounts/api/urls.py b/accounts/api/urls.py index 9e18fe7..9a62ae4 100644 --- a/accounts/api/urls.py +++ b/accounts/api/urls.py @@ -23,8 +23,16 @@ urlpatterns = [ path('profile/password-reset/', views.ProfilePasswordResetView.as_view(), name='password_reset'), # path('profile/kyc/', views.KycDocumentView.as_view(), name='pofile_kyc'), + path('player-id/add/', views.IAmPrincipalPlayerIDAPIView.as_view(), name='add_player_id'), + path('google-login/', views.GoogleLoginAPIView.as_view(), name='google-login'), + path( + "apple-login/", + views.decode_apple_token, + name="apple_login", + ), + diff --git a/accounts/api/views.py b/accounts/api/views.py index 4ff4d20..83333ff 100644 --- a/accounts/api/views.py +++ b/accounts/api/views.py @@ -1,11 +1,17 @@ +import json from django.db import transaction +from django.http import JsonResponse from django.shortcuts import get_object_or_404 from django.utils import timezone +import jwt from rest_framework import status from rest_framework.views import APIView from rest_framework_simplejwt.tokens import RefreshToken from django.conf import settings import requests +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_http_methods + # from .authenticate import authticate_with_otp_and_passsword from accounts.models import ( @@ -32,6 +38,7 @@ from .serializers import ( # RegistrationPasswordSerializer, # PhoneSerializer, EmailSerializer, + PlayerIDSerializer, RegistrationPasswordSerializer, RegistrationSerializer, ReferralCodeSerializer, @@ -156,6 +163,7 @@ class RegistrationDetailsView(APIView): email = request.data.get("email") print("email: ", email) referral_code = request.data.get("referral_code") + player_id = request.data.get("player_id") print("referral_code", referral_code) # Filter the user instance by phone number through reusable function @@ -177,6 +185,8 @@ class RegistrationDetailsView(APIView): try: instance = serializer.save() instance.register_complete = True + if player_id: + instance.player_id = player_id instance.save() except Exception as e: print("instance: E-", e) @@ -499,7 +509,9 @@ class ProfileView(APIView): def post(self, request, *args, **kwargs): instance = self.get_object() - serializer = self.serializer(instance, data=request.data) + serializer = self.serializer( + instance, data=request.data, context={"request": request} + ) if not serializer.is_valid(): error_response = { "status": status.HTTP_400_BAD_REQUEST, @@ -514,7 +526,7 @@ class ProfileView(APIView): return ApiResponse.error( message=constants.INTERNAL_SERVER_ERROR, errors=str(e) ) - return ApiResponse.success(message=constants.SUCCESS) + return ApiResponse.success(message=constants.SUCCESS, data=serializer.data) class ProfilePasswordResetView(APIView): @@ -618,6 +630,7 @@ class GoogleLoginAPIView(APIView): def post(self, request, *args, **kwargs): access_token = request.data.get("access_token") principal_type = request.data.get("principal_type") + referral_code = request.data.get("referral_code") if not access_token or not principal_type: return Response( {"error": "Access token & Principal Type is required"}, @@ -694,6 +707,47 @@ class GoogleLoginAPIView(APIView): principal.save() print("Created new user") message = "Details updated" + + try: + ReferralCode.create_referral_code_for_user_manager( + principal=principal, principal_type=principal.principal_type + ) + except Exception as e: + print("ReferralCode: E-", e) + error_response = { + "status": status.HTTP_400_BAD_REQUEST, + "message": constants.FAILURE, + "errors": str(e), + } + return ApiResponse.error(**error_response) + + if referral_code: + already_register_through_referral = ReferralRecord.objects.filter( + referred_principal=principal + ).exists() + if not already_register_through_referral: + try: + whos_referral_code = ReferralCode.objects.get( + referral_code=referral_code + ) + except Exception as e: + print("whos_referral_code: E-", e) + error_response = { + "status": status.HTTP_404_NOT_FOUND, + "message": constants.RECORD_NOT_FOUND, + "errors": str(e), + } + return ApiResponse.error(**error_response) + + # principal_type = IAmPrincipalType.objects + + ReferralRecord.objects.create( + referrer_principal=whos_referral_code.principal, # principal id of the User who invited + referred_principal=principal, # principal id of the User who join through invitation + principal_type=whos_referral_code.principal_type, # principal type of the user who invited + is_completed=True, + ) + token_data = generate_token_and_user_data(principal) token_data["type"] = str(principal.principal_type) @@ -718,3 +772,138 @@ class GoogleLoginAPIView(APIView): "error": error_details, "status": response.status_code, } + + +# Apple's public keys URL +APPLE_PUBLIC_KEYS_URL = "https://appleid.apple.com/auth/keys" + +# Your client ID +AUDIENCE = "com.app.goodTimes" + + +@csrf_exempt +@require_http_methods(["POST"]) +def decode_apple_token(request): + try: + data = request.POST + identity_token = data.get("token") + if not identity_token: + return JsonResponse({"error": "Token is required"}, status=400) + + principal_type = data.get("principal_type") + principal_type_instance = IAmPrincipalType.objects.filter( + name=principal_type + ).first() + if principal_type_instance is None: + return JsonResponse( + {"error": "No Principal Type Exists"}, + status=status.HTTP_400_BAD_REQUEST, + content_type="application/json", + ) + + # Fetch Apple's public keys + # Note: You might want to cache these keys and update them periodically rather than fetching them with every request + apple_keys_response = requests.get(APPLE_PUBLIC_KEYS_URL) + apple_keys = apple_keys_response.json() + + # Decode the token + # Note: This is a simplified example; you should handle the selection of the key and any potential errors during decoding + header_data = jwt.get_unverified_header(identity_token) + kid = header_data["kid"] + apple_key = next((key for key in apple_keys["keys"] if key["kid"] == kid), None) + if apple_key is None: + return JsonResponse({"error": "Invalid key ID"}, status=400) + + # Construct the public key + public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(apple_key)) + + # Decode the token + decoded = jwt.decode( + identity_token, public_key, algorithms=["RS256"], audience=AUDIENCE + ) + + # Check if there was an error in fetching user data + if "error" in decoded: + error_message = decoded.get("error", {}).get( + "error_description", "An error occurred while fetching user data." + ) + return JsonResponse( + {"error": error_message}, + status=decoded.get("status", status.HTTP_400_BAD_REQUEST), + ) + + print("decoded: ", decoded) + + email = decoded.get("email") + apple_id = decoded.get("sub") + print("apple_id: ", apple_id) + + # Checking if essential values are present and valid + if not apple_id: + # Return an error response if either 'email' or 'sub' is missing or empty + return JsonResponse( + {"error": "The token is missing required information."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + with transaction.atomic(): + apple_source, _ = IAmPrincipalSource.objects.get_or_create(name="apple") + principal = IAmPrincipal.objects.filter(apple_id=apple_id).first() + print("principal: ", principal) + if principal: + principal.email_verified = True + principal.principal_source = apple_source + principal.apple_id = apple_id + # Update any other fields that might change on each login + principal.save() + print("Updated existing user") + message = "Already Registered and Verified User" + else: + defaults = { + "email": f"{apple_id}@gmail.com", + "register_complete": True, + "email_verified": True, + "username": apple_id, # Or generate a unique username if necessary + "principal_source": apple_source, + } + + defaults["principal_type"] = principal_type_instance + defaults["apple_id"] = apple_id + principal = IAmPrincipal(**defaults) + default_password = f"SEMTG{apple_id[::-1]}" + principal.set_password(default_password) + principal.save() + print("Created new user") + message = "Registered Successfully" + token_data = generate_token_and_user_data(principal) + token_data["type"] = str(principal.principal_type) + + return JsonResponse({"message": message, "data": token_data}, status=200) + + # return JsonResponse(decoded) + except Exception as e: + return JsonResponse({"error": str(e)}, status=400) + + +class IAmPrincipalPlayerIDAPIView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def post(self, request, *args, **kwargs): + serializer = PlayerIDSerializer(data=request.data) + print("serializer: ", serializer) + + if serializer.is_valid(): + principal = request.user + principal.player_id = serializer.validated_data["player_id"] + principal.save() + return ApiResponse.success( + status=status.HTTP_200_OK, + message=constants.SUCCESS, + data=serializer.data, + ) + return ApiResponse.error( + status=status.HTTP_400_BAD_REQUEST, + message=constants.FAILURE, + errors=serializer.errors, + ) diff --git a/accounts/fixture_script.py b/accounts/fixture_script.py index 594d897..eecfd7d 100644 --- a/accounts/fixture_script.py +++ b/accounts/fixture_script.py @@ -14,6 +14,7 @@ from accounts.resource_action import ( RESOURCE_MANAGE_DASHBOARD, RESOURCE_MANAGE_IAM, RESOURCE_MANAGE_CUSTOMER, + RESOURCE_MANAGE_NOTIFICATIONS, RESOURCE_MANAGE_WALLET, RESOURCE_MANAGE_PAYMENT, RESOURCE_MANAGE_EVENTS, @@ -269,4 +270,16 @@ fixture_data = [ "action": [1, 2, 3, 4], }, }, + { + "model": "accounts.iamappresource", + "pk": 13, + "fields": { + "name": RESOURCE_MANAGE_NOTIFICATIONS, + "label": RESOURCE_MANAGE_NOTIFICATIONS, + "slug": RESOURCE_MANAGE_NOTIFICATIONS, + "created_on": "2023-09-28T16:17:42.815", + "modified_on": "2023-09-28T16:17:42.815", + "action": [1, 2, 3, 4], + }, + }, ] diff --git a/accounts/fixtures/resource_action_fixture.json b/accounts/fixtures/resource_action_fixture.json index a671277..2c7fa7a 100644 --- a/accounts/fixtures/resource_action_fixture.json +++ b/accounts/fixtures/resource_action_fixture.json @@ -301,5 +301,22 @@ 4 ] } + }, + { + "model": "accounts.iamappresource", + "pk": 13, + "fields": { + "name": "manage_notifications", + "label": "manage_notifications", + "slug": "manage_notifications", + "created_on": "2023-09-28T16:17:42.815", + "modified_on": "2023-09-28T16:17:42.815", + "action": [ + 1, + 2, + 3, + 4 + ] + } } ] \ No newline at end of file diff --git a/accounts/resource_action.py b/accounts/resource_action.py index 73a5a5f..72413c4 100644 --- a/accounts/resource_action.py +++ b/accounts/resource_action.py @@ -23,6 +23,7 @@ RESOURCE_MANAGE_REPORTS = "manage_reports" RESOURCE_MANAGE_SUBSCRIPTIONS = "manage_subscriptions" RESOURCE_MANAGE_FEEDBACK = "manage_feedback" RESOURCE_MANAGE_REFERRALS = "manage_referrals" +RESOURCE_MANAGE_NOTIFICATIONS = "manage_notifications" # These constants are used solely for managing the active and inactive state of pages diff --git a/goodtimes/constants.py b/goodtimes/constants.py index 2aa0665..ea24367 100644 --- a/goodtimes/constants.py +++ b/goodtimes/constants.py @@ -43,10 +43,10 @@ PASSWORD_CHANGE_FAILURE = "Password change failed. Please try again later." # Mobile OTP Related Constants -PHONE_NUMBER_EXISTS = "This phone number is already in use." +PHONE_NUMBER_EXISTS = "This email is already in use." EMAIL_EXISTS = "This email is already in use." PHONE_NUMBER_NOT_REGISTERED = "This phone number is not registered." -EMAIL_NOT_REGISTERED = "This phone number is not registered." +EMAIL_NOT_REGISTERED = "This email is not registered." PHONE_NUMBER_NOT_FOUND = "Phone number not found." PHONE_FIELD_IS_REQUIRED = "Phone field is required." PHONE_NUMBER_INVALID = 'Invalid phone number.' diff --git a/goodtimes/settings/base.py b/goodtimes/settings/base.py index af26e01..ee10d58 100644 --- a/goodtimes/settings/base.py +++ b/goodtimes/settings/base.py @@ -176,6 +176,7 @@ AUTHENTICATION_BACKENDS = [ # rest framework permission and authentication settings # https://www.django-rest-framework.org/api-guide/permissions/#setting-the-permission-policy REST_FRAMEWORK = { + "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), # Use Django's standard `django.contrib.auth` permissions, # or allow read-only access for unauthenticated users. "DEFAULT_PERMISSION_CLASSES": [ @@ -303,6 +304,9 @@ SIMPLE_JWT = { STRIPE_SECRET_KEY = env.str("STRIPE_SECRET_KEY") STRIPE_PUBLISH_KEY = env.str("STRIPE_PUBLISH_KEY") +ONE_SIGNAL_APP_ID = env.str("ONE_SIGNAL_APP_ID") +ONE_SIGNAL_API_KEY = env.str("ONE_SIGNAL_API_KEY") + CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", @@ -327,26 +331,4 @@ CRONJOBS = [ # ("0 9 * * 1-5", "manage_games.cron.update_game_status_live"), ] -# GOOGLE_OAUTH2_CLIENT_ID = env.str("DJANGO_GOOGLE_OAUTH2_CLIENT_ID", default="") -# GOOGLE_OAUTH2_CLIENT_SECRET = env.str("DJANGO_GOOGLE_OAUTH2_CLIENT_SECRET", default="") -# GOOGLE_OAUTH2_PROJECT_ID = env.str("DJANGO_GOOGLE_OAUTH2_PROJECT_ID", default="") - GOOGLE_MAPS_API_KEY = env.str("GOOGLE_MAPS_API_KEY") - -# SOCIALACCOUNT_PROVIDERS = { -# "google": { -# "APP": { -# "client_id": GOOGLE_OAUTH2_CLIENT_ID, # replace me -# "secret": GOOGLE_OAUTH2_CLIENT_SECRET, # replace me -# "key": "", # leave empty -# }, -# "SCOPE": [ -# "profile", -# "email", -# ], -# "AUTH_PARAMS": { -# "access_type": "online", -# }, -# "VERIFIED_EMAIL": True, -# }, -# } diff --git a/goodtimes/utils.py b/goodtimes/utils.py index cf592cb..d56def8 100644 --- a/goodtimes/utils.py +++ b/goodtimes/utils.py @@ -1,4 +1,5 @@ from rest_framework.response import Response +from rest_framework.renderers import JSONRenderer from rest_framework import status import string import random diff --git a/manage_cms/api/urls.py b/manage_cms/api/urls.py index 7cf2897..c07af51 100644 --- a/manage_cms/api/urls.py +++ b/manage_cms/api/urls.py @@ -14,4 +14,5 @@ urlpatterns = [ path('education/material/', views.EducationMaterialView.as_view(), name='education_material'), path('education/tags/', views.EducationTagsView.as_view(), name='education_tags'), + ] diff --git a/manage_cms/api/views.py b/manage_cms/api/views.py index 7006733..7c1cfbc 100644 --- a/manage_cms/api/views.py +++ b/manage_cms/api/views.py @@ -11,6 +11,9 @@ from manage_cms import models from taggit.models import Tag from taggit.models import TaggedItem from django.contrib.contenttypes.models import ContentType +import requests +from django.conf import settings +import urllib.parse # from nifty11_project.services import SMSError, SMSService from goodtimes.utils import ApiResponse @@ -141,10 +144,9 @@ class EducationTagsView(APIView): education_content_type = ContentType.objects.get_for_model(self.model) # Fetch tags associated with Education instances - education_tags = ( - TaggedItem.objects.filter(content_type=education_content_type) - .values_list("tag", flat=True) - ) + education_tags = TaggedItem.objects.filter( + content_type=education_content_type + ).values_list("tag", flat=True) # Retrieve actual tag objects from Tag model using the tag IDs obtained queryset = Tag.objects.filter(id__in=education_tags) @@ -156,3 +158,4 @@ class EducationTagsView(APIView): "data": serializer.data, } return ApiResponse.success(**success_response) + diff --git a/manage_cms/urls.py b/manage_cms/urls.py index b3ef006..d179036 100644 --- a/manage_cms/urls.py +++ b/manage_cms/urls.py @@ -49,4 +49,5 @@ urlpatterns = [ path('education/material/add/', views.EducationMaterialCreateOrUpdateView.as_view(), name='education_add_material'), path('education/material/edit//', views.EducationMaterialCreateOrUpdateView.as_view(), name='education_edit_material'), + ] diff --git a/manage_cms/views.py b/manage_cms/views.py index fcefb9d..8726fdf 100644 --- a/manage_cms/views.py +++ b/manage_cms/views.py @@ -26,7 +26,7 @@ from .forms import ( FaqsForm, FaqCategoryFrom, EducationVideoForm, - EducationMaterialForm + EducationMaterialForm, ) @@ -70,7 +70,7 @@ class NewsArticleCreateOrUpdateView(LoginRequiredMixin, generic.View): resource = resource_action.RESOURCE_MANAGE_CMS # Initialize the action as ACTION_CREATE (can change based on logic) - action = resource_action.ACTION_CREATE # Default action + action = resource_action.ACTION_CREATE # Default action template_name = "manage_cms/news_article_add.html" model = NewsAndArticles @@ -155,7 +155,7 @@ class NewsArticleCategoryCreateOrUpdateView(LoginRequiredMixin, generic.View): resource = resource_action.RESOURCE_MANAGE_CMS # Initialize the action as ACTION_CREATE (can change based on logic) - action = resource_action.ACTION_CREATE # Default action + action = resource_action.ACTION_CREATE # Default action template_name = "manage_cms/news_article_category_add.html" model = NewsAndArticlesCategory @@ -321,7 +321,7 @@ class AboutUsCreateOrUpdateView(LoginRequiredMixin, generic.View): resource = resource_action.RESOURCE_MANAGE_CMS # Initialize the action as ACTION_CREATE (can change based on logic) - action = resource_action.ACTION_CREATE # Default action + action = resource_action.ACTION_CREATE # Default action template_name = "manage_cms/about_us_add.html" model = Organization @@ -401,7 +401,7 @@ class TermsConditionCreateOrUpdateView(LoginRequiredMixin, generic.View): resource = resource_action.RESOURCE_MANAGE_CMS # Initialize the action as ACTION_CREATE (can change based on logic) - action = resource_action.ACTION_CREATE # Default action + action = resource_action.ACTION_CREATE # Default action template_name = "manage_cms/terms_and_condition_edit.html" model = Organization @@ -467,11 +467,7 @@ class FaqListView(LoginRequiredMixin, generic.ListView): context_object_name = "faqs_obj" def get_queryset(self): - return ( - super() - .get_queryset() - .filter(deleted=False) - ) + return super().get_queryset().filter(deleted=False) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -486,7 +482,7 @@ class FaqCreateOrUpdateView(LoginRequiredMixin, generic.View): resource = resource_action.RESOURCE_MANAGE_CMS # Initialize the action as ACTION_CREATE (can change based on logic) - action = resource_action.ACTION_CREATE # Default action + action = resource_action.ACTION_CREATE # Default action template_name = "manage_cms/faq_add.html" model = Faqs @@ -551,7 +547,7 @@ class FaqCategoryCreateOrUpdateView(LoginRequiredMixin, generic.View): resource = resource_action.RESOURCE_MANAGE_CMS # Initialize the action as ACTION_CREATE (can change based on logic) - action = resource_action.ACTION_CREATE # Default action + action = resource_action.ACTION_CREATE # Default action template_name = "manage_cms/faq_category_add.html" model = FaqCategory @@ -634,7 +630,7 @@ class PrivacyPolicyCreateOrUpdateView(LoginRequiredMixin, generic.View): resource = resource_action.RESOURCE_MANAGE_CMS # Initialize the action as ACTION_CREATE (can change based on logic) - action = resource_action.ACTION_CREATE # Default action + action = resource_action.ACTION_CREATE # Default action template_name = "manage_cms/privacy_policy_edit.html" model = Organization @@ -723,7 +719,7 @@ class OrganizationCreateOrUpdateView(LoginRequiredMixin, generic.View): resource = resource_action.RESOURCE_MANAGE_CMS # Initialize the action as ACTION_CREATE (can change based on logic) - action = resource_action.ACTION_CREATE # Default action + action = resource_action.ACTION_CREATE # Default action template_name = "manage_cms/organization_add.html" model = Organization @@ -788,18 +784,15 @@ class EducationView(LoginRequiredMixin, generic.ListView): context_object_name = "education_obj" def get_queryset(self): - return ( - super() - .get_queryset() - .prefetch_related("tags") - .filter(deleted=False) - ) + return super().get_queryset().prefetch_related("tags").filter(deleted=False) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["page_name"] = self.page_name context["video_obj"] = self.get_queryset().filter(content_type=Education.VIDEO) - context["material_obj"] = self.get_queryset().filter(content_type=Education.MATERIAL) + context["material_obj"] = self.get_queryset().filter( + content_type=Education.MATERIAL + ) print("video data", context["video_obj"]) return context @@ -810,7 +803,7 @@ class EducationCreateOrUpdateView(LoginRequiredMixin, generic.View): resource = resource_action.RESOURCE_MANAGE_CMS # Initialize the action as ACTION_CREATE (can change based on logic) - action = resource_action.ACTION_CREATE # Default action + action = resource_action.ACTION_CREATE # Default action page_title = None template_name = "manage_cms/education_add.html" diff --git a/manage_notifications/forms.py b/manage_notifications/forms.py index e69de29..7aec7e3 100644 --- a/manage_notifications/forms.py +++ b/manage_notifications/forms.py @@ -0,0 +1,17 @@ +from django import forms +from .models import PushNotification, NotificationCategory, PrincipalType + + +class PushNotificationForm(forms.ModelForm): + class Meta: + model = PushNotification + fields = [ + "title", + "notification_category", + "banner_image", + "principal_type", + "message", + ] + widgets = { + "message": forms.Textarea(attrs={"rows": 4}), + } diff --git a/manage_notifications/migrations/0001_initial.py b/manage_notifications/migrations/0001_initial.py new file mode 100644 index 0000000..dde1ce6 --- /dev/null +++ b/manage_notifications/migrations/0001_initial.py @@ -0,0 +1,234 @@ +# Generated by Django 5.0.2 on 2024-03-13 12:55 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="NotificationCategory", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("active", models.BooleanField(default=True)), + ("deleted", models.BooleanField(default=False)), + ("created_on", models.DateTimeField(auto_now_add=True)), + ("modified_on", models.DateTimeField(auto_now=True)), + ("name", models.CharField(max_length=100)), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_created", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "modified_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_modified", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "db_table": "notification_settings_category", + }, + ), + migrations.CreateModel( + name="NotificationSettings", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("active", models.BooleanField(default=True)), + ("deleted", models.BooleanField(default=False)), + ("created_on", models.DateTimeField(auto_now_add=True)), + ("modified_on", models.DateTimeField(auto_now=True)), + ("name", models.CharField(max_length=100)), + ( + "category", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="notification_category", + to="manage_notifications.notificationcategory", + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_created", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "modified_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_modified", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "db_table": "notification_settings", + }, + ), + migrations.CreateModel( + name="IAmPrincipalNotificationSettings", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("active", models.BooleanField(default=True)), + ("deleted", models.BooleanField(default=False)), + ("created_on", models.DateTimeField(auto_now_add=True)), + ("modified_on", models.DateTimeField(auto_now=True)), + ("is_enabled", models.BooleanField(default=True)), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_created", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "modified_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_modified", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "principal", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="notifications_principal", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "notification_setting", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="manage_notifications.notificationsettings", + ), + ), + ], + options={ + "db_table": "iam_principal_notification_settings", + }, + ), + migrations.CreateModel( + name="PushNotification", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("active", models.BooleanField(default=True)), + ("deleted", models.BooleanField(default=False)), + ("created_on", models.DateTimeField(auto_now_add=True)), + ("modified_on", models.DateTimeField(auto_now=True)), + ("title", models.CharField(max_length=255)), + ( + "banner_image", + models.ImageField( + blank=True, null=True, upload_to="push_notification_images/" + ), + ), + ( + "principal_type", + models.CharField( + choices=[ + ("event_user", "Event User"), + ("event_manager", "Event Manager"), + ("both", "Both"), + ], + max_length=50, + ), + ), + ("message", models.TextField()), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_created", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "modified_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_modified", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "notification_category", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="push_category", + to="manage_notifications.notificationcategory", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/manage_notifications/models.py b/manage_notifications/models.py index ab60b18..0eabaaa 100644 --- a/manage_notifications/models.py +++ b/manage_notifications/models.py @@ -1,57 +1,70 @@ -# from django.db import models -# from accounts.models import BaseModel, IAmPrincipal, IAmPrincipalType +from django.db import models +from accounts.models import BaseModel, IAmPrincipal, IAmPrincipalType -# # Create your models here. -# class PushNotification(BaseModel): -# title = models.CharField(max_length=255) -# banner_image = models.ImageField( -# upload_to="push_notification_images/", blank=True, null=True -# ) -# principal_type = models.CharField(max_length=50) -# message = models.TextField() -# published_datetime = models.DateTimeField() +# Create your models here. +class NotificationCategory(BaseModel): + name = models.CharField(max_length=100) -# def __str__(self): -# return self.title + class Meta: + db_table = "notification_settings_category" + + def __str__(self) -> str: + return f"name : {self.name}" -# class NotificationSettingsCategory(BaseModel): -# name = models.CharField(max_length=100) - -# class Meta: -# db_table = "notification_settings_category" - -# def __str__(self) -> str: -# return f"name : {self.name}" +class PrincipalType(models.TextChoices): + EVENT_USER = "event_user", "Event User" + EVENT_MANAGER = "event_manager", "Event Manager" + BOTH = "both", "Both" -# class NotificationSettings(BaseModel): -# name = models.CharField(max_length=100) -# category = models.ForeignKey( -# NotificationSettingsCategory, -# on_delete=models.CASCADE, -# related_name="notification_category", -# ) +class PushNotification(BaseModel): + title = models.CharField(max_length=255) + notification_category = models.ForeignKey( + NotificationCategory, + on_delete=models.CASCADE, + related_name="push_category", + ) + banner_image = models.ImageField( + upload_to="push_notification_images/", blank=True, null=True + ) + principal_type = models.CharField( + max_length=50, + choices=PrincipalType.choices, + ) + message = models.TextField() -# class Meta: -# db_table = "notification_settings" - -# def __str__(self) -> str: -# return f"name: {self.name}" + def __str__(self): + return self.title -# class IAmPrincipalNotificationSettings(BaseModel): -# principal = models.ForeignKey( -# IAmPrincipal, on_delete=models.CASCADE, related_name="notifications_principal" -# ) -# notification_setting = models.ForeignKey( -# NotificationSettings, on_delete=models.CASCADE -# ) -# is_enabled = models.BooleanField(default=True) +class NotificationSettings(BaseModel): + name = models.CharField(max_length=100) + category = models.ForeignKey( + NotificationCategory, + on_delete=models.CASCADE, + related_name="notification_category", + ) -# class Meta: -# db_table = "iam_principal_notification_settings" + class Meta: + db_table = "notification_settings" -# def __str__(self): -# return f"{self.principal.first_name} - {self.notification_setting.name}" + def __str__(self) -> str: + return f"name: {self.name}" + + +class IAmPrincipalNotificationSettings(BaseModel): + principal = models.ForeignKey( + IAmPrincipal, on_delete=models.CASCADE, related_name="notifications_principal" + ) + notification_setting = models.ForeignKey( + NotificationSettings, on_delete=models.CASCADE + ) + is_enabled = models.BooleanField(default=True) + + class Meta: + db_table = "iam_principal_notification_settings" + + def __str__(self): + return f"{self.principal.first_name} - {self.notification_setting.name}" diff --git a/manage_notifications/urls.py b/manage_notifications/urls.py index 0cef26b..f020fd9 100644 --- a/manage_notifications/urls.py +++ b/manage_notifications/urls.py @@ -6,5 +6,19 @@ from django.views.generic import TemplateView app_name = 'manage_notifications' urlpatterns = [ - + path( + "notification/add/", + views.PushNotificationsCreateOrUpdateView.as_view(), + name="notification_add", + ), + path( + "notification/edit//", + views.PushNotificationsCreateOrUpdateView.as_view(), + name="notification_edit", + ), + path( + "notification/list/", + views.PushNotificationView.as_view(), + name="notification_list", + ), ] diff --git a/manage_notifications/utils.py b/manage_notifications/utils.py new file mode 100644 index 0000000..fb5db92 --- /dev/null +++ b/manage_notifications/utils.py @@ -0,0 +1,101 @@ +# import requests +# from django.conf import settings + +# def onesignal_send_notification(notification): +# onesignal_app_id = settings.ONE_SIGNAL_APP_ID +# onesignal_rest_api_key = settings.ONE_SIGNAL_API_KEY + +# headers = { +# "Content-Type": "application/json; charset=utf-8", +# "Authorization": f"Basic {onesignal_rest_api_key}" +# } + +# # Determine player IDs based on notification_category and principal_type +# player_ids = [] + +# if notification.principal_type == PrincipalType.BOTH: +# # Get player IDs for both event_user and event_manager +# from .models import IAmPrincipal # Assuming your IAmPrincipal model is in the same app + +# event_user_principals = IAmPrincipal.objects.filter(iam_principal_type__name=PrincipalType.EVENT_USER) +# event_manager_principals = IAmPrincipal.objects.filter(iam_principal_type__name=PrincipalType.EVENT_MANAGER) + +# for principal in event_user_principals: +# # Assuming you have a field in IAmPrincipal that stores the player ID (e.g., 'one_signal_player_id') +# player_ids.append(principal.one_signal_player_id) + +# for principal in event_manager_principals: +# player_ids.append(principal.one_signal_player_id) + +# else: +# # Handle filtering for EVENT_USER or EVENT_MANAGER as needed (similar logic as above) + +# # Construct the notification payload +# data = { +# "app_id": onesignal_app_id, +# "headings": {"en": notification.title}, +# "contents": {"en": notification.message}, +# "include_player_ids": player_ids, # Include the filtered player IDs +# # Add other optional notification data according to OneSignal documentation +# } + +# if notification.banner_image: +# # Include image URL if provided (requires additional OneSignal configuration) +# data["large_icon"] = notification.banner_image.url # Assuming you have a URL property for the image field + +# # Send the notification request to OneSignal +# response = requests.post("https://onesignal.com/api/v1/notifications", headers=headers, json=data) + +# if response.status_code == 200: +# print("Notification sent successfully!") +# else: +# print(f"Error sending notification: {response.text}") + + +from onesignal_sdk.client import Client as OneSignalClient +from django.conf import settings +from .models import IAmPrincipalNotificationSettings, NotificationSettings, IAmPrincipal + + +def send_notification(notification_type, title, message, image_url=None): + # Initialize OneSignal client + onesignal_client = OneSignalClient( + app_id=settings.ONE_SIGNAL_APP_ID, rest_api_key=settings.ONE_SIGNAL_API_KEY + ) + + # Find all users who have enabled this type of notification + notification_setting = NotificationSettings.objects.filter( + name=notification_type + ).first() + if not notification_setting: + print("Notification type does not exist.") + return + + user_ids = IAmPrincipalNotificationSettings.objects.filter( + notification_setting=notification_setting, is_enabled=True + ).values_list("principal__id", flat=True) + + principals = ( + IAmPrincipal.objects.filter(id__in=user_ids) + .exclude(player_id__isnull=True) + .exclude(player_id__exact="") + ) + + # Extract OneSignal player IDs + player_ids = principals.values_list("player_id", flat=True) + + # Prepare notification payload + notification_payload = { + "headings": {"en": title}, + "contents": {"en": message}, + "include_player_ids": list(player_ids), + } + + if image_url: + notification_payload["big_picture"] = image_url + + # Send notification + response = onesignal_client.send_notification(notification_payload) + print(response.status_code, response.body) + + return response diff --git a/manage_notifications/views.py b/manage_notifications/views.py index 91ea44a..ca80a6c 100644 --- a/manage_notifications/views.py +++ b/manage_notifications/views.py @@ -1,3 +1,90 @@ -from django.shortcuts import render +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse_lazy +from django.views import generic +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib import messages +from accounts import resource_action +from goodtimes import constants +from manage_notifications.forms import PushNotificationForm +from manage_notifications.models import PushNotification # Create your views here. + + +class PushNotificationsCreateOrUpdateView(LoginRequiredMixin, generic.View): + # Set the page_name and resource + page_name = resource_action.RESOURCE_MANAGE_NOTIFICATIONS + resource = resource_action.RESOURCE_MANAGE_NOTIFICATIONS + + # Initialize the action as ACTION_CREATE (can change based on logic) + action = resource_action.ACTION_CREATE # Default action + + template_name = "manage_notifications/notification_add.html" + model = PushNotification + form_class = PushNotificationForm + success_url = reverse_lazy("manage_notifications:notification_list") + error_message = "An error occurred while saving the data." + + # Determine the success message dynamically based on whether it's an update or create + def get_success_message(self): + self.success_message = ( + constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED + ) + return self.success_message + + # Get the object (if exists) based on URL parameter 'pk' + def get_object(self): + pk = self.kwargs.get("pk") + return get_object_or_404(self.model, pk=pk) if pk else None + + # Add page_name and operation to the context + def get_context_data(self, **kwargs): + context = { + "page_name": self.page_name, + "operation": "Add" if not self.object else "Edit", + } + context.update(kwargs) # Include any additional context data passed to the view + return context + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + + # If an object is found, change action to ACTION_UPDATE + if self.object is not None: + self.action = resource_action.ACTION_UPDATE + + form = self.form_class(instance=self.object) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + # If an object is found, change action to ACTION_UPDATE + if self.object is not None: + self.action = resource_action.ACTION_UPDATE + + form = self.form_class(request.POST, request.FILES, instance=self.object) + if not form.is_valid(): + print(form.errors) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + form.save() + messages.success(self.request, self.get_success_message()) + return redirect(self.success_url) + + +class PushNotificationView(LoginRequiredMixin, generic.ListView): + page_name = resource_action.RESOURCE_MANAGE_EVENTS + resource = resource_action.RESOURCE_MANAGE_EVENTS + action = resource_action.ACTION_READ + model = PushNotification + template_name = "manage_notifications/notification_list.html" + context_object_name = "notification_obj" + + def get_queryset(self): + return super().get_queryset().filter(deleted=False) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["page_name"] = self.page_name + return context \ No newline at end of file diff --git a/manage_referrals/models.py b/manage_referrals/models.py index c23c702..a844c87 100644 --- a/manage_referrals/models.py +++ b/manage_referrals/models.py @@ -43,7 +43,8 @@ class ReferralCode(BaseModel): The method ensures each generated code is unique to avoid conflicts. """ - type = type.upper() + # type = type.upper() + type = type.replace("_", "").upper() name = name[:3].upper() if name else "GDTM" while True: random_number = "".join(random.choice(string.digits) for _ in range(4)) diff --git a/static/src/assets/css/payment/style.css b/static/src/assets/css/payment/style.css new file mode 100644 index 0000000..62b7f37 --- /dev/null +++ b/static/src/assets/css/payment/style.css @@ -0,0 +1,737 @@ +@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap') @import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap') * { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +:root { + --black: #000000; + --light-black: #050505; + --main-yellow: rgba(209, 170, 88, 1); + --light-yellow: rgba(229, 25, 94, 0.2); + --white: #ffffff; + --light-white: #f8f8f8; + --white-other: rgba(207, 207, 207, 1); + --white-mix: #cecece; + --border: #ff72a285; +} + +body { + font-family: "Poppins", sans-serif; +} + +.ptb { + padding: 40px 0; +} + +.sec-heading { + font-size: 38px; + font-weight: 600; + text-align: center; + /* padding-top: 40px; */ + color: var(--main-yellow); +} + +.sec-subheading { + font-size: 18px; + font-weight: 400; + text-align: center; + color: var(--white); +} + + +.big-heading { + font-size: 52px; + font-weight: 700; + color: var(--white); + letter-spacing: 1.8px; +} + + +.para { + font-size: 18px; + font-weight: 400; + color: rgba(255, 255, 255, 1); +} + +.sec-mini-heading { + font-size: 24px; + font-weight: 600; +} + + +.para-dark { + font-size: 24px; + /* font-weight: 600; */ +} + +.para-mid { + font-size: 18px; + color: rgba(255, 255, 255, 0.69); +} + +li { + list-style: none; +} + +.pt { + padding: 40px 0; +} + + +/* header */ + +header { + border-bottom: 1px solid var(--main-yellow); + position: absolute; + background-color: transparent; + top: 0; + left: 0; + width: 100%; + padding: 22px 0; + display: flex; + align-items: center; + z-index: 9999; +} + +header .header-main-inner { + display: flex; + align-items: center; + justify-content: space-between; +} + +header nav ul { + display: flex; + gap: 80px; + align-items: center; + margin: 0; +} + + +.header-main-inner .logo { + width: 212px; + height: 52px; +} + +.header-main-inner .logo img { + width: 100%; +} + + +header nav ul .menu-btn img { + width: 24px; + height: 24px; +} + +header nav ul li a { + color: var(--white); + font-size: 18px; + /* font-weight: 600; */ + position: relative; + text-decoration: none; +} + +.sticky { + background-color: var(--black); + position: fixed; + animation: slideDown 0.8s ease-out; + -webkit-animation: slideDown 0.8s ease-out; +} + +@keyframes slideDown { + 0% { + transform: translateY(-100%); + } + + 100% { + transform: translateY(0); + } +} + +header a.active { + color: var(--main-yellow); +} + +header nav ul li a::after { + content: ""; + position: absolute; + left: 50%; + width: 0%; + bottom: -10px; + border-bottom: 2px solid var(--main-yellow); + transition: all 0.3s; + transform: translateX(-50%); +} + +header nav ul li a:hover { + color: var(--main-yellow); +} + +header nav ul li a:hover:after { + width: 100%; +} + +.hamburger { + display: none; +} + +.hamburger { + position: relative; + width: 25px; + height: 25px; + display: none; + cursor: pointer; +} + +.hamburger img { + width: 25px; + height: 25px; +} + + +.overlay { + display: none; +} + +.cross-btn { + padding: 2px 20px; + text-align: right; + display: none; + font-size: 40px; + cursor: pointer; + color: var(--main-yellow); +} + +.cross-btn i { + font-size: 20px; + font-weight: 500; +} + +/* about-head */ +.head-sec header, +.terms-sec header { + /* position: inherit; */ + background-color: var(--black); +} + + + + +/* baner */ +.baner-section { + background-image: linear-gradient(rgba(4, 9, 10, 0.7), rgba(4, 9, 10, 0.7)), url(images/baner.jpg); + background-position: center; + background-size: cover; + height: 100vh; + display: flex; + align-items: center; +} + +.baner-section .row { + align-items: center; + padding-top: 100px; +} + +.baner-section .store-app { + display: flex; + align-items: center; + gap: 15px; + margin: 30px 0; +} + +.baner-img { + text-align: center; + padding-top: 20px; +} + +.baner-section .baner-img img { + width: 75%; +} + +.baner-content .big-heading span { + color: var(--main-yellow); +} + +.baner-content .grey-para { + margin-top: 24px; + font-size: 20px; + color: var(--white-other); +} + + +.baner-btn { + margin-top: 24px; +} + +.common-btn { + background: linear-gradient(90.02deg, #CDA34C 0.02%, #F1D6A0 52%, #D1A956 98.68%); + width: 252px; + /* height: 50px; */ + font-weight: 500; + border: none; + font-size: 18px; + font-weight: 600; + padding: 12px 0; +} + + +/* key-feature */ +.key-features { + background-color: #10100e; + color: white; +} + +.key-features .row { + align-items: center; + padding-bottom: 40px; +} + +.key-features .key-main-img img { + width: 75%; +} + +.key-features .key-right-first, +.key-right-second { + display: flex; + align-items: start; + gap: 20px; +} + +.key-right-second { + margin-top: 60px; +} + +/* easy-steps */ +.easy-steps { + background-color: var(--black); +} + +.easy-steps .para-dark { + margin-top: 50px; + color: var(--white); +} + +.easy-steps .para { + color: rgba(192, 192, 192, 1); + text-align: center; +} + +.easy-steps-main { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 25px; + margin-top: 100px; + padding-bottom: 40px; +} + +.easy-steps-first { + background-color: rgba(255, 255, 255, 0.05); + border: 1px solid var(--main-yellow); + border-top-left-radius: 14px; + border-top-right-radius: 14px; + display: flex; + align-items: center; + flex-direction: column; + position: relative; + height: 400px; + padding: 0px 20px; +} + +.easy-steps-first-img-num { + width: 75px; + height: 75px; + position: absolute; + top: -50px; +} + +img.easy-steps-first-img-bot { + position: absolute; + bottom: -1px; +} + +/* Adventure + */ + + +.Adventure { + background-color: #10100e; + color: white; +} + +.Adventure .row { + align-items: center; + padding-top: 40px; +} + +.Adventure-rti { + display: flex; + align-items: center; + gap: 20px; + margin-bottom: 40px; +} + +.Adventure-right img { + width: 75%; +} + +.Adventure-left .store-app { + display: flex; + gap: 15px; +} + +.Adventure-btn { + margin-top: 40px; +} + + +/* faq */ +.faq { + background-color: black; +} + +.main-faq { + padding: 40px 0 80px; +} + +div#accordionExample { + display: flex; + flex-direction: column; + gap: 30px; +} + +.faq .accordion-item { + border: 1px solid var(--main-yellow); + background-color: transparent; + color: #fff; + border-radius: 5px; +} + +.faq button.accordion-button:focus { + box-shadow: inherit; +} + +.faq button.accordion-button { + background-color: transparent; + color: var(--white); +} + +.accordion-button:not(.collapsed) { + color: var(--main-yellow) !important; + font-family: "Nunito Sans", sans-serif; + font-weight: 600; +} + +.accordion-item:first-of-type .accordion-button { + box-shadow: none; +} + +.accordion-button::after { + background-image: url(images/ab.png); +} + +.accordion-button:not(.collapsed)::after { + background-image: url(images/at.png); + transform: none; +} + + + +/* footer */ +.footer { + background-color: rgba(16, 16, 14, 1); +} + +.footer .footer-main-img { + width: 212px; + height: 52px; + padding: 40px 0; +} + +.footer .footer-main-img img { + width: 100%; +} + +.footer .footer-main-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + color: var(--white); + padding: 3rem 0 2rem; +} + +.footer .footer-main-grid .para-dark { + font-size: 18px; + font-weight: 600; +} + +.footer .footer-main-grid .para { + font-size: 16px; +} + + +.footer .store-app { + display: flex; + gap: 15px; + margin: 0; + flex-direction: column; +} + +.footer-btn .common-btn { + margin-bottom: 16px; +} + +.footer-main-grid-fourth { + margin-top: -20px; +} + +.copy-right { + color: #fff; + text-decoration: none; + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + padding-bottom: 20px; +} + +.store-app img { + width: 165px; +} + + +/* About us */ + +.about-us { + background-image: url(images/About\ Us\ Banner.png); + background-position: top; + background-size: cover; + background-repeat: no-repeat; + height: 500px; + display: flex; + align-items: center; + margin-top: 90px; +} + +.about-us .row { + align-items: center; +} + + +.about, +.terms { + background-color: var(--black); + color: var(--white); + padding: 40px 0 70px; +} + + +.terms-main { + background-image: url(images/terms.png); + background-position: top; + background-size: cover; + background-repeat: no-repeat; + height: 500px; + display: flex; + align-items: center; + margin-top: 90px; +} + + +/* mediascreen */ +@media (max-width: 1199px) { + .big-heading br { + display: none; + } +} + +@media (max-width: 1024px) { + .big-heading { + font-size: 42px; + } +} + +@media (max-width: 991px) { + /* .big-heading br { + display: none; + } */ + + .big-heading { + font-size: 35px; + } + + .store-app img { + width: 142px; + } + + .sec-mini-heading { + font-size: 18px; + } + + .para { + font-size: 14px; + } + + .easy-steps-main { + grid-template-columns: repeat(2, 1fr); + gap: 70px 20px; + margin-top: 70px; + } + + .footer .footer-main-grid { + grid-template-columns: repeat(2, 1fr); + } + + .footer-main-grid-fourth { + margin-top: 0px; + } + +} + + + +@media (max-width: 767px) { + .ptb { + padding: 20px 0 40px 0; + } + + .overlay { + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: -100%; + background-color: #60606054; + + } + + .cross-btn { + display: block; + } + + .hamburger, + .overlay { + display: block; + } + + .navs { + position: fixed; + top: 0; + left: -100%; + width: 300px; + height: 100%; + background: rgb(0 0 0); + transition: all .3s; + z-index: 1; + } + + .navs ul { + flex-direction: column; + padding: 00px 20px; + align-items: start; + gap: 14px; + } + + .navs ul li a { + color: whitesmoke; + } + + .common-btn { + width: 175px; + height: 40px; + } + + + .baner-section { + height: inherit; + padding: 40px 0; + } + + .baner-section .row { + flex-direction: column-reverse; + } + + .baner-section .store-app { + justify-content: center; + } + + .baner-btn { + text-align: center; + } + + .easy-steps-main { + grid-template-columns: repeat(1, 1fr); + } + + .footer .footer-main-grid { + grid-template-columns: repeat(2, 1fr); + gap: 20px; + } + + .easy-steps-first { + height: 400px; + } + + .key-features .key-main-img img { + width: 50%; + margin-bottom: 20px; + } + + .Adventure-rti { + margin-bottom: 20px; + } + + .Adventure .row { + flex-direction: column-reverse; + gap: 40px; + padding: 0; + } + + .Adventure { + padding: 30px 0; + } + + .Adventure-btn { + margin-top: 30px; + } + + .Adventure-right img { + width: 50%; + } + + .sec-heading { + font-size: 30px; + padding-top: 0; + } + + .about .para-mid, + .terms .para-mid { + font-size: 16px; + } + + .faq { + padding: 30px 0; + } + + .main-faq { + padding: 20px 0 30px; + } + + br { + display: none; + } +} + +@media (max-width: 444px) { + /* .easy-steps-main { + grid-template-columns: repeat(1, 1fr); + } */ + + .footer .footer-main-grid { + grid-template-columns: repeat(1, 1fr); + gap: 0px; + } + + .footer .store-app { + margin-bottom: 16px; + } +} \ No newline at end of file diff --git a/static/src/assets/js/payment/custom.js b/static/src/assets/js/payment/custom.js new file mode 100644 index 0000000..992f2c3 --- /dev/null +++ b/static/src/assets/js/payment/custom.js @@ -0,0 +1,61 @@ +// aos animation + +AOS.init(); + +// siderbar menu + +let cross_btn = document.querySelector('.cross-btn'); +let nav = document.querySelector('.navs'); +let ham = document.querySelector('.hamburger'); +let overlay = document.querySelector('.overlay'); +let navLinks = document.querySelector('.navLinks'); + +let openSide = function () { + nav.style.left = '0'; + overlay.style.left = '0'; + +} +let closeSide = function () { + nav.style.left = '-100%'; + overlay.style.left = '-100%'; +} + +ham.addEventListener('click', function () { + openSide(); +}) +cross_btn.addEventListener('click', function () { + closeSide(); +}) +overlay.addEventListener('click', function () { + closeSide(); +}) +navLinks.addEventListener('click', function () { + navLinks(); +}) + + +// active page + +document.addEventListener("DOMContentLoaded", function () { + var currentUrl = window.location.href; + var links = document.querySelectorAll("#header nav ul li a"); + + links.forEach(function (link) { + if (link.href === currentUrl) { + link.classList.add("active"); + } + }); +}); + + +// sticky header + +const header = document.querySelector("#header"); +window.addEventListener("scroll", () => { + const currentScroll = window.scrollY; + if (currentScroll > 100) { + header.classList.add('sticky'); + } else { + header.classList.remove('sticky'); + } +}); \ No newline at end of file diff --git a/templates/elements/sidebar.html b/templates/elements/sidebar.html index c001640..76ef3d9 100644 --- a/templates/elements/sidebar.html +++ b/templates/elements/sidebar.html @@ -148,7 +148,7 @@ -