Merge branch 'main' of https://github.com/Byeto-Company/hossein_por_shop
@@ -4,4 +4,6 @@ from django.apps import AppConfig
|
||||
class AccountConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'account'
|
||||
verbose_name = 'اکانت'
|
||||
verbose_name = 'اکانت'
|
||||
def ready(self):
|
||||
import account.signals
|
||||
@@ -129,7 +129,7 @@ class UserAddressModel(models.Model):
|
||||
city = models.CharField(max_length=30, verbose_name='شهر')
|
||||
province = models.CharField(max_length=30, 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):
|
||||
return f"{self.user.phone}, {self.name}"
|
||||
@@ -152,6 +152,10 @@ class PushSubscription(models.Model):
|
||||
def __str__(self):
|
||||
return f'{self.user} push'
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'اشتراک نوتیفیکیشن'
|
||||
verbose_name_plural = 'اشتراک های نوتیفیکیشن'
|
||||
|
||||
def send_notif(self, title, body, icon):
|
||||
payload = {
|
||||
"title": 'فروشگاه هی ملز',
|
||||
|
||||
@@ -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,12 +1,11 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
from djoser.urls.jwt import views as djoser_jwt_views
|
||||
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
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('yee_token_bedeeee', views.KonGhoshadToken.as_view()),
|
||||
path('address/create', views.CreateAddressView.as_view(), name='create-address'),
|
||||
|
||||
@@ -5,8 +5,8 @@ from rest_framework.response import Response
|
||||
from .serializers import *
|
||||
from .models import UserAddressModel, User, SecurityBreachAttemptModel
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||
from rest_framework_simplejwt.views import TokenObtainPairView
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter, extend_schema_view
|
||||
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
import ghasedak_sms
|
||||
@@ -19,7 +19,7 @@ from rest_framework_simplejwt.tokens import RefreshToken
|
||||
class SendOTPView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
@extend_schema(
|
||||
tags=["Authentication"],
|
||||
tags=["authentication"],
|
||||
request={
|
||||
"application/json": {
|
||||
"type": "object",
|
||||
@@ -65,12 +65,18 @@ Code: {otp}"""
|
||||
except Exception as e:
|
||||
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)
|
||||
@extend_schema_view(
|
||||
post=extend_schema(tags=['authentication'])
|
||||
)
|
||||
class TokenRefreshView(TokenRefreshView):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class CustomTokenObtainPairView(TokenObtainPairView):
|
||||
serializer_class = CustomTokenObtainPairSerializer
|
||||
@extend_schema(
|
||||
tags=["Authentication"]
|
||||
tags=["authentication"]
|
||||
)
|
||||
def post(self, request, *args, **kwargs):
|
||||
phone = request.data.get("phone")
|
||||
@@ -93,10 +99,10 @@ class CustomTokenObtainPairView(TokenObtainPairView):
|
||||
|
||||
|
||||
|
||||
class KonGhoshadToken(TokenObtainPairView):
|
||||
class KonGhoshadToken(APIView):
|
||||
serializer_class = CustomTokenObtainPairSerializer
|
||||
@extend_schema(
|
||||
tags=["Authentication"]
|
||||
tags=["authentication"]
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
random_user = User.objects.all().first()
|
||||
@@ -116,12 +122,16 @@ class KonGhoshadToken(TokenObtainPairView):
|
||||
class ProfileView(APIView):
|
||||
serializer_class = ProfileSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(
|
||||
tags=["accounts profile"]
|
||||
)
|
||||
def get(self, request):
|
||||
user_ser = self.serializer_class(instance=request.user, context={'request': request})
|
||||
return Response(user_ser.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
tags=["accounts profile"]
|
||||
)
|
||||
def patch(self, request):
|
||||
user = request.user
|
||||
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.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
post=extend_schema(tags=["accounts address"])
|
||||
)
|
||||
class CreateAddressView(generics.CreateAPIView):
|
||||
queryset = UserAddressModel.objects.all()
|
||||
serializer_class = UserAddressSerializer
|
||||
@@ -139,7 +153,10 @@ class CreateAddressView(generics.CreateAPIView):
|
||||
user = self.request.user
|
||||
is_first_address = not UserAddressModel.objects.filter(user=user).exists()
|
||||
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):
|
||||
queryset = UserAddressModel.objects.all()
|
||||
serializer_class = UserAddressSerializer
|
||||
@@ -147,21 +164,27 @@ class EditAddressView(generics.UpdateAPIView):
|
||||
|
||||
def get_queryset(self):
|
||||
return UserAddressModel.objects.filter(user=self.request.user)
|
||||
|
||||
@extend_schema_view(
|
||||
delete=extend_schema(tags=["accounts address"])
|
||||
)
|
||||
class DeleteAddressView(generics.DestroyAPIView):
|
||||
queryset = UserAddressModel.objects.all()
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return UserAddressModel.objects.filter(user=self.request.user)
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(tags=["accounts address"])
|
||||
)
|
||||
class GetUserAddressesView(generics.ListAPIView):
|
||||
serializer_class = UserAddressSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return UserAddressModel.objects.filter(user=self.request.user)
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(tags=["accounts address"])
|
||||
)
|
||||
class GetIDUserAddressView(generics.RetrieveAPIView):
|
||||
serializer_class = UserAddressSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
@@ -174,6 +197,9 @@ class GetIDUserAddressView(generics.RetrieveAPIView):
|
||||
class SubscribeView(APIView):
|
||||
serializer_class = PushSubscriptionSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
@extend_schema(
|
||||
tags=["accounts subscribe"]
|
||||
)
|
||||
def post(self, request):
|
||||
push_ser = self.serializer_class(data=request.data)
|
||||
if push_ser.is_valid():
|
||||
@@ -191,6 +217,9 @@ class UnsubscribeSerializer(serializers.Serializer):
|
||||
class UnsubscribeView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = UnsubscribeSerializer
|
||||
@extend_schema(
|
||||
tags=["accounts subscribe"]
|
||||
)
|
||||
def post(self, request):
|
||||
endpoint = request.data.get("end_point")
|
||||
if not endpoint:
|
||||
@@ -221,6 +250,7 @@ class LogoutView(APIView):
|
||||
|
||||
@extend_schema(
|
||||
request=LogoutSerializer,
|
||||
tags=["authentication"],
|
||||
responses={205: None, 400: "Bad request (invalid token or missing data)"},
|
||||
)
|
||||
def post(self, request):
|
||||
@@ -230,4 +260,12 @@ class LogoutView(APIView):
|
||||
token.blacklist()
|
||||
return Response(status=status.HTTP_205_RESET_CONTENT)
|
||||
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
|
||||
@@ -66,6 +66,7 @@ class BankAdmin(ModelAdmin):
|
||||
"extra_information",
|
||||
"created_at",
|
||||
"update_at",
|
||||
'order',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class Bank(models.Model):
|
||||
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"))
|
||||
update_at = models.DateTimeField(auto_now=True, editable=False, verbose_name=_("Updated at"))
|
||||
|
||||
@@ -3,9 +3,9 @@ from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from drf_spectacular.views import SpectacularSwaggerView, SpectacularAPIView
|
||||
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 account.views import CustomTokenObtainPairView
|
||||
from account.views import CustomTokenObtainPairView, TokenRefreshView
|
||||
from home.views import HomeView
|
||||
from .views import FakeAdminLoginView
|
||||
from azbankgateways.urls import az_bank_gateways_urls
|
||||
|
||||
@@ -35,6 +35,7 @@ class BankRecordInline(StackedInline):
|
||||
model = Bank
|
||||
extra = 0
|
||||
max_num = 0
|
||||
tab = True
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
@@ -46,11 +47,11 @@ class BankRecordInline(StackedInline):
|
||||
class OrderAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||
import_form_class = ImportForm
|
||||
export_form_class = ExportForm
|
||||
|
||||
search_fields = ['order_id', 'user__phone', 'user__first_name', 'user__last_name', 'user__email']
|
||||
list_filter = ['is_paid', 'status']
|
||||
actions_list = ['redirect_to_learn', 'udpate_bank_status']
|
||||
list_display = ['user', 'is_paid', 'status', 'discount_code', 'address',]
|
||||
readonly_fields = ('created_at', )
|
||||
list_display = ['order_id', 'user', 'is_paid', 'status', 'discount_code', 'address',]
|
||||
readonly_fields = ('created_at', 'order_id', 'tax', 'final_price', 'cart_total', 'discount', 'discount_code', 'user', 'address', 'is_paid')
|
||||
compressed_fields = True
|
||||
warn_unsaved_form = True
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -44,6 +44,7 @@ class OrderModel(models.Model):
|
||||
('CANCELED', 'لغو شده'),
|
||||
('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='کاربر')
|
||||
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="تاریخ ثبت سفارش")
|
||||
@@ -54,7 +55,6 @@ class OrderModel(models.Model):
|
||||
tax = 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='کل سبد خرید')
|
||||
# bank_records = models.ManyToManyField(Bank, max_length=100, verbose_name='رکورد بانکی', null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f'سفارش: {self.id + 1000}'
|
||||
@@ -66,39 +66,32 @@ class OrderModel(models.Model):
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
try:
|
||||
push_object = PushSubscription.objects.get(user=self.user)
|
||||
except:
|
||||
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')
|
||||
if not self.pk:
|
||||
last_instance = self.__class__.objects.order_by("pk").last()
|
||||
self.order_id = (last_instance.pk + 1001) if last_instance else 1001
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
# def discount(self):
|
||||
# # total_with_item_discount = sum(item.total_with_discount() for item in self.items.all())
|
||||
# # discount_percent = self.discount_code.percent
|
||||
# # return total_with_item_discount * ((100 - discount_percent) / 100)
|
||||
# pass
|
||||
def cal_discount(self):
|
||||
# total_with_item_discount = sum(item.total_with_discount() for item in self.items.all())
|
||||
# discount_percent = self.discount_code.percent
|
||||
# return total_with_item_discount * ((100 - discount_percent) / 100)
|
||||
pass
|
||||
|
||||
|
||||
# def tax(self):
|
||||
# return self.total_without_tax() * 0.2
|
||||
def cal_tax(self):
|
||||
return self.total_without_tax() * 0.2
|
||||
|
||||
|
||||
# def total(self):
|
||||
# pass
|
||||
# return self.total_with_discount() + self.tax()
|
||||
def cal_total(self):
|
||||
pass
|
||||
return self.total_with_discount() + self.tax()
|
||||
|
||||
|
||||
# def final_price(self):
|
||||
# pass
|
||||
def cal_final_price(self):
|
||||
pass
|
||||
|
||||
# def submit_cart(self):
|
||||
# pass
|
||||
|
||||
|
||||
|
||||
@@ -108,15 +101,20 @@ class OrderItemModel(models.Model):
|
||||
quantity = models.PositiveSmallIntegerField(verbose_name="تعداد")
|
||||
price = models.PositiveIntegerField(verbose_name='قیمت', default=0)
|
||||
product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول")
|
||||
discount = models.SmallIntegerField(default=0, verbose_name='تخفیف')
|
||||
class Meta:
|
||||
verbose_name = 'ایتم سبد خرید'
|
||||
verbose_name_plural = 'ایتم های سبد خرید'
|
||||
|
||||
def total(self):
|
||||
return self.quantity * self.product.price
|
||||
# def total(self):
|
||||
# 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):
|
||||
return f'({self.product}) - ({self.order.user})'
|
||||
|
||||
|
||||
@@ -108,7 +108,6 @@ class CartSerializer(serializers.ModelSerializer):
|
||||
class OrderListSerializer(serializers.ModelSerializer):
|
||||
count = serializers.SerializerMethodField()
|
||||
images = serializers.SerializerMethodField()
|
||||
order_id = serializers.SerializerMethodField()
|
||||
verbose_status = serializers.SerializerMethodField()
|
||||
class Meta:
|
||||
model = OrderModel
|
||||
@@ -127,8 +126,6 @@ class OrderListSerializer(serializers.ModelSerializer):
|
||||
for item in obj.items.all()[:3]
|
||||
]
|
||||
return filter(lambda x: x is not None, image_list)
|
||||
def get_order_id(self, obj):
|
||||
return obj.id + 1000
|
||||
|
||||
|
||||
class OrderGetSerializer(serializers.ModelSerializer):
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from .views import CartItemViews, CartView, OrderlistView, CartItemClear, ApplyDiscountView, OrderGetView, SetAddressForCartView
|
||||
from .views import PaymentView, callback_view
|
||||
from .views import *
|
||||
|
||||
urlpatterns = [
|
||||
path('all', OrderlistView.as_view(), name='order-list'),
|
||||
|
||||
@@ -10,7 +10,7 @@ from .models import OrderItemModel, OrderModel, DiscountCode
|
||||
from .permissons import CanDeleteCartItemPermissions, GetOrderPermission, SetAddressPermissions
|
||||
from azbankgateways import bankfactories, models as bank_models
|
||||
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 order.models import OrderModel
|
||||
from django.urls import reverse
|
||||
@@ -23,7 +23,10 @@ from account.models import UserAddressModel
|
||||
# pass
|
||||
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
post=extend_schema(tags=["cart discount code"]),
|
||||
delete=extend_schema(tags=["cart discount code"]),
|
||||
)
|
||||
class ApplyDiscountView(APIView):
|
||||
serializer_class = DiscountCodeSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
@@ -52,6 +55,9 @@ class ApplyDiscountView(APIView):
|
||||
class CartItemClear(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = OrderItemSerailzier
|
||||
@extend_schema(
|
||||
tags=["order cart"]
|
||||
)
|
||||
def delete(self, request):
|
||||
cart_order, created = OrderModel.objects.get_or_create(
|
||||
user=request.user,
|
||||
@@ -60,7 +66,10 @@ class CartItemClear(APIView):
|
||||
cart_order.items.all().delete()
|
||||
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):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = OrderItemSerailzier
|
||||
@@ -101,6 +110,9 @@ class CartItemViews(APIView):
|
||||
class CartView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = CartSerializer
|
||||
@extend_schema(
|
||||
tags=["order cart"]
|
||||
)
|
||||
def get(self, request):
|
||||
user = request.user
|
||||
cart_instance, created = OrderModel.objects.get_or_create(user=user, status='CART')
|
||||
@@ -144,7 +156,8 @@ class OrderlistView(APIView):
|
||||
required=False,
|
||||
type=OpenApiTypes.STR,
|
||||
),
|
||||
]
|
||||
],
|
||||
tags=["order"]
|
||||
)
|
||||
def get(self, request):
|
||||
user = request.user
|
||||
@@ -191,7 +204,8 @@ class PaymentView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = BankTypeSerializer
|
||||
@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):
|
||||
print(request.data.get('gateway_type'))
|
||||
@@ -261,6 +275,9 @@ class SetAddressSerilizer(serializers.Serializer):
|
||||
class SetAddressForCartView(APIView):
|
||||
serializer_class = SetAddressSerilizer
|
||||
permission_classes = [IsAuthenticated, SetAddressPermissions]
|
||||
@extend_schema(
|
||||
tags=["order cart"]
|
||||
)
|
||||
def post(self, request):
|
||||
address_id = request.data.get('address_id', None)
|
||||
if not address_id:
|
||||
|
||||
@@ -178,16 +178,22 @@ class CommentModel(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):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'نوع متغییر محصول'
|
||||
verbose_name_plural = 'نوع های متغییر محصول'
|
||||
|
||||
class AttributeValue(models.Model):
|
||||
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)
|
||||
class Meta:
|
||||
unique_together = ('attribute_type', 'value')
|
||||
verbose_name = 'مقدار متغییر محصول'
|
||||
verbose_name_plural = 'مقدار های متغییر محصول'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.attribute_type}: {self.value}"
|
||||
|
||||
@@ -20,6 +20,10 @@ class Attachment(models.Model):
|
||||
self.name = self.file.name
|
||||
super(Attachment, self).save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'پیوست تیکت'
|
||||
verbose_name_plural = 'پیوست های تیکت'
|
||||
|
||||
class Ticket(models.Model):
|
||||
# objects = jmodels.jManager()
|
||||
STATUS_CHOICES = [
|
||||
|
||||
@@ -9,6 +9,10 @@ services:
|
||||
- django
|
||||
networks:
|
||||
- default
|
||||
environment:
|
||||
- API_BASE_URL="https://api.heymlz.com"
|
||||
- DEBUG="false"
|
||||
- NUXT_IMAGE_DOMAINS="https://c262408.parspack.net"
|
||||
|
||||
django:
|
||||
container_name: shop_backend
|
||||
|
||||
@@ -35,6 +35,6 @@ const closeModal = () => {
|
||||
/>
|
||||
</ToastProvider>
|
||||
|
||||
<VueQueryDevtools dir="ltr" buttonPosition="bottom-right" />
|
||||
<VueQueryDevtools dir="ltr" buttonPosition="top-right"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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 {
|
||||
@apply text-black bg-slate-100;
|
||||
@apply btn-lg;
|
||||
|
||||
@@ -12,42 +12,55 @@ const {} = toRefs(props);
|
||||
</script>
|
||||
|
||||
<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="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
|
||||
v-for="i in 10"
|
||||
class="text-[40px] lg:text-[50px] text-white whitespace-nowrap font-semibold"
|
||||
>
|
||||
TEST {{ i }}
|
||||
</span>
|
||||
<span
|
||||
v-for="i in 10"
|
||||
class="text-[40px] lg:text-[50px] text-white whitespace-nowrap font-semibold"
|
||||
>
|
||||
TEST {{ i }}
|
||||
</span>
|
||||
<template v-for="i in 10">
|
||||
<div class="text-[30px] lg:text-[40px] text-white whitespace-nowrap font-semibold opacity-85">
|
||||
HEYMLZ
|
||||
</div>
|
||||
<NuxtImg src="/img/heymlz/heymlz-logo.png" class="h-[45px] invert opacity-85" />
|
||||
</template>
|
||||
<template v-for="i in 10">
|
||||
<div class="text-[30px] lg:text-[40px] text-white whitespace-nowrap font-semibold opacity-85">
|
||||
HEYMLZ
|
||||
</div>
|
||||
<NuxtImg src="/img/heymlz/heymlz-logo.png" class="h-[45px] invert opacity-85" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rotate-z-2 z-10">
|
||||
<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
|
||||
v-for="i in 10"
|
||||
class="text-[40px] lg:text-[50px] text-slate-300 whitespace-nowrap font-semibold"
|
||||
>
|
||||
TEST {{ i }}
|
||||
</span>
|
||||
<span
|
||||
v-for="i in 10"
|
||||
class="text-[40px] lg:text-[50px] text-slate-300 whitespace-nowrap font-semibold"
|
||||
>
|
||||
TEST {{ i }}
|
||||
</span>
|
||||
<template v-for="i in 1">
|
||||
<NuxtImg src="/img/brands/brand-1.png" class="h-[45px] grayscale opacity-40" />
|
||||
<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" />
|
||||
<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>
|
||||
<template v-for="i in 1">
|
||||
<NuxtImg src="/img/brands/brand-1.png" class="h-[45px] grayscale opacity-40" />
|
||||
<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" />
|
||||
<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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
// types
|
||||
type Props = {
|
||||
variant?: "solid" | "secondary" | "outlined" | "ghost";
|
||||
variant?: "solid" | "secondary" | "outlined" | "ghost" | "primary";
|
||||
size?: "xl" | "lg" | "md";
|
||||
startIcon?: string;
|
||||
endIcon?: string;
|
||||
@@ -24,6 +24,7 @@ const classes = computed(() => {
|
||||
"btn-secondary": variant.value === "secondary",
|
||||
"btn-outlined": variant.value === "outlined",
|
||||
"btn-ghost": variant.value === "ghost",
|
||||
"btn-primary": variant.value === "primary",
|
||||
},
|
||||
{
|
||||
"btn-xl": size.value === "xl",
|
||||
|
||||
@@ -26,7 +26,7 @@ withDefaults(defineProps<Props>(), {
|
||||
</span>
|
||||
<NuxtLink to="/products">
|
||||
<Button
|
||||
variant="outlined"
|
||||
variant="primary"
|
||||
class="rounded-full max-sm:typo-label-sm max-sm:py-2"
|
||||
end-icon="ci:arrow-left"
|
||||
>
|
||||
|
||||
@@ -25,28 +25,27 @@ const onSwiper = (swiper: SwiperClass) => {
|
||||
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">-->
|
||||
<!-- <span class="text-white typo-h-6 md:typo-h-5 lg:typo-h-4">-->
|
||||
<!-- دسته بندی ها-->
|
||||
<!-- </span>-->
|
||||
<!-- </div>-->
|
||||
<div class="w-full relative translate-y-[-200px] z-10">
|
||||
<div class="flex-col-center gap-6">
|
||||
<span class="text-white typo-h-6 md:typo-h-5 lg:typo-h-4">
|
||||
دسته بندی ها
|
||||
</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">
|
||||
<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="{
|
||||
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
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
@@ -109,7 +108,7 @@ const onSwiper = (swiper: SwiperClass) => {
|
||||
|
||||
<div class="w-full flex justify-center items-center">
|
||||
<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">
|
||||
مشاهده همه دسته ها
|
||||
</Button>
|
||||
|
||||
@@ -168,7 +168,7 @@ onUnmounted(() => {
|
||||
:centered-slides="true"
|
||||
:breakpoints="{
|
||||
768: {
|
||||
spaceBetween : 40,
|
||||
spaceBetween : 40
|
||||
}
|
||||
}"
|
||||
@swiper="onSwiper"
|
||||
@@ -234,7 +234,8 @@ onUnmounted(() => {
|
||||
</span>
|
||||
<NuxtLink :to="slide.link">
|
||||
<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>
|
||||
@@ -249,7 +250,7 @@ onUnmounted(() => {
|
||||
>
|
||||
<button @click="swiper_instance?.slidePrev()">
|
||||
<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"
|
||||
/>
|
||||
</button>
|
||||
@@ -264,7 +265,7 @@ onUnmounted(() => {
|
||||
<button>
|
||||
<Icon
|
||||
@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"
|
||||
/>
|
||||
</button>
|
||||
|
||||
@@ -20,7 +20,7 @@ await suspense();
|
||||
مقالات اخیر سایت
|
||||
</span>
|
||||
<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">
|
||||
نمایش همه
|
||||
</Button>
|
||||
|
||||
@@ -15,6 +15,13 @@ const activeSlideVideo = ref<"left" | "right" | "none">("none");
|
||||
const draggableEl = 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, {
|
||||
initialValue: { x: 0, y: 0 },
|
||||
axis: "x"
|
||||
@@ -22,6 +29,17 @@ const { x: dragAxisX } = useDraggable(draggableEl, {
|
||||
|
||||
// watch
|
||||
|
||||
watch(heymlzElementIsVisible, (newValue) => {
|
||||
if (newValue) {
|
||||
showHeymlzAnimation.value = true;
|
||||
setTimeout(() => {
|
||||
showHeymlzAnimation.value = false;
|
||||
}, 3200);
|
||||
}
|
||||
}, {
|
||||
once: true
|
||||
});
|
||||
|
||||
watch(() => clipPathPercent.value, (newValue) => {
|
||||
if (newValue > 80) {
|
||||
activeSlideVideo.value = "right";
|
||||
@@ -51,18 +69,19 @@ watch(
|
||||
<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-h-6 md:typo-h-5 lg:typo-h-3 text-black">
|
||||
تفاوت محصلات ما را ببینید
|
||||
</span>
|
||||
تفاوت محصلات ما را ببینید
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
ref="previewContainerEl"
|
||||
class="rounded-200 overflow-hidden h-[70svh] md:h-[80svh] relative"
|
||||
class="rounded-200 overflow-hidden h-[70svh] relative"
|
||||
>
|
||||
<Transition name="fade">
|
||||
<NuxtImg
|
||||
v-if="activeSlideVideo !== 'right'"
|
||||
: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"
|
||||
/>
|
||||
<video
|
||||
@@ -76,13 +95,14 @@ watch(
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<div class="absolute size-full right-0 w-full">
|
||||
<div class="absolute size-full right-0 w-full" ref="heymlzElement">
|
||||
|
||||
<Transition name="fade">
|
||||
<NuxtImg
|
||||
v-if="activeSlideVideo !== 'left'"
|
||||
: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"
|
||||
/>
|
||||
<video
|
||||
@@ -97,12 +117,14 @@ watch(
|
||||
</Transition>
|
||||
|
||||
<video
|
||||
v-if="showHeymlzAnimation"
|
||||
src="/video/heymlz/heymlz-pulling.webm"
|
||||
autoplay
|
||||
muted
|
||||
playsinline
|
||||
loop
|
||||
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="{
|
||||
left: `${clipPathPercent}%`,
|
||||
filter: 'drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.3))'
|
||||
@@ -113,17 +135,23 @@ watch(
|
||||
:style="{
|
||||
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
|
||||
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
|
||||
name="ci:arrows"
|
||||
size="24"
|
||||
class="**:stroke-white"
|
||||
class="transition-all"
|
||||
:class="showHeymlzAnimation ? '**:stroke-black' : '**:stroke-white'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,10 +160,13 @@ watch(
|
||||
<div
|
||||
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">
|
||||
<span class="typo-p-sm md:typo-p-md">
|
||||
{{ homeData!.difreance_section.description1 }}
|
||||
</span>
|
||||
<div
|
||||
class="flex flex-col gap-2 text-black transition-opacity"
|
||||
:class="activeSlideVideo === 'right' ? 'opacity-0' : ''"
|
||||
>
|
||||
<span class="typo-p-sm md:typo-p-md">
|
||||
{{ homeData!.difreance_section.description1 }}
|
||||
</span>
|
||||
<NuxtLink
|
||||
:to="homeData!.difreance_section.link1"
|
||||
class="typo-h-6 md:typo-h-5 lg:typo-h-3"
|
||||
@@ -143,10 +174,13 @@ watch(
|
||||
{{ homeData!.difreance_section.title1 }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 text-black">
|
||||
<span class="typo-p-sm md:typo-p-md text-end">
|
||||
{{ homeData!.difreance_section.description2 }}
|
||||
</span>
|
||||
<div
|
||||
class="flex flex-col gap-2 text-black transition-opacity"
|
||||
:class="activeSlideVideo === 'left' ? 'opacity-0' : ''"
|
||||
>
|
||||
<span class="typo-p-sm md:typo-p-md text-end">
|
||||
{{ homeData!.difreance_section.description2 }}
|
||||
</span>
|
||||
<NuxtLink
|
||||
:to="homeData!.difreance_section.link2"
|
||||
class="typo-h-6 md:typo-h-5 lg:typo-h-3 text-end"
|
||||
|
||||
@@ -92,7 +92,6 @@ onUnmounted(() => {
|
||||
:to="slide.link"
|
||||
class="showcase-slide origin-bottom absolute size-full bg-transparent flex items-center justify-center"
|
||||
>
|
||||
|
||||
<NuxtImg
|
||||
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"
|
||||
@@ -101,13 +100,22 @@ onUnmounted(() => {
|
||||
}"
|
||||
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">
|
||||
{{ slide.title }}
|
||||
</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">
|
||||
{{ slide.description }}
|
||||
</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>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
@@ -21,18 +21,18 @@ const { circle } = toRefs(props);
|
||||
<div
|
||||
:style="{
|
||||
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="{
|
||||
'rounded-full overflow-hidden': circle,
|
||||
}"
|
||||
>
|
||||
<img
|
||||
<NuxtImg
|
||||
:style="{
|
||||
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"
|
||||
alt="ai-loading"
|
||||
/>
|
||||
|
||||
@@ -20,6 +20,8 @@ const { isLoggedIn } = useAuth();
|
||||
const route = useRoute();
|
||||
const id = route.params.id as string | number;
|
||||
|
||||
const scrollToBottomTimer = ref<NodeJS.Timeout | null>(null);
|
||||
|
||||
const chatContainerEl = ref<HTMLElement | null>(null);
|
||||
|
||||
const lastMessageBeforeUpdate = ref(0);
|
||||
@@ -63,7 +65,10 @@ useInfiniteScroll(
|
||||
// methods
|
||||
|
||||
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
|
||||
@@ -137,8 +142,7 @@ whenever(
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
maskImage:
|
||||
'linear-gradient(to top, transparent, black 5%, black, black)',
|
||||
maskImage: 'linear-gradient(to top, transparent, black 5%, black, black)'
|
||||
}"
|
||||
class="hide-scrollbar flex flex-col py-7 gap-6 h-full overflow-y-auto"
|
||||
ref="chatContainerEl"
|
||||
@@ -184,11 +188,20 @@ whenever(
|
||||
</Transition>
|
||||
</template>
|
||||
<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
|
||||
>
|
||||
<img class="size-[50px]" src="/img/heymlz/heymlz-idle.gif" alt="" />
|
||||
Please sign in first
|
||||
<NuxtImg class="size-[250px]" src="/img/heymlz/heymlz-loading-1.gif" alt="" />
|
||||
<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>
|
||||
</Transition>
|
||||
|
||||
@@ -7,6 +7,9 @@ import { useToast } from "~/composables/global/useToast";
|
||||
|
||||
// state
|
||||
|
||||
const route = useRoute();
|
||||
const id = route.params.id as string | number;
|
||||
|
||||
const { $queryClient: queryClient } = useNuxtApp();
|
||||
|
||||
const { addToast } = useToast();
|
||||
@@ -27,7 +30,7 @@ const sendMessage = async () => {
|
||||
|
||||
await createMessage({
|
||||
new_message: value,
|
||||
productId: 1,
|
||||
productId: id,
|
||||
});
|
||||
} catch (e) {
|
||||
addToast({
|
||||
|
||||
@@ -54,13 +54,12 @@ onMounted(() => {
|
||||
`#chat-message-content-${id.value}`,
|
||||
{
|
||||
text: "",
|
||||
duration: 2.5,
|
||||
ease: "none",
|
||||
},
|
||||
{
|
||||
text: { value: content.value, rtl: false },
|
||||
duration: 2.5,
|
||||
ease: "none",
|
||||
duration: 2.5,
|
||||
onUpdate: () => emit("textUpdate"),
|
||||
}
|
||||
);
|
||||
@@ -78,9 +77,9 @@ onMounted(() => {
|
||||
<div
|
||||
class="relative overflow-hidden flex items-center justify-center mt-px bg-slate-300 rounded-full size-[35px] shrink-0"
|
||||
>
|
||||
<img
|
||||
v-if="!reverse"
|
||||
src="/img/footer-bg.jpg"
|
||||
<NuxtImg
|
||||
v-if="reverse"
|
||||
src="/img/heymlz/footer-share.svg"
|
||||
class="size-full object-cover absolute"
|
||||
alt="profile"
|
||||
/>
|
||||
|
||||
@@ -6,9 +6,9 @@ import { useAuth } from "~/composables/api/auth/useAuth";
|
||||
|
||||
// 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
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
@@ -26,7 +26,7 @@ const useGetBranch = (productId: string | number, enabled: Ref<boolean>) => {
|
||||
limit: number;
|
||||
offset: number;
|
||||
}) => {
|
||||
const { data } = await axios.get<GetBranchResponse>(
|
||||
const { data } = await axios.get<GetChatResponse>(
|
||||
`${API_ENDPOINTS.chat.messages}/${productId}`,
|
||||
{
|
||||
params: {
|
||||
@@ -65,4 +65,4 @@ const useGetBranch = (productId: string | number, enabled: Ref<boolean>) => {
|
||||
});
|
||||
};
|
||||
|
||||
export default useGetBranch;
|
||||
export default useGetChat;
|
||||
|
||||
@@ -15,7 +15,6 @@ const disableLoadingOverlay = useState("disableLoadingOverlay", () => false);
|
||||
const response = await suspense();
|
||||
|
||||
if (response.isError) {
|
||||
console.log(response);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: `Landing error : ${response.error.message}`
|
||||
|
||||
@@ -160,8 +160,8 @@ const resetForm = () => {
|
||||
/>
|
||||
<div class="flex items-center justify-center flex-col size-full translate-y-[-80px]">
|
||||
<video
|
||||
class="aspect-square w-[450px] translate-y-[197px] animate-fade-in"
|
||||
src="/video/heymlz/heymlz-seat-2.webm"
|
||||
class="aspect-square w-[450px] translate-y-[157px] animate-fade-in"
|
||||
src="/video/heymlz/heymlz-handshake-full.webm"
|
||||
:style="{
|
||||
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">
|
||||
<Input
|
||||
v-if="!showOtp"
|
||||
class="w-full tracking-[2px]"
|
||||
class="w-full tracking-[3px] persian-number"
|
||||
v-model="loginInfo.phone"
|
||||
placeholder="9380123456"
|
||||
placeholder="۹۳۸۰۱۲۳۴۵۶"
|
||||
dir="ltr"
|
||||
:error="formValidator$.phone.$error"
|
||||
>
|
||||
@@ -193,7 +193,7 @@ const resetForm = () => {
|
||||
size="24"
|
||||
/>
|
||||
<span class="text-slate-500 typo-label-sm">
|
||||
+98
|
||||
+۹۸
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 32 KiB |