This commit is contained in:
Mamalizz
2025-03-27 23:08:29 +03:30
47 changed files with 355 additions and 157 deletions
+3 -1
View File
@@ -4,4 +4,6 @@ from django.apps import AppConfig
class AccountConfig(AppConfig): class AccountConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'account' name = 'account'
verbose_name = 'اکانت' verbose_name = 'اکانت'
def ready(self):
import account.signals
+5 -1
View File
@@ -129,7 +129,7 @@ class UserAddressModel(models.Model):
city = models.CharField(max_length=30, verbose_name='شهر') city = models.CharField(max_length=30, verbose_name='شهر')
province = models.CharField(max_length=30, verbose_name='استان') province = models.CharField(max_length=30, verbose_name='استان')
for_me = models.BooleanField(default=False, verbose_name='برای خود کاربر') for_me = models.BooleanField(default=False, verbose_name='برای خود کاربر')
is_main = models.BooleanField(default=False) is_main = models.BooleanField(default=False, verbose_name='ادرس اصلی کاربر')
def __str__(self): def __str__(self):
return f"{self.user.phone}, {self.name}" return f"{self.user.phone}, {self.name}"
@@ -152,6 +152,10 @@ class PushSubscription(models.Model):
def __str__(self): def __str__(self):
return f'{self.user} push' return f'{self.user} push'
class Meta:
verbose_name = 'اشتراک نوتیفیکیشن'
verbose_name_plural = 'اشتراک های نوتیفیکیشن'
def send_notif(self, title, body, icon): def send_notif(self, title, body, icon):
payload = { payload = {
"title": 'فروشگاه هی ملز', "title": 'فروشگاه هی ملز',
+10
View File
@@ -0,0 +1,10 @@
from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import UserAddressModel
@receiver(post_save, sender=UserAddressModel)
def ensure_single_main_address(sender, instance, **kwargs):
if instance.is_main:
with transaction.atomic():
UserAddressModel.objects.filter(user=instance.user).exclude(pk=instance.pk).update(is_main=False)
+1 -2
View File
@@ -1,12 +1,11 @@
from django.urls import path from django.urls import path
from . import views from . import views
from djoser.urls.jwt import views as djoser_jwt_views
urlpatterns = [ urlpatterns = [
path('profile', views.ProfileView.as_view()), path('profile', views.ProfileView.as_view()),
path('verify', djoser_jwt_views.TokenVerifyView.as_view(), name='jwt-verify'), path('verify', views.TokenVerifyView.as_view(), name='jwt-verify'),
path('send_otp', views.SendOTPView.as_view(), name='send-otp-view'), path('send_otp', views.SendOTPView.as_view(), name='send-otp-view'),
path('yee_token_bedeeee', views.KonGhoshadToken.as_view()), path('yee_token_bedeeee', views.KonGhoshadToken.as_view()),
path('address/create', views.CreateAddressView.as_view(), name='create-address'), path('address/create', views.CreateAddressView.as_view(), name='create-address'),
+51 -13
View File
@@ -5,8 +5,8 @@ from rest_framework.response import Response
from .serializers import * from .serializers import *
from .models import UserAddressModel, User, SecurityBreachAttemptModel from .models import UserAddressModel, User, SecurityBreachAttemptModel
from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.permissions import IsAuthenticated, AllowAny
from drf_spectacular.utils import extend_schema, OpenApiParameter from drf_spectacular.utils import extend_schema, OpenApiParameter, extend_schema_view
from rest_framework_simplejwt.views import TokenObtainPairView from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
import ghasedak_sms import ghasedak_sms
@@ -19,7 +19,7 @@ from rest_framework_simplejwt.tokens import RefreshToken
class SendOTPView(APIView): class SendOTPView(APIView):
permission_classes = [AllowAny] permission_classes = [AllowAny]
@extend_schema( @extend_schema(
tags=["Authentication"], tags=["authentication"],
request={ request={
"application/json": { "application/json": {
"type": "object", "type": "object",
@@ -65,12 +65,18 @@ Code: {otp}"""
except Exception as e: except Exception as e:
return Response({'detail': f'error: {e} مشتی فعلا برو تو غمت نباشه تا بعدا یه کاریش بکنم', 'otp_code': otp}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response({'detail': f'error: {e} مشتی فعلا برو تو غمت نباشه تا بعدا یه کاریش بکنم', 'otp_code': otp}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# return Response({'detail': f'An error occurred: {e}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # return Response({'detail': f'An error occurred: {e}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@extend_schema_view(
post=extend_schema(tags=['authentication'])
)
class TokenRefreshView(TokenRefreshView):
pass
class CustomTokenObtainPairView(TokenObtainPairView): class CustomTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer serializer_class = CustomTokenObtainPairSerializer
@extend_schema( @extend_schema(
tags=["Authentication"] tags=["authentication"]
) )
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
phone = request.data.get("phone") phone = request.data.get("phone")
@@ -93,10 +99,10 @@ class CustomTokenObtainPairView(TokenObtainPairView):
class KonGhoshadToken(TokenObtainPairView): class KonGhoshadToken(APIView):
serializer_class = CustomTokenObtainPairSerializer serializer_class = CustomTokenObtainPairSerializer
@extend_schema( @extend_schema(
tags=["Authentication"] tags=["authentication"]
) )
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
random_user = User.objects.all().first() random_user = User.objects.all().first()
@@ -116,12 +122,16 @@ class KonGhoshadToken(TokenObtainPairView):
class ProfileView(APIView): class ProfileView(APIView):
serializer_class = ProfileSerializer serializer_class = ProfileSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@extend_schema(
tags=["accounts profile"]
)
def get(self, request): def get(self, request):
user_ser = self.serializer_class(instance=request.user, context={'request': request}) user_ser = self.serializer_class(instance=request.user, context={'request': request})
return Response(user_ser.data, status=status.HTTP_200_OK) return Response(user_ser.data, status=status.HTTP_200_OK)
@extend_schema(
tags=["accounts profile"]
)
def patch(self, request): def patch(self, request):
user = request.user user = request.user
user_ser = self.serializer_class(user, data=request.data, partial=True, context={'request': request}) user_ser = self.serializer_class(user, data=request.data, partial=True, context={'request': request})
@@ -130,6 +140,10 @@ class ProfileView(APIView):
return Response(user_ser.data) return Response(user_ser.data)
return Response(user_ser.errors, status=status.HTTP_400_BAD_REQUEST) return Response(user_ser.errors, status=status.HTTP_400_BAD_REQUEST)
@extend_schema_view(
post=extend_schema(tags=["accounts address"])
)
class CreateAddressView(generics.CreateAPIView): class CreateAddressView(generics.CreateAPIView):
queryset = UserAddressModel.objects.all() queryset = UserAddressModel.objects.all()
serializer_class = UserAddressSerializer serializer_class = UserAddressSerializer
@@ -139,7 +153,10 @@ class CreateAddressView(generics.CreateAPIView):
user = self.request.user user = self.request.user
is_first_address = not UserAddressModel.objects.filter(user=user).exists() is_first_address = not UserAddressModel.objects.filter(user=user).exists()
serializer.save(user=user, is_main=is_first_address) serializer.save(user=user, is_main=is_first_address)
@extend_schema_view(
put=extend_schema(tags=["accounts address"]),
patch=extend_schema(tags=["accounts address"])
)
class EditAddressView(generics.UpdateAPIView): class EditAddressView(generics.UpdateAPIView):
queryset = UserAddressModel.objects.all() queryset = UserAddressModel.objects.all()
serializer_class = UserAddressSerializer serializer_class = UserAddressSerializer
@@ -147,21 +164,27 @@ class EditAddressView(generics.UpdateAPIView):
def get_queryset(self): def get_queryset(self):
return UserAddressModel.objects.filter(user=self.request.user) return UserAddressModel.objects.filter(user=self.request.user)
@extend_schema_view(
delete=extend_schema(tags=["accounts address"])
)
class DeleteAddressView(generics.DestroyAPIView): class DeleteAddressView(generics.DestroyAPIView):
queryset = UserAddressModel.objects.all() queryset = UserAddressModel.objects.all()
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
def get_queryset(self): def get_queryset(self):
return UserAddressModel.objects.filter(user=self.request.user) return UserAddressModel.objects.filter(user=self.request.user)
@extend_schema_view(
get=extend_schema(tags=["accounts address"])
)
class GetUserAddressesView(generics.ListAPIView): class GetUserAddressesView(generics.ListAPIView):
serializer_class = UserAddressSerializer serializer_class = UserAddressSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
def get_queryset(self): def get_queryset(self):
return UserAddressModel.objects.filter(user=self.request.user) return UserAddressModel.objects.filter(user=self.request.user)
@extend_schema_view(
get=extend_schema(tags=["accounts address"])
)
class GetIDUserAddressView(generics.RetrieveAPIView): class GetIDUserAddressView(generics.RetrieveAPIView):
serializer_class = UserAddressSerializer serializer_class = UserAddressSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
@@ -174,6 +197,9 @@ class GetIDUserAddressView(generics.RetrieveAPIView):
class SubscribeView(APIView): class SubscribeView(APIView):
serializer_class = PushSubscriptionSerializer serializer_class = PushSubscriptionSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@extend_schema(
tags=["accounts subscribe"]
)
def post(self, request): def post(self, request):
push_ser = self.serializer_class(data=request.data) push_ser = self.serializer_class(data=request.data)
if push_ser.is_valid(): if push_ser.is_valid():
@@ -191,6 +217,9 @@ class UnsubscribeSerializer(serializers.Serializer):
class UnsubscribeView(APIView): class UnsubscribeView(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
serializer_class = UnsubscribeSerializer serializer_class = UnsubscribeSerializer
@extend_schema(
tags=["accounts subscribe"]
)
def post(self, request): def post(self, request):
endpoint = request.data.get("end_point") endpoint = request.data.get("end_point")
if not endpoint: if not endpoint:
@@ -221,6 +250,7 @@ class LogoutView(APIView):
@extend_schema( @extend_schema(
request=LogoutSerializer, request=LogoutSerializer,
tags=["authentication"],
responses={205: None, 400: "Bad request (invalid token or missing data)"}, responses={205: None, 400: "Bad request (invalid token or missing data)"},
) )
def post(self, request): def post(self, request):
@@ -230,4 +260,12 @@ class LogoutView(APIView):
token.blacklist() token.blacklist()
return Response(status=status.HTTP_205_RESET_CONTENT) return Response(status=status.HTTP_205_RESET_CONTENT)
except Exception as e: except Exception as e:
return Response(status=status.HTTP_400_BAD_REQUEST) return Response(status=status.HTTP_400_BAD_REQUEST)
from djoser.urls.jwt import views as djoser_jwt_views
@extend_schema_view(
post=extend_schema(tags=["authentication"])
)
class TokenVerifyView(djoser_jwt_views.TokenVerifyView):
pass
+1
View File
@@ -66,6 +66,7 @@ class BankAdmin(ModelAdmin):
"extra_information", "extra_information",
"created_at", "created_at",
"update_at", "update_at",
'order',
] ]
+1 -1
View File
@@ -73,7 +73,7 @@ class Bank(models.Model):
max_length=255, blank=True, null=True, verbose_name=_("Bank choose identifier") max_length=255, blank=True, null=True, verbose_name=_("Bank choose identifier")
) )
order = models.ForeignKey(OrderModel, on_delete=models.SET_NULL, null=True, blank=True ,related_name='bank_records') order = models.ForeignKey(OrderModel, on_delete=models.SET_NULL, null=True, blank=True ,related_name='bank_records', verbose_name='سفارش')
created_at = models.DateTimeField(auto_now_add=True, editable=False, verbose_name=_("Created at")) created_at = models.DateTimeField(auto_now_add=True, editable=False, verbose_name=_("Created at"))
update_at = models.DateTimeField(auto_now=True, editable=False, verbose_name=_("Updated at")) update_at = models.DateTimeField(auto_now=True, editable=False, verbose_name=_("Updated at"))
+2 -2
View File
@@ -3,9 +3,9 @@ from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from drf_spectacular.views import SpectacularSwaggerView, SpectacularAPIView from drf_spectacular.views import SpectacularSwaggerView, SpectacularAPIView
from django.conf import settings from django.conf import settings
from rest_framework_simplejwt.views import TokenObtainPairView,TokenRefreshView from rest_framework_simplejwt.views import TokenObtainPairView
from product import views from product import views
from account.views import CustomTokenObtainPairView from account.views import CustomTokenObtainPairView, TokenRefreshView
from home.views import HomeView from home.views import HomeView
from .views import FakeAdminLoginView from .views import FakeAdminLoginView
from azbankgateways.urls import az_bank_gateways_urls from azbankgateways.urls import az_bank_gateways_urls
+4 -3
View File
@@ -35,6 +35,7 @@ class BankRecordInline(StackedInline):
model = Bank model = Bank
extra = 0 extra = 0
max_num = 0 max_num = 0
tab = True
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
return False return False
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
@@ -46,11 +47,11 @@ class BankRecordInline(StackedInline):
class OrderAdmin(ModelAdmin, ImportExportModelAdmin): class OrderAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm import_form_class = ImportForm
export_form_class = ExportForm export_form_class = ExportForm
search_fields = ['order_id', 'user__phone', 'user__first_name', 'user__last_name', 'user__email']
list_filter = ['is_paid', 'status'] list_filter = ['is_paid', 'status']
actions_list = ['redirect_to_learn', 'udpate_bank_status'] actions_list = ['redirect_to_learn', 'udpate_bank_status']
list_display = ['user', 'is_paid', 'status', 'discount_code', 'address',] list_display = ['order_id', 'user', 'is_paid', 'status', 'discount_code', 'address',]
readonly_fields = ('created_at', ) readonly_fields = ('created_at', 'order_id', 'tax', 'final_price', 'cart_total', 'discount', 'discount_code', 'user', 'address', 'is_paid')
compressed_fields = True compressed_fields = True
warn_unsaved_form = True warn_unsaved_form = True
# exclude = ('bank_records',) # exclude = ('bank_records',)
@@ -0,0 +1,23 @@
# Generated by Django 5.1.2 on 2025-03-27 10:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0023_remove_ordermodel_bank_records'),
]
operations = [
migrations.AddField(
model_name='orderitemmodel',
name='discount',
field=models.SmallIntegerField(default=0, verbose_name='تخفیف'),
),
migrations.AddField(
model_name='ordermodel',
name='order_id',
field=models.PositiveIntegerField(blank=True, null=True, unique=True),
),
]
+25 -27
View File
@@ -44,6 +44,7 @@ class OrderModel(models.Model):
('CANCELED', 'لغو شده'), ('CANCELED', 'لغو شده'),
('REFUNDED', 'مرجوع شده'), ('REFUNDED', 'مرجوع شده'),
] ]
order_id = models.PositiveIntegerField(unique=True, null=True, blank=True, verbose_name='شماره سفارش')
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='orders', verbose_name='کاربر') user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='orders', verbose_name='کاربر')
address = models.ForeignKey(UserAddressModel, on_delete=models.SET_NULL, related_name='orders', null=True, verbose_name='ادرس') address = models.ForeignKey(UserAddressModel, on_delete=models.SET_NULL, related_name='orders', null=True, verbose_name='ادرس')
created_at = jmodels.jDateField(blank=True, null=True, verbose_name="تاریخ ثبت سفارش") created_at = jmodels.jDateField(blank=True, null=True, verbose_name="تاریخ ثبت سفارش")
@@ -54,7 +55,6 @@ class OrderModel(models.Model):
tax = models.BigIntegerField(null=True, blank=True, verbose_name='مالیات') tax = models.BigIntegerField(null=True, blank=True, verbose_name='مالیات')
final_price = models.BigIntegerField(null=True, blank=True, verbose_name='قیمت نهایی') final_price = models.BigIntegerField(null=True, blank=True, verbose_name='قیمت نهایی')
cart_total = models.BigIntegerField(null=True, blank=True, verbose_name='کل سبد خرید') cart_total = models.BigIntegerField(null=True, blank=True, verbose_name='کل سبد خرید')
# bank_records = models.ManyToManyField(Bank, max_length=100, verbose_name='رکورد بانکی', null=True, blank=True)
def __str__(self): def __str__(self):
return f'سفارش: {self.id + 1000}' return f'سفارش: {self.id + 1000}'
@@ -66,39 +66,32 @@ class OrderModel(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
try: if not self.pk:
push_object = PushSubscription.objects.get(user=self.user) last_instance = self.__class__.objects.order_by("pk").last()
except: self.order_id = (last_instance.pk + 1001) if last_instance else 1001
print('object not found')
try:
push_object.send_notif(f'سفارش شما به {self.get_status_display()} تغییر کرد', f'سفارش شما به {self.get_status_display()} تغییر کرد', ProductImageModel.objects.all().first().image.url)
except:
print('didnt send')
super().save(*args, **kwargs) super().save(*args, **kwargs)
# def discount(self): def cal_discount(self):
# # total_with_item_discount = sum(item.total_with_discount() for item in self.items.all()) # total_with_item_discount = sum(item.total_with_discount() for item in self.items.all())
# # discount_percent = self.discount_code.percent # discount_percent = self.discount_code.percent
# # return total_with_item_discount * ((100 - discount_percent) / 100) # return total_with_item_discount * ((100 - discount_percent) / 100)
# pass pass
# def tax(self): def cal_tax(self):
# return self.total_without_tax() * 0.2 return self.total_without_tax() * 0.2
# def total(self): def cal_total(self):
# pass pass
# return self.total_with_discount() + self.tax() return self.total_with_discount() + self.tax()
# def final_price(self): def cal_final_price(self):
# pass pass
# def submit_cart(self):
# pass
@@ -108,15 +101,20 @@ class OrderItemModel(models.Model):
quantity = models.PositiveSmallIntegerField(verbose_name="تعداد") quantity = models.PositiveSmallIntegerField(verbose_name="تعداد")
price = models.PositiveIntegerField(verbose_name='قیمت', default=0) price = models.PositiveIntegerField(verbose_name='قیمت', default=0)
product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول") product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول")
discount = models.SmallIntegerField(default=0, verbose_name='تخفیف')
class Meta: class Meta:
verbose_name = 'ایتم سبد خرید' verbose_name = 'ایتم سبد خرید'
verbose_name_plural = 'ایتم های سبد خرید' verbose_name_plural = 'ایتم های سبد خرید'
def total(self): # def total(self):
return self.quantity * self.product.price # return self.quantity * self.product.price
# def total_with_discount(self):
# return self.quantity * self.product.get_toman_price_after_discount()
def update_fields(self):
pass
def total_with_discount(self):
return self.quantity * self.product.get_toman_price_after_discount()
def __str__(self): def __str__(self):
return f'({self.product}) - ({self.order.user})' return f'({self.product}) - ({self.order.user})'
-3
View File
@@ -108,7 +108,6 @@ class CartSerializer(serializers.ModelSerializer):
class OrderListSerializer(serializers.ModelSerializer): class OrderListSerializer(serializers.ModelSerializer):
count = serializers.SerializerMethodField() count = serializers.SerializerMethodField()
images = serializers.SerializerMethodField() images = serializers.SerializerMethodField()
order_id = serializers.SerializerMethodField()
verbose_status = serializers.SerializerMethodField() verbose_status = serializers.SerializerMethodField()
class Meta: class Meta:
model = OrderModel model = OrderModel
@@ -127,8 +126,6 @@ class OrderListSerializer(serializers.ModelSerializer):
for item in obj.items.all()[:3] for item in obj.items.all()[:3]
] ]
return filter(lambda x: x is not None, image_list) return filter(lambda x: x is not None, image_list)
def get_order_id(self, obj):
return obj.id + 1000
class OrderGetSerializer(serializers.ModelSerializer): class OrderGetSerializer(serializers.ModelSerializer):
+1 -2
View File
@@ -1,8 +1,7 @@
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from .views import CartItemViews, CartView, OrderlistView, CartItemClear, ApplyDiscountView, OrderGetView, SetAddressForCartView from .views import *
from .views import PaymentView, callback_view
urlpatterns = [ urlpatterns = [
path('all', OrderlistView.as_view(), name='order-list'), path('all', OrderlistView.as_view(), name='order-list'),
+22 -5
View File
@@ -10,7 +10,7 @@ from .models import OrderItemModel, OrderModel, DiscountCode
from .permissons import CanDeleteCartItemPermissions, GetOrderPermission, SetAddressPermissions from .permissons import CanDeleteCartItemPermissions, GetOrderPermission, SetAddressPermissions
from azbankgateways import bankfactories, models as bank_models from azbankgateways import bankfactories, models as bank_models
from azbankgateways.exceptions import AZBankGatewaysException from azbankgateways.exceptions import AZBankGatewaysException
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes, extend_schema_view
from utils.pagination import StructurePagination from utils.pagination import StructurePagination
from order.models import OrderModel from order.models import OrderModel
from django.urls import reverse from django.urls import reverse
@@ -23,7 +23,10 @@ from account.models import UserAddressModel
# pass # pass
@extend_schema_view(
post=extend_schema(tags=["cart discount code"]),
delete=extend_schema(tags=["cart discount code"]),
)
class ApplyDiscountView(APIView): class ApplyDiscountView(APIView):
serializer_class = DiscountCodeSerializer serializer_class = DiscountCodeSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@@ -52,6 +55,9 @@ class ApplyDiscountView(APIView):
class CartItemClear(APIView): class CartItemClear(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
serializer_class = OrderItemSerailzier serializer_class = OrderItemSerailzier
@extend_schema(
tags=["order cart"]
)
def delete(self, request): def delete(self, request):
cart_order, created = OrderModel.objects.get_or_create( cart_order, created = OrderModel.objects.get_or_create(
user=request.user, user=request.user,
@@ -60,7 +66,10 @@ class CartItemClear(APIView):
cart_order.items.all().delete() cart_order.items.all().delete()
return Response({'detail': f'سبد خرید با موفقیت خالی شد'}, status=status.HTTP_204_NO_CONTENT) return Response({'detail': f'سبد خرید با موفقیت خالی شد'}, status=status.HTTP_204_NO_CONTENT)
@extend_schema_view(
post=extend_schema(tags=["order cart"]),
delete=extend_schema(tags=["order cart"]),
)
class CartItemViews(APIView): class CartItemViews(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
serializer_class = OrderItemSerailzier serializer_class = OrderItemSerailzier
@@ -101,6 +110,9 @@ class CartItemViews(APIView):
class CartView(APIView): class CartView(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
serializer_class = CartSerializer serializer_class = CartSerializer
@extend_schema(
tags=["order cart"]
)
def get(self, request): def get(self, request):
user = request.user user = request.user
cart_instance, created = OrderModel.objects.get_or_create(user=user, status='CART') cart_instance, created = OrderModel.objects.get_or_create(user=user, status='CART')
@@ -144,7 +156,8 @@ class OrderlistView(APIView):
required=False, required=False,
type=OpenApiTypes.STR, type=OpenApiTypes.STR,
), ),
] ],
tags=["order"]
) )
def get(self, request): def get(self, request):
user = request.user user = request.user
@@ -191,7 +204,8 @@ class PaymentView(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
serializer_class = BankTypeSerializer serializer_class = BankTypeSerializer
@extend_schema( @extend_schema(
description="choices=['BMI', 'SEP', 'ZARINPAL', 'IDPAY', 'ZIBAL', 'BAHAMTA', 'MELLAT', 'PAYV1']" description="choices=['BMI', 'SEP', 'ZARINPAL', 'IDPAY', 'ZIBAL', 'BAHAMTA', 'MELLAT', 'PAYV1']",
tags=['order payment']
) )
def post(self, request): def post(self, request):
print(request.data.get('gateway_type')) print(request.data.get('gateway_type'))
@@ -261,6 +275,9 @@ class SetAddressSerilizer(serializers.Serializer):
class SetAddressForCartView(APIView): class SetAddressForCartView(APIView):
serializer_class = SetAddressSerilizer serializer_class = SetAddressSerilizer
permission_classes = [IsAuthenticated, SetAddressPermissions] permission_classes = [IsAuthenticated, SetAddressPermissions]
@extend_schema(
tags=["order cart"]
)
def post(self, request): def post(self, request):
address_id = request.data.get('address_id', None) address_id = request.data.get('address_id', None)
if not address_id: if not address_id:
+7 -1
View File
@@ -178,16 +178,22 @@ class CommentModel(models.Model):
class AttributeType(models.Model): class AttributeType(models.Model):
name = models.CharField(verbose_name='نام نوع اتربیوت', max_length=100) name = models.CharField(verbose_name='نام نوع متغییر', max_length=100)
def __str__(self): def __str__(self):
return self.name return self.name
class Meta:
verbose_name = 'نوع متغییر محصول'
verbose_name_plural = 'نوع های متغییر محصول'
class AttributeValue(models.Model): class AttributeValue(models.Model):
attribute_type = models.ForeignKey(AttributeType, on_delete=models.CASCADE, blank=True, null=True) attribute_type = models.ForeignKey(AttributeType, on_delete=models.CASCADE, blank=True, null=True)
value = models.CharField(verbose_name='مقدار نوع اتربیوت', max_length=100, blank=True, null=True) value = models.CharField(verbose_name='مقدار نوع اتربیوت', max_length=100, blank=True, null=True)
class Meta: class Meta:
unique_together = ('attribute_type', 'value') unique_together = ('attribute_type', 'value')
verbose_name = 'مقدار متغییر محصول'
verbose_name_plural = 'مقدار های متغییر محصول'
def __str__(self): def __str__(self):
return f"{self.attribute_type}: {self.value}" return f"{self.attribute_type}: {self.value}"
+4
View File
@@ -20,6 +20,10 @@ class Attachment(models.Model):
self.name = self.file.name self.name = self.file.name
super(Attachment, self).save(*args, **kwargs) super(Attachment, self).save(*args, **kwargs)
class Meta:
verbose_name = 'پیوست تیکت'
verbose_name_plural = 'پیوست های تیکت'
class Ticket(models.Model): class Ticket(models.Model):
# objects = jmodels.jManager() # objects = jmodels.jManager()
STATUS_CHOICES = [ STATUS_CHOICES = [
+4
View File
@@ -9,6 +9,10 @@ services:
- django - django
networks: networks:
- default - default
environment:
- API_BASE_URL="https://api.heymlz.com"
- DEBUG="false"
- NUXT_IMAGE_DOMAINS="https://c262408.parspack.net"
django: django:
container_name: shop_backend container_name: shop_backend
+1 -1
View File
@@ -35,6 +35,6 @@ const closeModal = () => {
/> />
</ToastProvider> </ToastProvider>
<VueQueryDevtools dir="ltr" buttonPosition="bottom-right" /> <VueQueryDevtools dir="ltr" buttonPosition="top-right"/>
</div> </div>
</template> </template>
+25
View File
@@ -44,6 +44,31 @@
} }
} }
@utility btn-primary {
@apply text-white bg-blue-500 border-[1.5px] border-transparent;
@apply btn-lg;
svg[class~="iconify"] path {
@apply stroke-white;
}
&:hover {
@apply bg-transparent border-blue-500 text-blue-500;
svg[class~="iconify"] path {
@apply stroke-blue-500;
}
}
&:disabled {
@apply bg-slate-100 text-slate-400;
svg[class~="iconify"] path {
@apply stroke-slate-400;
}
}
}
@utility btn-secondary { @utility btn-secondary {
@apply text-black bg-slate-100; @apply text-black bg-slate-100;
@apply btn-lg; @apply btn-lg;
+40 -27
View File
@@ -12,42 +12,55 @@ const {} = toRefs(props);
</script> </script>
<template> <template>
<div class="relative w-full flex flex-col justify-center min-h-[450px] md:h-[80svh]"> <div class="relative w-full flex flex-col justify-center min-h-[450px] h-svh">
<div class="flex-col-center gap-6 mb-32">
<span class="typo-h-6 max-sm:text-xl md:typo-h-5 lg:typo-h-4 text-black">
مجله در ستون و سطرآنچ
</span>
<p class="text-slate-500 text-center max-w-[750px] typo-p-lg xl:typo-p-xl">
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و
متون بلکه روزنامه و مجله در ستون و سطرآنچنان که
</p>
</div>
<div class="-rotate-z-2 z-20"> <div class="-rotate-z-2 z-20">
<div <div
class="bg-warning-500 flex pr-20 gap-12 sm:gap-20 py-2 w-max animate-marquee-reverse" class="bg-blue-500 flex items-center pr-20 gap-12 sm:gap-20 w-max animate-marquee-reverse h-[140px]"
> >
<span <template v-for="i in 10">
v-for="i in 10" <div class="text-[30px] lg:text-[40px] text-white whitespace-nowrap font-semibold opacity-85">
class="text-[40px] lg:text-[50px] text-white whitespace-nowrap font-semibold" HEYMLZ
> </div>
TEST {{ i }} <NuxtImg src="/img/heymlz/heymlz-logo.png" class="h-[45px] invert opacity-85" />
</span> </template>
<span <template v-for="i in 10">
v-for="i in 10" <div class="text-[30px] lg:text-[40px] text-white whitespace-nowrap font-semibold opacity-85">
class="text-[40px] lg:text-[50px] text-white whitespace-nowrap font-semibold" HEYMLZ
> </div>
TEST {{ i }} <NuxtImg src="/img/heymlz/heymlz-logo.png" class="h-[45px] invert opacity-85" />
</span> </template>
</div> </div>
</div> </div>
<div class="rotate-z-2 z-10"> <div class="rotate-z-2 z-10">
<div <div
class="bg-slate-50 flex pr-20 gap-12 sm:gap-20 py-2 w-max animate-marquee" class="bg-slate-100/70 flex items-center pr-20 gap-12 sm:gap-20 w-max animate-marquee h-[140px]"
> >
<span <template v-for="i in 1">
v-for="i in 10" <NuxtImg src="/img/brands/brand-1.png" class="h-[45px] grayscale opacity-40" />
class="text-[40px] lg:text-[50px] text-slate-300 whitespace-nowrap font-semibold" <NuxtImg src="/img/brands/brand-2.png" class="h-[45px] grayscale opacity-40" />
> <NuxtImg src="/img/brands/brand-3.png" class="h-[45px] grayscale opacity-40" />
TEST {{ i }} <NuxtImg src="/img/brands/brand-4.png" class="h-[45px] grayscale opacity-40" />
</span> <NuxtImg src="/img/brands/brand-5.png" class="h-[45px] grayscale opacity-40" />
<span <NuxtImg src="/img/brands/brand-6.png" class="h-[45px] grayscale opacity-40" />
v-for="i in 10" </template>
class="text-[40px] lg:text-[50px] text-slate-300 whitespace-nowrap font-semibold" <template v-for="i in 1">
> <NuxtImg src="/img/brands/brand-1.png" class="h-[45px] grayscale opacity-40" />
TEST {{ i }} <NuxtImg src="/img/brands/brand-2.png" class="h-[45px] grayscale opacity-40" />
</span> <NuxtImg src="/img/brands/brand-3.png" class="h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-4.png" class="h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-5.png" class="h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-6.png" class="h-[45px] grayscale opacity-40" />
</template>
</div> </div>
</div> </div>
</div> </div>
+2 -1
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
// types // types
type Props = { type Props = {
variant?: "solid" | "secondary" | "outlined" | "ghost"; variant?: "solid" | "secondary" | "outlined" | "ghost" | "primary";
size?: "xl" | "lg" | "md"; size?: "xl" | "lg" | "md";
startIcon?: string; startIcon?: string;
endIcon?: string; endIcon?: string;
@@ -24,6 +24,7 @@ const classes = computed(() => {
"btn-secondary": variant.value === "secondary", "btn-secondary": variant.value === "secondary",
"btn-outlined": variant.value === "outlined", "btn-outlined": variant.value === "outlined",
"btn-ghost": variant.value === "ghost", "btn-ghost": variant.value === "ghost",
"btn-primary": variant.value === "primary",
}, },
{ {
"btn-xl": size.value === "xl", "btn-xl": size.value === "xl",
+1 -1
View File
@@ -26,7 +26,7 @@ withDefaults(defineProps<Props>(), {
</span> </span>
<NuxtLink to="/products"> <NuxtLink to="/products">
<Button <Button
variant="outlined" variant="primary"
class="rounded-full max-sm:typo-label-sm max-sm:py-2" class="rounded-full max-sm:typo-label-sm max-sm:py-2"
end-icon="ci:arrow-left" end-icon="ci:arrow-left"
> >
+15 -16
View File
@@ -25,28 +25,27 @@ const onSwiper = (swiper: SwiperClass) => {
class="flex flex-col justify-center gap-4 bg-black h-[150svh] relative overflow-hidden" class="flex flex-col justify-center gap-4 bg-black h-[150svh] relative overflow-hidden"
> >
<!-- <div class="w-full flex justify-center items-center relative z-10">--> <div class="w-full relative translate-y-[-200px] z-10">
<!-- <span class="text-white typo-h-6 md:typo-h-5 lg:typo-h-4">--> <div class="flex-col-center gap-6">
<!-- دسته بندی ها--> <span class="text-white typo-h-6 md:typo-h-5 lg:typo-h-4">
<!-- </span>--> دسته بندی ها
<!-- </div>--> </span>
<p class="text-slate-300 text-center max-w-[750px] typo-p-lg xl:typo-p-xl">
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها
و
متون بلکه روزنامه و مجله در ستون و سطرآنچنان که
</p>
</div>
</div>
<NuxtImg
src="/img/categories-gradient.png"
class="animate-spin [animation-duration:16s] object-cover absolute size-full brightness-45 scale-115 aspect-square"
:style="{
maskImage: 'radial-gradient(black, transparent 50%)'
}"
alt=""
/>
<div class="w-full my-20 relative"> <div class="w-full my-20 relative">
<video <video
class="aspect-square w-[450px] translate-[-253px] absolute left-1/2 -translate-x-1/2 z-10" class="aspect-square w-[450px] translate-[-293px] absolute left-1/2 -translate-x-1/2 z-10"
:style="{ :style="{
filter: 'drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.4))', filter: 'drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.4))',
}" }"
src="/video/heymlz/heymlz-seat-2.webm" src="/video/heymlz/heymlz-handshake-part-1.webm"
autoplay autoplay
playsinline playsinline
webkit-playsinline webkit-playsinline
@@ -109,7 +108,7 @@ const onSwiper = (swiper: SwiperClass) => {
<div class="w-full flex justify-center items-center"> <div class="w-full flex justify-center items-center">
<NuxtLink to="/category"> <NuxtLink to="/category">
<Button variant="solid" class="invert rounded-full max-xs:typo-label-sm !px-4 xs:!px-8" <Button variant="primary" class="rounded-full max-xs:typo-label-sm !px-4 xs:!px-8"
end-icon="ci:arrow-left"> end-icon="ci:arrow-left">
مشاهده همه دسته ها مشاهده همه دسته ها
</Button> </Button>
+5 -4
View File
@@ -168,7 +168,7 @@ onUnmounted(() => {
:centered-slides="true" :centered-slides="true"
:breakpoints="{ :breakpoints="{
768: { 768: {
spaceBetween : 40, spaceBetween : 40
} }
}" }"
@swiper="onSwiper" @swiper="onSwiper"
@@ -234,7 +234,8 @@ onUnmounted(() => {
</span> </span>
<NuxtLink :to="slide.link"> <NuxtLink :to="slide.link">
<Button <Button
class="max-sm:hidden max-lg:typo-label-xs invert rounded-full hover:bg-transparent" variant="primary"
class="max-sm:hidden max-lg:typo-label-xs px-7 rounded-full hover:bg-transparent"
> >
مشاهده مشاهده
</Button> </Button>
@@ -249,7 +250,7 @@ onUnmounted(() => {
> >
<button @click="swiper_instance?.slidePrev()"> <button @click="swiper_instance?.slidePrev()">
<Icon <Icon
class="**:stroke-white cursor-pointer size-5 md:size-6" class="**:stroke-white cursor-pointer size-6 md:size-8"
name="ci:arrow-right" name="ci:arrow-right"
/> />
</button> </button>
@@ -264,7 +265,7 @@ onUnmounted(() => {
<button> <button>
<Icon <Icon
@click="swiper_instance?.slideNext()" @click="swiper_instance?.slideNext()"
class="**:stroke-white cursor-pointer size-5 md:size-6" class="**:stroke-white cursor-pointer size-6 md:size-8"
name="ci:arrow-left" name="ci:arrow-left"
/> />
</button> </button>
+1 -1
View File
@@ -20,7 +20,7 @@ await suspense();
مقالات اخیر سایت مقالات اخیر سایت
</span> </span>
<NuxtLink to="/articles"> <NuxtLink to="/articles">
<Button variant="outlined" class="rounded-full max-sm:typo-label-sm max-sm:py-2" <Button variant="primary" class="rounded-full max-sm:typo-label-sm max-sm:py-2"
end-icon="ci:arrow-left"> end-icon="ci:arrow-left">
نمایش همه نمایش همه
</Button> </Button>
+52 -18
View File
@@ -15,6 +15,13 @@ const activeSlideVideo = ref<"left" | "right" | "none">("none");
const draggableEl = ref<HTMLElement | null>(null); const draggableEl = ref<HTMLElement | null>(null);
const previewContainerEl = ref<HTMLElement | null>(null); const previewContainerEl = ref<HTMLElement | null>(null);
const heymlzElement = useTemplateRef<HTMLDivElement>("heymlzElement");
const heymlzElementIsVisible = useElementVisibility(heymlzElement, {
rootMargin: "0px 0px -40% 0px"
});
const showHeymlzAnimation = ref(false);
const { x: dragAxisX } = useDraggable(draggableEl, { const { x: dragAxisX } = useDraggable(draggableEl, {
initialValue: { x: 0, y: 0 }, initialValue: { x: 0, y: 0 },
axis: "x" axis: "x"
@@ -22,6 +29,17 @@ const { x: dragAxisX } = useDraggable(draggableEl, {
// watch // watch
watch(heymlzElementIsVisible, (newValue) => {
if (newValue) {
showHeymlzAnimation.value = true;
setTimeout(() => {
showHeymlzAnimation.value = false;
}, 3200);
}
}, {
once: true
});
watch(() => clipPathPercent.value, (newValue) => { watch(() => clipPathPercent.value, (newValue) => {
if (newValue > 80) { if (newValue > 80) {
activeSlideVideo.value = "right"; activeSlideVideo.value = "right";
@@ -51,18 +69,19 @@ watch(
<div class="flex flex-col items-center gap-3 mb-10 lg:mb-16"> <div class="flex flex-col items-center gap-3 mb-10 lg:mb-16">
<span class="typo-p-sm md:typo-p-md text-slate-500">مقایسه محصولات</span> <span class="typo-p-sm md:typo-p-md text-slate-500">مقایسه محصولات</span>
<span class="typo-h-6 md:typo-h-5 lg:typo-h-3 text-black"> <span class="typo-h-6 md:typo-h-5 lg:typo-h-3 text-black">
تفاوت محصلات ما را ببینید تفاوت محصلات ما را ببینید
</span> </span>
</div> </div>
<div <div
ref="previewContainerEl" ref="previewContainerEl"
class="rounded-200 overflow-hidden h-[70svh] md:h-[80svh] relative" class="rounded-200 overflow-hidden h-[70svh] relative"
> >
<Transition name="fade"> <Transition name="fade">
<NuxtImg <NuxtImg
v-if="activeSlideVideo !== 'right'" v-if="activeSlideVideo !== 'right'"
:src="homeData!.difreance_section.image1" :src="homeData!.difreance_section.image1"
class="select-none absolute size-full object-cover brightness-[95%]" :class="showHeymlzAnimation ? 'brightness-35' : 'brightness-[95%]'"
class="select-none absolute size-full object-cover transition-[filter] duration-250"
:alt="homeData!.difreance_section.title1" :alt="homeData!.difreance_section.title1"
/> />
<video <video
@@ -76,13 +95,14 @@ watch(
/> />
</Transition> </Transition>
<div class="absolute size-full right-0 w-full"> <div class="absolute size-full right-0 w-full" ref="heymlzElement">
<Transition name="fade"> <Transition name="fade">
<NuxtImg <NuxtImg
v-if="activeSlideVideo !== 'left'" v-if="activeSlideVideo !== 'left'"
:src="homeData!.difreance_section.image2" :src="homeData!.difreance_section.image2"
class="overlay-image select-none absolute object-cover size-full brightness-[95%]" :class="showHeymlzAnimation ? 'brightness-35' : 'brightness-[95%]'"
class="overlay-image select-none absolute object-cover size-full transition-[filter] duration-250"
:alt="homeData!.difreance_section.title2" :alt="homeData!.difreance_section.title2"
/> />
<video <video
@@ -97,12 +117,14 @@ watch(
</Transition> </Transition>
<video <video
v-if="showHeymlzAnimation"
src="/video/heymlz/heymlz-pulling.webm" src="/video/heymlz/heymlz-pulling.webm"
autoplay autoplay
muted muted
playsinline playsinline
loop
webkit-playsinline webkit-playsinline
class="size-[300px] absolute translate-x-[-100px] z-10 top-[32%] -translate-y-1/2" class="size-[400px] absolute translate-x-[-107px] z-10 top-[50%] -translate-y-1/2"
:style="{ :style="{
left: `${clipPathPercent}%`, left: `${clipPathPercent}%`,
filter: 'drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.3))' filter: 'drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.3))'
@@ -113,17 +135,23 @@ watch(
:style="{ :style="{
left: `${clipPathPercent}%`, left: `${clipPathPercent}%`,
}" }"
class="select-none w-2 h-full bg-black absolute left-0 flex items-center justify-center" :class="[
activeSlideVideo !== 'none' ? 'opacity-10' : '',
showHeymlzAnimation ? 'bg-neutral-300' : 'bg-black'
]"
class="select-none w-2 h-full absolute left-0 flex items-center justify-center transition-opacity duration-250"
> >
<div <div
ref="draggableEl" ref="draggableEl"
class="cursor-grab hover:scale-115 transition-transform rounded-full absolute bg-black size-11 flex items-center justify-center" :class="showHeymlzAnimation ? 'bg-neutral-300' : 'bg-black'"
class="cursor-grab hover:scale-115 transition-transform rounded-full absolute size-11 flex items-center justify-center"
> >
<Icon <Icon
name="ci:arrows" name="ci:arrows"
size="24" size="24"
class="**:stroke-white" class="transition-all"
:class="showHeymlzAnimation ? '**:stroke-black' : '**:stroke-white'"
/> />
</div> </div>
</div> </div>
@@ -132,10 +160,13 @@ watch(
<div <div
class="max-xs:hidden absolute bottom-0 p-6 md:p-10 w-full flex justify-between items-end" class="max-xs:hidden absolute bottom-0 p-6 md:p-10 w-full flex justify-between items-end"
> >
<div class="flex flex-col gap-2 text-black"> <div
<span class="typo-p-sm md:typo-p-md"> class="flex flex-col gap-2 text-black transition-opacity"
{{ homeData!.difreance_section.description1 }} :class="activeSlideVideo === 'right' ? 'opacity-0' : ''"
</span> >
<span class="typo-p-sm md:typo-p-md">
{{ homeData!.difreance_section.description1 }}
</span>
<NuxtLink <NuxtLink
:to="homeData!.difreance_section.link1" :to="homeData!.difreance_section.link1"
class="typo-h-6 md:typo-h-5 lg:typo-h-3" class="typo-h-6 md:typo-h-5 lg:typo-h-3"
@@ -143,10 +174,13 @@ watch(
{{ homeData!.difreance_section.title1 }} {{ homeData!.difreance_section.title1 }}
</NuxtLink> </NuxtLink>
</div> </div>
<div class="flex flex-col gap-2 text-black"> <div
<span class="typo-p-sm md:typo-p-md text-end"> class="flex flex-col gap-2 text-black transition-opacity"
{{ homeData!.difreance_section.description2 }} :class="activeSlideVideo === 'left' ? 'opacity-0' : ''"
</span> >
<span class="typo-p-sm md:typo-p-md text-end">
{{ homeData!.difreance_section.description2 }}
</span>
<NuxtLink <NuxtLink
:to="homeData!.difreance_section.link2" :to="homeData!.difreance_section.link2"
class="typo-h-6 md:typo-h-5 lg:typo-h-3 text-end" class="typo-h-6 md:typo-h-5 lg:typo-h-3 text-end"
+10 -2
View File
@@ -92,7 +92,6 @@ onUnmounted(() => {
:to="slide.link" :to="slide.link"
class="showcase-slide origin-bottom absolute size-full bg-transparent flex items-center justify-center" class="showcase-slide origin-bottom absolute size-full bg-transparent flex items-center justify-center"
> >
<NuxtImg <NuxtImg
class="w-[280px] xs:w-[350px] lg:w-[500px] xl:w-[650px] z-20 mb-30 sm:mb-20 lg:mb-30" class="w-[280px] xs:w-[350px] lg:w-[500px] xl:w-[650px] z-20 mb-30 sm:mb-20 lg:mb-30"
:src="slide.image" :src="slide.image"
@@ -101,13 +100,22 @@ onUnmounted(() => {
}" }"
alt="" alt=""
/> />
<div class="flex flex-col items-center justify-center gap-4 text-center absolute z-20 mt-20"> <div class="flex flex-col items-center justify-center gap-6 text-center absolute z-20 mt-20">
<span class="text-white typo-h-6 sm:typo-h-5 lg:typo-h-4 xl:typo-h-3"> <span class="text-white typo-h-6 sm:typo-h-5 lg:typo-h-4 xl:typo-h-3">
{{ slide.title }} {{ slide.title }}
</span> </span>
<p class="text-white max-w-[320px] xs:max-w-[360px] sm:max-w-[480px] lg:max-w-[550px] xl:max-w-[750px] typo-p-sm lg:typo-p-md xl:typo-p-lg"> <p class="text-white max-w-[320px] xs:max-w-[360px] sm:max-w-[480px] lg:max-w-[550px] xl:max-w-[750px] typo-p-sm lg:typo-p-md xl:typo-p-lg">
{{ slide.description }} {{ slide.description }}
</p> </p>
<NuxtLink :to="slide.link">
<Button
variant="primary"
end-icon="ci:arrow-left"
class="mt-8 max-sm:hidden max-lg:typo-label-xs px-10 rounded-full hover:bg-transparent"
>
مشاهده دسته بندی
</Button>
</NuxtLink>
</div> </div>
</NuxtLink> </NuxtLink>
</div> </div>
@@ -21,18 +21,18 @@ const { circle } = toRefs(props);
<div <div
:style="{ :style="{
height: `${size}px`, height: `${size}px`,
width: circle ? `${size}px` : '100%', width: circle ? `${size}px` : '100%'
}" }"
class="relative flex items-center w-full justify-center shrink-0" class="relative flex items-center w-full justify-center shrink-0"
:class="{ :class="{
'rounded-full overflow-hidden': circle, 'rounded-full overflow-hidden': circle,
}" }"
> >
<img <NuxtImg
:style="{ :style="{
maskImage: 'radial-gradient(black, transparent 70%)' maskImage: 'radial-gradient(black, transparent 70%)'
}" }"
src="/public/img/ai-loading-2.gif" src="/img/heymlz/heymlz-idle.gif"
class="size-full object-cover absolute pt-2" class="size-full object-cover absolute pt-2"
alt="ai-loading" alt="ai-loading"
/> />
@@ -20,6 +20,8 @@ const { isLoggedIn } = useAuth();
const route = useRoute(); const route = useRoute();
const id = route.params.id as string | number; const id = route.params.id as string | number;
const scrollToBottomTimer = ref<NodeJS.Timeout | null>(null);
const chatContainerEl = ref<HTMLElement | null>(null); const chatContainerEl = ref<HTMLElement | null>(null);
const lastMessageBeforeUpdate = ref(0); const lastMessageBeforeUpdate = ref(0);
@@ -63,7 +65,10 @@ useInfiniteScroll(
// methods // methods
const scrollToBottom = () => { const scrollToBottom = () => {
chatContainerScrollY.value = chatContainerEl.value?.scrollHeight ?? 0; if (scrollToBottomTimer.value) clearTimeout(scrollToBottomTimer.value);
scrollToBottomTimer.value = setTimeout(() => {
chatContainerScrollY.value = chatContainerEl.value?.scrollHeight ?? 0;
}, 50);
}; };
// computed // computed
@@ -137,8 +142,7 @@ whenever(
> >
<div <div
:style="{ :style="{
maskImage: maskImage: 'linear-gradient(to top, transparent, black 5%, black, black)'
'linear-gradient(to top, transparent, black 5%, black, black)',
}" }"
class="hide-scrollbar flex flex-col py-7 gap-6 h-full overflow-y-auto" class="hide-scrollbar flex flex-col py-7 gap-6 h-full overflow-y-auto"
ref="chatContainerEl" ref="chatContainerEl"
@@ -184,11 +188,20 @@ whenever(
</Transition> </Transition>
</template> </template>
<div <div
class="text-black p-4.5 size-full flex justify-center items-center" class="text-black p-6 size-full flex justify-center items-center flex-col"
v-else v-else
> >
<img class="size-[50px]" src="/img/heymlz/heymlz-idle.gif" alt="" /> <NuxtImg class="size-[250px]" src="/img/heymlz/heymlz-loading-1.gif" alt="" />
Please sign in first <div class="flex flex-col gap-4 items-center">
<span class="text-center typo-p-xl font-bold">سلام دوست عزیز!</span>
<p class="text-center typo-p-md">
من میتونم هر سوالی رو درمورد این محصول جواب بدم
اگه میخوای شروع کنیم روی دکمه زیر کلیک کن
</p>
</div>
<NuxtLink to="/signin">
<Button class="mt-8 rounded-full px-10">ورود به فروشگاه</Button>
</NuxtLink>
</div> </div>
</div> </div>
</Transition> </Transition>
@@ -7,6 +7,9 @@ import { useToast } from "~/composables/global/useToast";
// state // state
const route = useRoute();
const id = route.params.id as string | number;
const { $queryClient: queryClient } = useNuxtApp(); const { $queryClient: queryClient } = useNuxtApp();
const { addToast } = useToast(); const { addToast } = useToast();
@@ -27,7 +30,7 @@ const sendMessage = async () => {
await createMessage({ await createMessage({
new_message: value, new_message: value,
productId: 1, productId: id,
}); });
} catch (e) { } catch (e) {
addToast({ addToast({
@@ -54,13 +54,12 @@ onMounted(() => {
`#chat-message-content-${id.value}`, `#chat-message-content-${id.value}`,
{ {
text: "", text: "",
duration: 2.5,
ease: "none", ease: "none",
}, },
{ {
text: { value: content.value, rtl: false }, text: { value: content.value, rtl: false },
duration: 2.5,
ease: "none", ease: "none",
duration: 2.5,
onUpdate: () => emit("textUpdate"), onUpdate: () => emit("textUpdate"),
} }
); );
@@ -78,9 +77,9 @@ onMounted(() => {
<div <div
class="relative overflow-hidden flex items-center justify-center mt-px bg-slate-300 rounded-full size-[35px] shrink-0" class="relative overflow-hidden flex items-center justify-center mt-px bg-slate-300 rounded-full size-[35px] shrink-0"
> >
<img <NuxtImg
v-if="!reverse" v-if="reverse"
src="/img/footer-bg.jpg" src="/img/heymlz/footer-share.svg"
class="size-full object-cover absolute" class="size-full object-cover absolute"
alt="profile" alt="profile"
/> />
+4 -4
View File
@@ -6,9 +6,9 @@ import { useAuth } from "~/composables/api/auth/useAuth";
// types // types
export type GetBranchResponse = ApiPaginated<Chat>; export type GetChatResponse = ApiPaginated<Chat>;
const useGetBranch = (productId: string | number, enabled: Ref<boolean>) => { const useGetChat = (productId: string | number, enabled: Ref<boolean>) => {
// state // state
const { $axios: axios } = useNuxtApp(); const { $axios: axios } = useNuxtApp();
@@ -26,7 +26,7 @@ const useGetBranch = (productId: string | number, enabled: Ref<boolean>) => {
limit: number; limit: number;
offset: number; offset: number;
}) => { }) => {
const { data } = await axios.get<GetBranchResponse>( const { data } = await axios.get<GetChatResponse>(
`${API_ENDPOINTS.chat.messages}/${productId}`, `${API_ENDPOINTS.chat.messages}/${productId}`,
{ {
params: { params: {
@@ -65,4 +65,4 @@ const useGetBranch = (productId: string | number, enabled: Ref<boolean>) => {
}); });
}; };
export default useGetBranch; export default useGetChat;
-1
View File
@@ -15,7 +15,6 @@ const disableLoadingOverlay = useState("disableLoadingOverlay", () => false);
const response = await suspense(); const response = await suspense();
if (response.isError) { if (response.isError) {
console.log(response);
throw createError({ throw createError({
statusCode: 500, statusCode: 500,
statusMessage: `Landing error : ${response.error.message}` statusMessage: `Landing error : ${response.error.message}`
+5 -5
View File
@@ -160,8 +160,8 @@ const resetForm = () => {
/> />
<div class="flex items-center justify-center flex-col size-full translate-y-[-80px]"> <div class="flex items-center justify-center flex-col size-full translate-y-[-80px]">
<video <video
class="aspect-square w-[450px] translate-y-[197px] animate-fade-in" class="aspect-square w-[450px] translate-y-[157px] animate-fade-in"
src="/video/heymlz/heymlz-seat-2.webm" src="/video/heymlz/heymlz-handshake-full.webm"
:style="{ :style="{
filter: 'drop-shadow(0px 4px 20px rgba(0, 0, 0, 0.15))' filter: 'drop-shadow(0px 4px 20px rgba(0, 0, 0, 0.15))'
}" }"
@@ -179,9 +179,9 @@ const resetForm = () => {
<form @submit.prevent class="max-w-[500px] w-full mt-12"> <form @submit.prevent class="max-w-[500px] w-full mt-12">
<Input <Input
v-if="!showOtp" v-if="!showOtp"
class="w-full tracking-[2px]" class="w-full tracking-[3px] persian-number"
v-model="loginInfo.phone" v-model="loginInfo.phone"
placeholder="9380123456" placeholder="۹۳۸۰۱۲۳۴۵۶"
dir="ltr" dir="ltr"
:error="formValidator$.phone.$error" :error="formValidator$.phone.$error"
> >
@@ -193,7 +193,7 @@ const resetForm = () => {
size="24" size="24"
/> />
<span class="text-slate-500 typo-label-sm"> <span class="text-slate-500 typo-label-sm">
+98 +۹۸
</span> </span>
</div> </div>
</template> </template>
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.
Binary file not shown.