This commit is contained in:
Mamalizz
2025-03-27 23:08:29 +03:30
47 changed files with 355 additions and 157 deletions
+2
View File
@@ -5,3 +5,5 @@ class AccountConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'account'
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='شهر')
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": 'فروشگاه هی ملز',
+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 . 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'),
+50 -12
View File
@@ -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):
@@ -231,3 +261,11 @@ class LogoutView(APIView):
return Response(status=status.HTTP_205_RESET_CONTENT)
except Exception as e:
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",
"created_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")
)
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"))
+2 -2
View File
@@ -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
+4 -3
View File
@@ -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),
),
]
+25 -27
View File
@@ -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})'
-3
View File
@@ -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 -2
View File
@@ -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'),
+22 -5
View File
@@ -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:
+7 -1
View File
@@ -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}"
+4
View File
@@ -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 = [
+4
View File
@@ -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
+1 -1
View File
@@ -35,6 +35,6 @@ const closeModal = () => {
/>
</ToastProvider>
<VueQueryDevtools dir="ltr" buttonPosition="bottom-right" />
<VueQueryDevtools dir="ltr" buttonPosition="top-right"/>
</div>
</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 {
@apply text-black bg-slate-100;
@apply btn-lg;
+40 -27
View File
@@ -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>
+2 -1
View File
@@ -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",
+1 -1
View File
@@ -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"
>
+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"
>
<!-- <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>
+5 -4
View File
@@ -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>
+1 -1
View File
@@ -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>
+52 -18
View File
@@ -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"
+10 -2
View File
@@ -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"
/>
+4 -4
View File
@@ -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;
-1
View File
@@ -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}`
+5 -5
View File
@@ -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>
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.