This commit is contained in:
Mamalizz
2025-03-13 01:41:29 +03:30
42 changed files with 701 additions and 290 deletions
+2 -4
View File
@@ -1,6 +1,6 @@
from django.db import models
from account.models import User
from product.models import ProductModel, DollorModel
from product.models import ProductModel
from django.conf import settings
import openai
from time import sleep
@@ -24,9 +24,7 @@ class ProductChatModel(models.Model):
def save(self, *args, **kwargs):
if not self.thread:
client = openai.OpenAI(api_key=settings.OPENAI_API_KEY)
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
dollor_price = dollor_object.price
product_json = DynamicProductSerializer(instance=self.product, context={'dollor_price': dollor_price, 'view_type': 'chat'}).data
product_json = DynamicProductSerializer(instance=self.product, context={'view_type': 'chat'}).data
try:
thread = client.beta.threads.create(
+3
View File
@@ -0,0 +1,3 @@
from .celery import app as celery_app
__all__ = ('celery_app',)
+8
View File
@@ -0,0 +1,8 @@
import os
from celery import Celery
from django.conf import settings
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings.production')
app = Celery('core')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
+17 -8
View File
@@ -30,7 +30,7 @@ EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD")
DEFAULT_FROM_EMAIL = os.getenv("SECRET_KEY")
# Security and Debugging
SECRET_KEY = os.getenv("SECRET_KEY") or 'this is just for cron job dont judge me'
SECRET_KEY = os.getenv("SECRET_KEY")
DEBUG = True
BASE_DIR = Path(__file__).resolve().parent.parent.parent
@@ -65,7 +65,8 @@ INSTALLED_APPS = [
"rest_framework.authtoken",
"import_export",
"django_jalali",
'django_crontab',
'django_celery_beat',
'azbankgateways',
# Custom Apps
"product",
"account",
@@ -235,11 +236,19 @@ AWS_S3_OBJECT_PARAMETERS = {
}
# ==============================================================================
# django CRONJOBS
# bank gateway configs
# ==============================================================================
CRONJOBS = [
('* * * * *', 'product.cron.update_product_prices', f'>> {BASE_DIR}/logfile.log 2>&1'),
]
AZ_IRANIAN_BANK_GATEWAYS = {
'GATEWAYS': {
'ZARINPAL': {
'MERCHANT_CODE': 'Merchant-Code',
'SANDBOX': True,
}
},
'IS_SAMPLE_FORM_ENABLE': True,
'DEFAULT_BANK': 'ZARINPAL',
'CURRENCY': 'IRR',
'TRACKING_CODE_QUERY_PARAM': 'tc',
'BANK_PRIORITIES': ['ZARINPAL'],
}
+20 -1
View File
@@ -46,4 +46,23 @@ MEDIA_URL = 'https://c262408.parspack.net/'
MEDIA_ROOT = '/app/media'
STATIC_URL = '/shop_static/'
STATIC_ROOT = '/app/static'
STATIC_ROOT = '/app/static'
# ==============================================================================
# django cerery
# ==============================================================================
CELERY_BROKER_URL = "redis://redis:6379/0"
CELERY_RESULT_BACKEND = "redis://redis:6379/0"
CELERY_TIMEZONE = "UTC"
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
'update-prices-every-minute': {
'task': 'product.tasks.update_product_prices',
'schedule': crontab(minute='*'),
},
}
+46 -1
View File
@@ -135,6 +135,11 @@ UNFOLD = {
"link": reverse_lazy("admin:product_dollormodel_changelist"),
"badge": "utils.admin.dollor_price",
},
{
"title": _("کد تخفیف"),
"icon": "payments",
"link": reverse_lazy("admin:order_discountcode_changelist"),
},
],
},
@@ -260,7 +265,47 @@ UNFOLD = {
],
},
{
"title": _("تسک های سلری"),
"collapsible": True,
"items": [
{
"title": _("Clocked"),
"icon": "hourglass_bottom",
"link": reverse_lazy(
"admin:django_celery_beat_clockedschedule_changelist"
),
},
{
"title": _("Crontabs"),
"icon": "update",
"link": reverse_lazy(
"admin:django_celery_beat_crontabschedule_changelist"
),
},
{
"title": _("Intervals"),
"icon": "arrow_range",
"link": reverse_lazy(
"admin:django_celery_beat_intervalschedule_changelist"
),
},
{
"title": _("Periodic tasks"),
"icon": "task",
"link": reverse_lazy(
"admin:django_celery_beat_periodictask_changelist"
),
},
{
"title": _("Solar events"),
"icon": "event",
"link": reverse_lazy(
"admin:django_celery_beat_solarschedule_changelist"
),
},
],
},
],
},
-1
View File
@@ -13,6 +13,5 @@ COPY . /app/
CMD ["sh", "-c", "python manage.py makemigrations && \
python manage.py migrate && \
python manage.py crontab add && \
python manage.py collectstatic --no-input && \
gunicorn core.wsgi:application --bind 0.0.0.0:8000 --workers 3"]
+59 -1
View File
@@ -92,4 +92,62 @@ class HomeImageAdmin(ModelAdmin, ImportExportModelAdmin):
ArrayField: {
"widget": ArrayWidget,
}
}
}
# admin.py
from django.contrib import admin
from unfold.admin import ModelAdmin
from django_celery_beat.models import (
ClockedSchedule,
CrontabSchedule,
IntervalSchedule,
PeriodicTask,
SolarSchedule,
)
from django_celery_beat.admin import ClockedScheduleAdmin as BaseClockedScheduleAdmin
from django_celery_beat.admin import CrontabScheduleAdmin as BaseCrontabScheduleAdmin
from django_celery_beat.admin import PeriodicTaskAdmin as BasePeriodicTaskAdmin
from django_celery_beat.admin import PeriodicTaskForm, TaskSelectWidget
from unfold.widgets import *
admin.site.unregister(PeriodicTask)
admin.site.unregister(IntervalSchedule)
admin.site.unregister(CrontabSchedule)
admin.site.unregister(SolarSchedule)
admin.site.unregister(ClockedSchedule)
class UnfoldTaskSelectWidget(UnfoldAdminSelectWidget, TaskSelectWidget):
pass
class UnfoldPeriodicTaskForm(PeriodicTaskForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["task"].widget = UnfoldAdminTextInputWidget()
self.fields["regtask"].widget = UnfoldTaskSelectWidget()
@admin.register(PeriodicTask)
class PeriodicTaskAdmin(BasePeriodicTaskAdmin, ModelAdmin):
form = UnfoldPeriodicTaskForm
@admin.register(IntervalSchedule)
class IntervalScheduleAdmin(ModelAdmin):
pass
@admin.register(CrontabSchedule)
class CrontabScheduleAdmin(BaseCrontabScheduleAdmin, ModelAdmin):
pass
@admin.register(SolarSchedule)
class SolarScheduleAdmin(ModelAdmin):
pass
@admin.register(ClockedSchedule)
class ClockedScheduleAdmin(BaseClockedScheduleAdmin, ModelAdmin):
pass
+1 -3
View File
@@ -20,8 +20,6 @@ class HomeView(APIView):
authentication_classes = []
def get(self, request):
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
dollor_price = dollor_object.price
sliders = SliderModel.objects.all()
slider_ser = SliderSerializer(instance=sliders, many=True, context={'request': request})
@@ -30,7 +28,7 @@ class HomeView(APIView):
sub_category_ser = SubCategorySerializer(instance=sub_categories, many=True, context={'request': request})
products_to_show = ProductModel.objects.filter(show=True)
product_ser = DynamicProductSerializer(instance=products_to_show, many=True, context={'request': request, 'dollor_price': dollor_price, 'view_type': 'list'})
product_ser = DynamicProductSerializer(instance=products_to_show, many=True, context={'request': request, 'view_type': 'list'})
home_image = HomeImageModel.objects.all().first()
home_image_ser = HomeImageSerializer(instance=home_image, context={'request': request})
+6
View File
@@ -19,6 +19,12 @@ class OrderItemModelInline(StackedInline):
@admin.register(DiscountCode)
class DiscountCodeAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm
export_form_class = ExportForm
list_display = ['code', 'expiration_date', 'percent', 'quantity']
@admin.register(OrderModel)
class OrderAdmin(ModelAdmin, ImportExportModelAdmin):
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-03-10 20:10
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('order', '0011_orderitemmodel_price'),
]
operations = [
migrations.RenameField(
model_name='discountcode',
old_name='name',
new_name='code',
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-03-11 18:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0012_rename_name_discountcode_code'),
]
operations = [
migrations.AddField(
model_name='ordermodel',
name='cart_total',
field=models.BigIntegerField(blank=True, null=True, verbose_name='کل سبد خرید'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-03-11 19:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0013_ordermodel_cart_total'),
]
operations = [
migrations.AlterField(
model_name='orderitemmodel',
name='price',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='قیمت'),
),
]
+28 -19
View File
@@ -6,20 +6,31 @@ from .execptions import DiscountNotAvailableError
from django_jalali.db import models as jmodels
class DiscountCode(models.Model):
name = models.CharField(max_length=50, verbose_name='کد تخفیف')
code = models.CharField(max_length=50, verbose_name='کد تخفیف')
percent = models.DecimalField(max_digits=4, decimal_places=2, verbose_name='درصد')
quantity = models.PositiveIntegerField(verbose_name='تعداد')
expiration_date = models.DateTimeField(verbose_name='تاریخ انقضا')
def __str__(self):
return self.name
return self.code
class Meta:
verbose_name = 'کد تخفیف'
verbose_name_plural = 'کد های تخفیف'
def is_valid(self):
return self.expiration_date > timezone.now() and self.quantity > 0
def not_valid_reason(self):
if self.expiration_date > timezone.now() and self.quantity > 0:
return 'این کد معتبر میباشد'
elif not self.expiration_date > timezone.now():
return 'تایم کد تخفیف تمام شده'
elif not self.quantity > 0:
return 'این کد تخفیف تمام شده است'
else:
print('log later bug')
class OrderModel(models.Model):
@@ -39,6 +50,12 @@ class OrderModel(models.Model):
is_paid = models.BooleanField(default=False, verbose_name="وضعیت پرداخت")
discount_code = models.ForeignKey(DiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف")
status = models.CharField(max_length=20, choices=STATUS_CHOICES, verbose_name="وضعیت سفارش")
discount = 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='قیمت نهایی')
cart_total = models.BigIntegerField(null=True, blank=True, verbose_name='کل سبد خرید')
def __str__(self):
return f'سفارش: {self.id}'
@@ -47,9 +64,6 @@ class OrderModel(models.Model):
verbose_name_plural = 'سفارشات'
# def total_without_tax(self):
# return sum(item.total() for item in self.items.all())
def save(self, *args, **kwargs):
try:
@@ -63,15 +77,12 @@ class OrderModel(models.Model):
super().save(*args, **kwargs)
def discount(self):
pass
# total_with_item_discount = sum(item.total_with_discount() for item in self.items.all())
# if self.discount_code:
# if not self.discount_code.is_valid():
# raise DiscountNotAvailableError('این کد تخفیف دیگر معتبر نیست')
# discount_percent = self.discount_code.percent
# return total_with_item_discount * ((100 - discount_percent) / 100)
# return total_with_item_discount
# discount_percent = self.discount_code.percent
# return total_with_item_discount * ((100 - discount_percent) / 100)
pass
def tax(self):
@@ -82,22 +93,20 @@ class OrderModel(models.Model):
pass
# return self.total_with_discount() + self.tax()
def remove_order_item(self, item_pk, quantity):
def final_price(self):
pass
def add_order_item(self, item_pk, quantity):
status = ''
return status
def clear_cart(self):
def submit_cart(self):
pass
class OrderItemModel(models.Model):
order = models.ForeignKey(OrderModel, on_delete=models.CASCADE, related_name='items', verbose_name='سفارش')
quantity = models.PositiveSmallIntegerField(verbose_name="تعداد")
price = models.PositiveIntegerField(verbose_name='قیمت')
price = models.PositiveIntegerField(verbose_name='قیمت', blank=True, null=True)
product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول")
class Meta:
verbose_name = 'ایتم سبد خرید'
+82 -6
View File
@@ -1,29 +1,105 @@
from rest_framework import serializers
from .models import OrderItemModel, OrderModel
from product.serializers import ProductVariantSerialzier
from .models import OrderItemModel, OrderModel, DiscountCode
from product.serializers import ProductVariantSerialzier, AttributeValueSerialzier, ProductImageSerailizer
from account.serializers import UserAddressSerializer
from product.models import ProductVariant
class ProductVariantSerialzier(serializers.ModelSerializer):
product_attributes = AttributeValueSerialzier(many=True)
image = serializers.SerializerMethodField()
discount_amount = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
price = serializers.SerializerMethodField()
final_price = serializers.SerializerMethodField()
category = serializers.SerializerMethodField()
class Meta:
model = ProductVariant
fields = ['title', 'product_attributes', 'in_stock', 'price', 'discount', 'color', 'image', 'discount_amount', 'category', 'final_price']
def get_discount_amount(self, obj):
discount_amount = int(obj.price * (obj.discount / 100))
return f'{discount_amount:,.0f} تومان'
def get_final_price(self, obj):
final_price = obj.price - int(obj.price * (obj.discount / 100))
return f'{final_price:,.0f} تومان'
def get_price(self, obj):
return f'{obj.price:,.0f} تومان'
def get_image(self, obj):
return self.context.get('request').build_absolute_uri(obj.images.all().first().image.url)
def get_title(self, obj):
return obj.product.name
def get_category(self, obj):
return obj.product.category.name
class DiscountCodeSerializer(serializers.ModelSerializer):
class Meta:
model = DiscountCode
fields = ('code', 'percent')
read_only_fields = ('percent',)
class OrderItemSerailzier(serializers.ModelSerializer):
product = serializers.SerializerMethodField()
class Meta:
model = OrderItemModel
fields = "__all__"
read_only_fields = ('order', 'product')
exclude = ('price', 'order')
read_only_fields = ('order', 'product', )
def get_product(self, obj):
return ProductVariantSerialzier(instance=obj.product, context={'request': self.context.get('request')}).data
class CartSerializer(serializers.ModelSerializer):
items = OrderItemSerailzier(many=True)
cart_total = serializers.SerializerMethodField()
tax = serializers.SerializerMethodField()
final_price = serializers.SerializerMethodField()
discount_code = serializers.SerializerMethodField()
class Meta:
model = OrderModel
fields = ['address', 'created_at', 'is_paid', 'status', 'discount_code', 'items']
fields = [ 'discount_code', 'items', 'cart_total', 'tax', 'final_price']
def get_discount_code(self, obj):
if obj.discount_code:
return {
'code': f'{obj.discount_code.code}',
'percent': obj.discount_code.percent,
'amount': f'{10000:,.0f} تومان'
}
else:
return None
def get_tax(self, obj):
return f'{1000:,.0f} تومان'
def get_cart_total(self, obj):
return f'{10000:,.0f} تومان'
def get_final_price(self, obj):
return f'{8000:,.0f} تومان'
class OrderSerializer(serializers.ModelSerializer):
count = serializers.SerializerMethodField()
images = serializers.SerializerMethodField()
address = UserAddressSerializer()
items = OrderItemSerailzier(many=True)
class Meta:
model = OrderModel
fields = ['address', 'created_at', 'is_paid', 'status', 'discount_code', "images", "count", "id"]
fields = ['address', 'created_at', 'items', 'status', 'discount_code', "images", "count", "id"]
def get_count(self, obj):
return obj.items.all().count()
+4 -2
View File
@@ -1,11 +1,13 @@
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
from .views import CartItemViews, CartView, OrderlistView
from .views import CartItemViews, CartView, OrderlistView, CartItemClear, ApplyDiscountView
urlpatterns = [
path('list', OrderlistView.as_view(), name='order-list'),
path('all', OrderlistView.as_view(), name='order-list'),
path('cart', CartView.as_view()),
path('cart/discount', ApplyDiscountView.as_view()),
path('cart/all', CartItemClear.as_view()),
path('cart/item/<int:pk>', CartItemViews.as_view(), name='change-item-cart'),
# path('payment', CartView.as_view()),
# path('', CartView.as_view()),
+93 -2
View File
@@ -7,7 +7,7 @@ from rest_framework.permissions import IsAuthenticated
from .serializers import *
# from cart.models import
from rest_framework import status
from .models import OrderItemModel, OrderModel
from .models import OrderItemModel, OrderModel, DiscountCode
try:
pass
except DiscountNotAvailableError:
@@ -24,6 +24,36 @@ pay
class ApplyDiscountView(APIView):
serializer_class = DiscountCodeSerializer
permission_classes = [IsAuthenticated]
def post(self, request):
cart_order, created = OrderModel.objects.get_or_create(
user=request.user,
status='CART'
)
discount_code = get_object_or_404(DiscountCode, code=request.data.get('code'))
if not discount_code.is_valid():
return Response({'detail': discount_code.not_valid_reason()}, status=status.HTTP_400_BAD_REQUEST)
cart_order.discount_code = discount_code
cart_order.save()
return Response({'detail': 'کد تخفیف با موفقیت اعمال شد'}, status=status.HTTP_200_OK)
class CartItemClear(APIView):
permission_classes = [IsAuthenticated]
serializer_class = OrderItemSerailzier
def delete(self, request):
cart_order, created = OrderModel.objects.get_or_create(
user=request.user,
status='CART'
)
cart_order.items.all().delete()
return Response({'detail': f'سبد خرید با موفقیت خالی شد'}, status=status.HTTP_204_NO_CONTENT)
class CartItemViews(APIView):
permission_classes = [IsAuthenticated]
serializer_class = OrderItemSerailzier
@@ -77,4 +107,65 @@ class OrderlistView(APIView):
user = request.user
orders = OrderModel.objects.filter(user=user).exclude(status="CART")
orders_ser = self.serializer_class(instance=orders, many=True, context={'request': request})
return Response(orders_ser.data, status=status.HTTP_200_OK)
return Response(orders_ser.data, status=status.HTTP_200_OK)
# from rest_framework.views import APIView
# from rest_framework.response import Response
# from rest_framework import status
# from azbankgateways import bankfactories, models as bank_models
# class PaymentView(APIView):
# def post(self, request):
# amount = request.data.get('amount')
# user = request.user
# payment = Payment.objects.create(amount=amount, bank_type='ZARINPAL')
# factory = bankfactories.ZarinpalBankFactory()
# try:
# bank = factory.create(
# amount=amount,
# user=user,
# callback_url='http://.com/callback/',
# reference_model=payment,
# )
# bank.ready()
# return Response({'gateway_url': bank.redirect_url}, status=status.HTTP_200_OK)
# except Exception as e:
# return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
# from django.views.decorators.csrf import csrf_exempt
# from rest_framework.decorators import api_view
# from rest_framework.response import Response
# from azbankgateways import bankfactories, models as bank_models
# @csrf_exempt
# @api_view(['POST'])
# def callback_view(request):
# tracking_code = request.POST.get('tracking_code')
# payment_id = request.POST.get('payment_id')
# payment = Payment.objects.get(id=payment_id)
# bank_type = payment.bank_type
# factory = bankfactories.BankFactory.get_bank(bank_type)
# try:
# result = factory.verify_transaction(tracking_code)
# if result.is_success:
# payment.status = 'Paid'
# payment.tracking_code = tracking_code
# payment.save()
# return Response({'status': 'Payment successful'})
# else:
# payment.status = 'Failed'
# payment.save()
# return Response({'status': 'Payment failed'})
# except Exception as e:
# return Response({'error': str(e)})
-5
View File
@@ -1,5 +0,0 @@
from product.models import ProductVariant
def update_product_prices():
print('calling the update product prices from cron')
ProductVariant.update_all_prices()
+8
View File
@@ -0,0 +1,8 @@
from celery import shared_task
from product.models import ProductVariant
@shared_task
def update_product_prices():
print("\033[92m Calling update product prices from Celery\033[00m")
ProductVariant.update_all_prices()
print("\033[92m its working\033[00m")
+10 -13
View File
@@ -54,9 +54,7 @@ class ProductView(APIView):
authentication_classes = []
def get(self, request, pk):
product = get_object_or_404(ProductModel, id=pk)
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
dollor_price = dollor_object.price
product_ser = self.serializer_class(instance=product, many=False, context={'dollor_price': dollor_price, 'request': request, 'view_type': 'instance'})
product_ser = self.serializer_class(instance=product, many=False, context={'request': request, 'view_type': 'instance'})
return Response(product_ser.data, status=status.HTTP_200_OK)
@@ -103,6 +101,7 @@ class AllProductsView(APIView):
"Sort results by one of the following fields:\n"
"`name`, `-name`, `price`, `-price`, `created_at`, `-created_at`."
"\nPrefix with `-` for descending order."
"remove the price form sorting templory "
),
required=False,
type=OpenApiTypes.STR,
@@ -171,17 +170,17 @@ class AllProductsView(APIView):
products = products.filter(Q(name__icontains=search_query) | Q(description__icontains=search_query))
# # Price filters
# price_gte = request.query_params.get('price_gte', None)
# price_lte = request.query_params.get('price_lte', None)
price_gte = request.query_params.get('price_gte', None)
price_lte = request.query_params.get('price_lte', None)
# if price_gte:
# products = products.filter(variants__min_price__gte=price_gte)
# if price_lte:
# products = products.filter(variants__min_price__lte=price_lte)
if price_gte:
products = products.filter(variants__price__gte=price_gte)
if price_lte:
products = products.filter(variants__price__lte=price_lte)
# Sorting
sort_by = request.query_params.get('sort', None)
if sort_by in ['name', '-name', 'price', '-price', 'created_at', '-created_at']:
if sort_by in ['name', '-name', 'created_at', '-created_at']:
products = products.order_by(sort_by)
else:
products = products.order_by('name')
@@ -189,9 +188,7 @@ class AllProductsView(APIView):
# Pagination
paginator = self.pagination_class()
paginated_products = paginator.paginate_queryset(products, request)
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
dollor_price = dollor_object.price
serializer = self.serializer_class(paginated_products, many=True, context={'dollor_price': dollor_price, 'request': request, 'view_type': 'list'})
serializer = self.serializer_class(paginated_products, many=True, context={'request': request, 'view_type': 'list'})
return paginator.get_paginated_response(serializer.data)
except MainCategoryModel.DoesNotExist:
+16 -2
View File
@@ -1,19 +1,27 @@
aiohappyeyeballs==2.4.0
aiohttp==3.10.5
aiosignal==1.3.1
amqp==5.3.1
annotated-types==0.7.0
anyio==4.6.0
asgiref==3.8.1
attrs==24.2.0
az-iranian-bank-gateways==2.0.5
beautifulsoup4==4.12.3
billiard==4.2.1
boto3==1.36.26
botocore==1.36.26
branca==0.8.1
celery==5.4.0
certifi==2024.8.30
cffi==1.17.1
charset-normalizer==3.3.2
click==8.1.8
click-didyoumean==0.3.1
click-plugins==1.1.1
click-repl==0.3.0
colorama==0.4.6
cron-descriptor==1.4.5
cryptography==44.0.1
defusedxml==0.8.0rc2
diff-match-patch==20230430
@@ -21,11 +29,10 @@ distro==1.9.0
Django==5.1.2
django-admin-interface==0.28.5
django-admin-persian-fonts==0.2
django-celery-beat==2.7.0
django-cleanup==8.1.0
django-colorfield==0.11.0
django-cors-headers==4.4.0
django-cron==0.6.0
django-crontab==0.7.1
django-dbbackup==4.2.1
django-dirtyfields==1.9.3
django-filter==24.3
@@ -33,6 +40,7 @@ django-import-export==4.1.1
django-iranian-cities==1.0.2
django-jalali==7.3.0
django-storages==1.14.5
django-timezone-field==7.1
django-unfold==0.48.0
djangorestframework==3.15.2
djangorestframework-simplejwt==5.3.1
@@ -65,6 +73,7 @@ jiter==0.8.2
jmespath==1.0.1
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
kombu==5.4.2
lxml==5.2.2
MarkupPy==1.14
MarkupSafe==3.0.2
@@ -78,6 +87,7 @@ openpyxl==3.1.2
packaging==24.2
pillow==10.4.0
platformdirs==4.2.2
prompt_toolkit==3.0.50
propcache==0.2.0
psutil==6.0.0
psycopg2-binary==2.9.10
@@ -88,6 +98,7 @@ pydantic==2.10.6
pydantic_core==2.27.2
PyJWT==2.10.1
pyTelegramBotAPI==4.23.0
python-crontab==3.2.0
python-dateutil==2.9.0.post0
python-decouple==3.8
python-dotenv==1.0.1
@@ -97,6 +108,7 @@ python3-openid==3.2.0
pytz==2024.2
pywebpush==2.0.3
PyYAML==6.0.2
redis==5.2.1
referencing==0.35.1
requests==2.32.3
requests-file==2.1.0
@@ -119,6 +131,8 @@ typing_extensions==4.12.2
tzdata==2024.1
uritemplate==4.1.1
urllib3==2.2.3
vine==5.1.0
wcwidth==0.2.13
whitenoise==6.7.0
xlrd==2.0.1
xlwt==1.3.0
+46 -8
View File
@@ -1,5 +1,6 @@
services:
frontend:
container_name: shop_frontend
build:
context: ./frontend
ports:
@@ -10,6 +11,7 @@ services:
- default
django:
container_name: shop_backend
build:
context: ./backend
ports:
@@ -18,18 +20,13 @@ services:
- db
volumes:
- ./backend:/app
- /root/vol/shop/media:/app/media
- /root/vol/shop/static:/app/static
command:
[
"sh",
"-c",
"python manage.py migrate && python manage.py collectstatic --no-input && python manage.py crontab add && gunicorn core.wsgi:application --bind 0.0.0.0:8000 --workers 3",
]
- media_data:/app/media
- media_data:/app/static
networks:
- default
db:
container_name: shop_db
image: postgres:16
environment:
POSTGRES_DB: hshop
@@ -43,6 +40,7 @@ services:
- default
db-backup:
container_name: shop_backup
build:
context: ./backup
depends_on:
@@ -61,6 +59,46 @@ services:
networks:
- default
redis:
container_name: hshop_redis
image: redis:alpine
ports:
- "6379:6379"
networks:
- default
celery_worker:
container_name: shop_celery_worker
build:
context: ./backend
command: celery -A core worker --loglevel=info
depends_on:
- django
- redis
volumes:
- ./backend:/app
environment:
- CELERY_BROKER_URL=redis://redis:6379/0
networks:
- default
celery_beat:
container_name: shop_celery_beat
build:
context: ./backend
command: celery -A core beat --loglevel=info
depends_on:
- django
- redis
volumes:
- ./backend:/app
environment:
- CELERY_BROKER_URL=redis://redis:6379/0
networks:
- default
volumes:
postgres_data:
media_data:
+5 -5
View File
@@ -19,11 +19,11 @@ const closeModal = () => {
<NuxtPwaManifest />
<UpdatePwaModal
:isShow="updateAvailable"
@update="handleUpdate"
@close="closeModal"
/>
<!-- <UpdatePwaModal-->
<!-- :isShow="updateAvailable"-->
<!-- @update="handleUpdate"-->
<!-- @close="closeModal"-->
<!-- />-->
<NuxtLayout>
<ToastProvider>
@@ -11,10 +11,14 @@ type Props = {
const props = defineProps<Props>();
const { articles } = toRefs(props);
// state
const isMobile = useMediaQuery('(max-width: 1024px)');
</script>
<template>
<div class="columns-2 gap-8 w-full space-y-8">
<div class="columns-1 xs:columns-2 xl:columns-3 gap-6 sm:gap-8 w-full space-y-8">
<BlogPost
v-for="article in articles"
:key="article.id"
@@ -24,6 +28,7 @@ const { articles } = toRefs(props);
:comments="2"
:id="article.id"
:date="article.created_at"
:variant="isMobile ? 'sm' : 'lg'"
tag="تگ ندارد"
/>
</div>
+25 -31
View File
@@ -14,33 +14,33 @@ type Props = {
// props
const props = withDefaults(defineProps<Props>(), {
withDefaults(defineProps<Props>(), {
variant: "lg"
});
const {} = toRefs(props);
</script>
<template>
<NuxtLink :to="`/article/${id}`" class="block">
<div
<div>
<NuxtLink
:to="`/article/${id}`"
:class="variant === 'lg' ? 'aspect-square rounded-150 overflow-hidden' : 'h-fit'"
class="group w-full relative"
class="group w-full relative block"
>
<Tag
v-if="variant === 'lg'"
class="bg-success-500 absolute left-6 lg:left-10 top-6 lg:top-10 z-20"
class="bg-success-500 absolute left-6 top-6 z-20"
>
اسپیکر
</Tag>
<div
v-if="variant === 'sm'"
class="h-[350px] rounded-150 overflow-hidden relative"
class="aspect-square w-full rounded-150 overflow-hidden relative"
>
<Tag
class="bg-success-500 absolute z-20 left-6 top-6"
class="bg-success-500 absolute z-20 left-4 sm:left-6 top-4 sm:top-6 max-sm:text-xs"
>
اسپیکر
</Tag>
@@ -53,51 +53,45 @@ const {} = toRefs(props);
</div>
<div
:class="variant === 'lg' ? 'absolute px-6 lg:px-10' : 'invert mt-8'"
class="bottom-6 lg:bottom-10 flex flex-col gap-4 lg:gap-6 z-20"
:class="variant === 'lg' ? 'absolute px-6' : 'invert mt-6'"
class="bottom-6 lg:bottom-8 flex flex-col gap-4 z-20"
>
<div class="flex items-center gap-4 lg:gap-6">
<div class="flex items-center gap-2">
<Icon
name="ci:comment"
size="18"
class="**:stroke-white"
class="**:stroke-white size-3 md:size-3.5"
/>
<span class="typo-p-sm text-white">
<span class="typo-p-xs md:typo-p-sm text-white">
۰ نظر
</span>
</div>
<div class="flex items-center gap-2">
<Icon
name="ci:calendar"
size="18"
class="**:stroke-white"
class="**:stroke-white size-3 md:size-3.5"
/>
<span class="typo-p-sm text-white">
۳۱ مهر ۱۴۰۳
</span>
<span class="typo-p-xs md:typo-p-sm text-white">
۳۱ مهر ۱۴۰۳
</span>
</div>
</div>
<div class="flex gap-4 flex-col">
<span
:class="variant === 'lg' ? 'line-clamp-2' : ''"
class="text-lg font-medium lg:typo-h-6 text-white"
class="text-base md:text-lg font-medium lg:typo-h-6 text-white"
>
{{ title }}
</span>
<!-- <p-->
<!-- :class="variant === 'lg' ? 'typo-h-4' : 'typo-h-6 text-slate-500'"-->
<!-- class="text-white text-justify"-->
<!-- v-html="description"-->
<!-- />-->
<p
v-if="variant === 'sm'"
class="text-white typo-p-xs max-sm:!leading-[175%] sm:typo-p-sm md:typo-p-md line-clamp-3"
>
تا با نرم افزارها شناخت بیشتری را برای طراحان رایانه ای علی الخصوص طراحان خلاقی، و فرهنگ پیشرو در زبان فارسی ایجاد کرد، در این صورت می توان امید داشت که تمام و دشواری موجود در ارائه راهکارها
</p>
</div>
<NuxtLink :to="`/article/${id}`" class="underline text-white typo-p-md">
بیشتر بخوانید...
</NuxtLink>
</div>
<img
@@ -111,6 +105,6 @@ const {} = toRefs(props);
v-if="variant === 'lg'"
class="w-full h-full bg-linear-to-t from-black to-transparent absolute inset-0 z-15"
/>
</div>
</NuxtLink>
</NuxtLink>
</div>
</template>
+5 -5
View File
@@ -12,20 +12,20 @@ const {} = toRefs(props);
</script>
<template>
<div class="relative w-full flex flex-col justify-center min-h-[700px] h-[80svh]">
<div class="relative w-full flex flex-col justify-center min-h-[450px] md:h-[80svh]">
<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"
>
<span
v-for="i in 10"
class="text-[20px] sm:text-[30px] lg:text-[50px] text-white whitespace-nowrap font-semibold"
class="text-[40px] lg:text-[50px] text-white whitespace-nowrap font-semibold"
>
TEST {{ i }}
</span>
<span
v-for="i in 10"
class="text-[20px] sm:text-[30px] lg:text-[50px] text-white whitespace-nowrap font-semibold"
class="text-[40px] lg:text-[50px] text-white whitespace-nowrap font-semibold"
>
TEST {{ i }}
</span>
@@ -38,13 +38,13 @@ const {} = toRefs(props);
>
<span
v-for="i in 10"
class="text-[20px] sm:text-[30px] lg:text-[50px] text-slate-300 whitespace-nowrap font-semibold"
class="text-[40px] lg:text-[50px] text-slate-300 whitespace-nowrap font-semibold"
>
TEST {{ i }}
</span>
<span
v-for="i in 10"
class="text-[20px] sm:text-[30px] lg:text-[50px] text-slate-300 whitespace-nowrap font-semibold"
class="text-[40px] lg:text-[50px] text-slate-300 whitespace-nowrap font-semibold"
>
TEST {{ i }}
</span>
+69 -94
View File
@@ -1,99 +1,34 @@
<script setup lang="ts">
// imports
import { NAV_LINKS } from "~/constants";
</script>
<template>
<footer class="w-full bg-black flex-center">
<div class="size-full grid grid-cols-2 items-stretch container">
<div
class="h-full flex flex-col items-start justify-between pe-[5rem] py-[5rem]"
>
<div class="flex flex-col items-start gap-[1.5rem] w-full">
<span class="text-white typo-sub-h-xl font-light"
>از آخرین اخبار، نوشتهها، مقالات و تخفیفها با خبر شوید
😎
</span>
<div class="flex flex-col items-start gap-[.75rem] w-full">
<div
class="flex items-center justify-start gap-[.5rem] w-full"
>
<Input
placeholder="آدرس الکترونیکی شما"
class="bg-slate-950 border-slate-800 hover:border-slate-800 w-8/12"
/>
<Button
class="invert rounded-100 size-[3rem] !**:stroke-black"
end-icon="ci:arrow-left"
/>
</div>
<span class="text-slate-400 typo-p-sm">
با عضویت, شما با
<NuxtLink to="#" class="text-cyan-500 underline"
>قوانین و مقررات</NuxtLink
>
سایت موافقت می کنید.
</span>
</div>
</div>
<span class="text-white typo-label-sm font-light">
© ۲۰۲۵ - ملز; هوشمند ترین وبسایت ایران
<div class="bg-black relative overflow-hidden">
<img src="/img/footer-bg.jpg" alt="" class="absolute z-10 object-cover opacity-45" />
<div class="flex flex-col gap-4 items-center justify-center relative z-20">
<div class="flex items-center flex-col pb-[10px] pt-[80px] lg:py-[150px] justify-center">
<video
src="/video/loading.mp4"
autoplay
muted
loop
class="size-[150px] lg:size-[220px] rounded-full"
/>
<span class="font-bold text-2xl lg:text-5xl text-gradient bg-gradient-to-l from-blue-500 to-blue-700">
فروشگاه هیملز
</span>
</div>
<div
class="flex flex-col items-start ps-[5rem] py-[5rem] gap-[6.875rem]"
>
<div class="w-full flex items-start gap-[3rem]">
<div class="flex flex-col gap-[1.5rem] w-full">
<NuxtLink
v-for="(link, index) in NAV_LINKS"
:key="index"
:to="link.path"
class="typo-h-5 font-light text-white hover:text-white/70 transition-all"
>
{{ link.title }}
</NuxtLink>
</div>
<div class="flex flex-col gap-[.75rem] w-full">
<NuxtLink
to="#"
class="typo-label-md font-light text-slate-400 hover:text-slate-500 transition-all"
>
سوالات متدوال
</NuxtLink>
<NuxtLink
to="#"
class="typo-label-md font-light text-slate-400 hover:text-slate-500 transition-all"
>
قوانین و مقررات
</NuxtLink>
<NuxtLink
to="#"
class="typo-label-md font-light text-slate-400 hover:text-slate-500 transition-all"
>
گزارش خطا و باگ
</NuxtLink>
<NuxtLink
to="#"
class="typo-label-md font-light text-slate-400 hover:text-slate-500 transition-all"
>
حریم خصوصی
</NuxtLink>
<NuxtLink
to="#"
class="typo-label-md font-light text-slate-400 hover:text-slate-500 transition-all"
>
تماس با ما
</NuxtLink>
</div>
</div>
<footer class="w-full flex max-lg:flex-col justify-between py-[100px] max-lg:gap-16 container items-center lg:items-start relative z-20">
<div class="flex flex-col gap-4 text-white max-w-[300px]">
<div class="flex items-center justify-end text-white w-full">
<div class="flex items-center gap-[1rem]">
<h3 class="font-bold text-xl xl:text-3xl max-lg:text-center">
با ما در ارتباط باشید...
</h3>
<p class="text-md font-thin leading-[175%] mt-4 max-lg:text-center">
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها
</p>
<div class="flex items-center gap-4 mt-6 max-lg:justify-center">
<NuxtLink to="#" class="flex-center size-[1.5rem]">
<Icon
name="ci:instagram"
@@ -124,7 +59,47 @@ import { NAV_LINKS } from "~/constants";
</NuxtLink>
</div>
</div>
</div>
<div class="flex justify-center lg:justify-end flex-1">
<div class="flex flex-col gap-6 text-white max-lg:text-center">
<h3 class="font-bold">
لینک های مفید
</h3>
<ul class="flex flex-col gap-4 font-thin">
<li>از طراحان گرافیک است</li>
<li>تولید نامفهوم</li>
<li>ستون و سطرآنچنان که لازم</li>
<li>روزنامه و مجله در ستون</li>
</ul>
</div>
</div>
<div class="flex justify-end flex-1">
<div class="flex flex-col gap-6 text-white max-lg:text-center">
<h3 class="font-bold">
لینک های مفید
</h3>
<ul class="flex flex-col gap-4 font-thin">
<li>از طراحان گرافیک است</li>
<li>تولید نامفهوم</li>
<li>ستون و سطرآنچنان که لازم</li>
<li>روزنامه و مجله در ستون</li>
</ul>
</div>
</div>
<div class="flex justify-end flex-1">
<div class="flex flex-col gap-6 text-white max-lg:text-center">
<h3 class="font-bold w-full">
لینک های مفید
</h3>
<ul class="flex flex-col gap-4 font-thin">
<li>از طراحان گرافیک است</li>
<li>تولید نامفهوم</li>
<li>ستون و سطرآنچنان که لازم</li>
<li>روزنامه و مجله در ستون</li>
</ul>
</div>
</div>
</footer>
</div>
</footer>
</template>
</div>
</template>
@@ -33,7 +33,7 @@ const formattedDate = useDateFormat(date.value, "YYYY/MM/DD");
{{ formattedDate }}
</span>
</div>
<Rating />
<Rating :rate="2"/>
</div>
<div class="typo-p-md">
{{ content }}
+1 -1
View File
@@ -22,7 +22,7 @@ const onSwiper = (swiper: SwiperClass) => {
<template>
<section
ref="sectionTarget"
class="flex flex-col justify-center gap-4 bg-black h-[150svh] mt-40 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">
+1 -1
View File
@@ -159,7 +159,7 @@ onUnmounted(() => {
<template>
<div
id="header-slider-container"
class="w-full mb-20 z-50 max-md:mt-[80px]"
class="w-full z-50"
>
<div id="header-slider-wrapper" class="relative">
<Swiper
+4 -3
View File
@@ -15,16 +15,17 @@ await suspense();
<template>
<section class="mt-20 container">
<div class="flex items-center justify-between mb-20">
<div class="flex items-center justify-between mb-12 md:mb-20">
<span class="typo-h-6 max-sm:text-xl md:typo-h-5 lg:typo-h-4 text-black">
مقالات اخیر سایت
</span>
<NuxtLink to="/articles">
<Button variant="outlined" class="rounded-full max-sm:typo-label-sm max-sm:py-2" end-icon="ci:arrow-left">
<Button variant="outlined" class="rounded-full max-sm:typo-label-sm max-sm:py-2"
end-icon="ci:arrow-left">
نمایش همه
</Button>
</NuxtLink>
</div>
<ArticlesList :articles="articles!.results" />
<ArticlesList :articles="[...articles!.results,...articles!.results,...articles!.results,...articles!.results,...articles!.results,...articles!.results]" />
</section>
</template>
@@ -51,7 +51,7 @@ const onSwiper = (swiper: SwiperClass) => {
<div class="flex justify-center items-center">
<div class="max-w-[900px] px-4 text-white flex flex-col items-center gap-4">
<Icon name="ci:instagram" size="28" class="**:stroke-white" />
<p class="text-base xs:text-lg sm:typo-h-6 lg:typo-h-5 !font-medium leading-[150%] lg:leading-[175%] max-sm:px-4 sm:max-w-[600px] lg:max-w-[800px] text-center">
<p class="text-base xs:text-lg sm:typo-h-6 lg:typo-h-5 !font-normal !leading-[150%] lg:leading-[175%] max-sm:px-4 sm:max-w-[600px] lg:max-w-[800px] text-center">
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با
استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه و مجله
در ستون و سطرآنچنان که لازم.
+66 -61
View File
@@ -47,94 +47,99 @@ watch(
<template>
<div class="container">
<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">
<div>
<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>
</div>
<div
ref="previewContainerEl"
class="rounded-200 overflow-hidden h-[70svh] md:h-[80svh] relative"
>
<Transition name="fade">
<img
v-if="activeSlideVideo !== 'right'"
:src="homeData!.difreance_section.image1"
class="select-none absolute size-full object-cover brightness-[95%]"
:alt="homeData!.difreance_section.title1"
/>
<video
v-else
autoplay
muted
src="/video/vid-3.mp4"
class="select-none absolute size-full object-cover brightness-[95%]"
/>
</Transition>
<div class="absolute size-full right-0 w-full">
</div>
<div
ref="previewContainerEl"
class="rounded-200 overflow-hidden h-[70svh] md:h-[80svh] relative"
>
<Transition name="fade">
<img
v-if="activeSlideVideo !== 'left'"
:src="homeData!.difreance_section.image2"
class="overlay-image select-none absolute object-cover size-full brightness-[95%]"
:alt="homeData!.difreance_section.title2"
v-if="activeSlideVideo !== 'right'"
:src="homeData!.difreance_section.image1"
class="select-none absolute size-full object-cover brightness-[95%]"
:alt="homeData!.difreance_section.title1"
/>
<video
v-else
autoplay
muted
src="/video/vid-3.mp4"
class="overlay-image select-none absolute object-cover size-full brightness-[95%]"
class="select-none absolute size-full object-cover brightness-[95%]"
/>
</Transition>
<div
:style="{
<div class="absolute size-full right-0 w-full">
<Transition name="fade">
<img
v-if="activeSlideVideo !== 'left'"
:src="homeData!.difreance_section.image2"
class="overlay-image select-none absolute object-cover size-full brightness-[95%]"
:alt="homeData!.difreance_section.title2"
/>
<video
v-else
autoplay
muted
src="/video/vid-3.mp4"
class="overlay-image select-none absolute object-cover size-full brightness-[95%]"
/>
</Transition>
<div
:style="{
left: `${clipPathPercent}%`,
}"
class="select-none w-2 h-full bg-black absolute left-0 flex items-center justify-center"
>
<div
ref="draggableEl"
class="cursor-grab hover:scale-115 transition-transform rounded-full absolute bg-black size-11 flex items-center justify-center"
class="select-none w-2 h-full bg-black absolute left-0 flex items-center justify-center"
>
<Icon
name="ci:arrows"
size="24"
class="**:stroke-white"
/>
<div
ref="draggableEl"
class="cursor-grab hover:scale-115 transition-transform rounded-full absolute bg-black size-11 flex items-center justify-center"
>
<Icon
name="ci:arrows"
size="24"
class="**:stroke-white"
/>
</div>
</div>
</div>
</div>
<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">
<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>
<NuxtLink
:to="homeData!.difreance_section.link1"
class="typo-h-6 md:typo-h-5 lg:typo-h-3"
>
{{ homeData!.difreance_section.title1 }}
</NuxtLink>
</div>
<div class="flex flex-col gap-2 text-black">
<NuxtLink
:to="homeData!.difreance_section.link1"
class="typo-h-6 md:typo-h-5 lg:typo-h-3"
>
{{ 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>
<NuxtLink
:to="homeData!.difreance_section.link2"
class="typo-h-6 md:typo-h-5 lg:typo-h-3 text-end"
>
{{ homeData!.difreance_section.title2 }}
</NuxtLink>
<NuxtLink
:to="homeData!.difreance_section.link2"
class="typo-h-6 md:typo-h-5 lg:typo-h-3 text-end"
>
{{ homeData!.difreance_section.title2 }}
</NuxtLink>
</div>
</div>
</div>
</div>
<div class="h-80"></div>
</div>
</template>
@@ -94,7 +94,7 @@ onUnmounted(() => {
<template>
<div
id="products-showcase-container"
class="mb-40 perspective-midrange w-full h-[125svh] bg-black flex items-center justify-center"
class="perspective-midrange w-full h-[125svh] bg-black flex items-center justify-center"
>
<NuxtLink
v-for="slide in homeData!.show_case_slider"
@@ -42,7 +42,7 @@ const submitComment = async () => {
>
<h3 class="typo-h-3">نظرات کاربران</h3>
<div class="flex flex-col gap-2">
<Rating />
<Rating :rate="2" />
<span class="typo-p-sm">
بر اساس {{ comments?.count }} نظر
</span>
+1 -1
View File
@@ -103,7 +103,7 @@ watch(
درصد تخفیف
</div>
</div>
<Rating />
<Rating :rate="3" />
</div>
<div
+10 -6
View File
@@ -25,14 +25,18 @@ if (response.isError) {
<template>
<div class="w-full">
<LoadingOverlay />
<Hero />
<Hero class="mb-20 max-md:mt-[80px]" />
<Preview />
<ProductsShowcase />
<!-- <ProductsSlider title="محصولات پرفروش" />-->
<ProductsGrid title="محصولات پرفروش" :products="[...homeData!.products,...homeData!.products]"/>
<Categories />
<ProductsShowcase class="mb-40" />
<ProductsGrid
title="محصولات پرفروش"
:products="[...homeData!.products,...homeData!.products]"
/>
<Categories class="mt-40" />
<Brands />
<MostRecentComments />
<LatestStories />
<ClientOnly>
<LatestStories class="mb-20" />
</ClientOnly>
</div>
</template>
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 MiB

Binary file not shown.
+1 -1
View File
@@ -109,7 +109,7 @@ declare global {
full_name: string;
profile_photo: string;
};
category: number;
category: SubCategory;
};
type UserComment = {