merge
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from .celery import app as celery_app
|
||||
|
||||
__all__ = ('celery_app',)
|
||||
@@ -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)
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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='*'),
|
||||
},
|
||||
}
|
||||
@@ -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"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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})
|
||||
|
||||
@@ -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
@@ -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 = 'ایتم سبد خرید'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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)})
|
||||
@@ -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()
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با
|
||||
استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه و مجله
|
||||
در ستون و سطرآنچنان که لازم.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -103,7 +103,7 @@ watch(
|
||||
درصد تخفیف
|
||||
</div>
|
||||
</div>
|
||||
<Rating />
|
||||
<Rating :rate="3" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -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.
Vendored
+1
-1
@@ -109,7 +109,7 @@ declare global {
|
||||
full_name: string;
|
||||
profile_photo: string;
|
||||
};
|
||||
category: number;
|
||||
category: SubCategory;
|
||||
};
|
||||
|
||||
type UserComment = {
|
||||
|
||||
Reference in New Issue
Block a user