merge
This commit is contained in:
@@ -31,5 +31,5 @@ jobs:
|
|||||||
script: |
|
script: |
|
||||||
cd /root/hshop/
|
cd /root/hshop/
|
||||||
docker compose down
|
docker compose down
|
||||||
docker compose build
|
docker compose build --no-cache
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.idea
|
||||||
@@ -61,7 +61,7 @@ Code: {otp}"""
|
|||||||
# response = sms_api.send_otp_sms(otp_input)
|
# response = sms_api.send_otp_sms(otp_input)
|
||||||
|
|
||||||
if response['statusCode'] == 200:
|
if response['statusCode'] == 200:
|
||||||
return Response({'detail': 'OTP sent successfully'}, status=status.HTTP_200_OK)
|
return Response({'detail': f'OTP sent successfully {otp}'}, status=status.HTTP_200_OK)
|
||||||
else:
|
else:
|
||||||
print(response)
|
print(response)
|
||||||
return Response({'detail': f'مشکلی در ارسال کد رخ داد'}, status=status.HTTP_200_OK)
|
return Response({'detail': f'مشکلی در ارسال کد رخ داد'}, status=status.HTTP_200_OK)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from .models import BlogModel
|
from .models import BlogModel
|
||||||
from account.models import User
|
from account.models import User
|
||||||
|
from product.serializers import SubCategorySerializer
|
||||||
|
|
||||||
class AuthorSerializer(serializers.ModelSerializer):
|
class AuthorSerializer(serializers.ModelSerializer):
|
||||||
full_name = serializers.SerializerMethodField()
|
full_name = serializers.SerializerMethodField()
|
||||||
@@ -14,6 +15,7 @@ class AuthorSerializer(serializers.ModelSerializer):
|
|||||||
return 'ادمین وبسایت'
|
return 'ادمین وبسایت'
|
||||||
|
|
||||||
class BlogSerilizer(serializers.ModelSerializer):
|
class BlogSerilizer(serializers.ModelSerializer):
|
||||||
|
category = SubCategorySerializer()
|
||||||
author = AuthorSerializer()
|
author = AuthorSerializer()
|
||||||
class Meta:
|
class Meta:
|
||||||
model = BlogModel
|
model = BlogModel
|
||||||
@@ -22,6 +24,7 @@ class BlogSerilizer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class AllBlogSerilizer(serializers.ModelSerializer):
|
class AllBlogSerilizer(serializers.ModelSerializer):
|
||||||
author = AuthorSerializer()
|
author = AuthorSerializer()
|
||||||
|
category = SubCategorySerializer()
|
||||||
class Meta:
|
class Meta:
|
||||||
model = BlogModel
|
model = BlogModel
|
||||||
exclude = ('is_published', 'content', 'summery', )
|
exclude = ('is_published', 'content',)
|
||||||
@@ -30,7 +30,7 @@ EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD")
|
|||||||
DEFAULT_FROM_EMAIL = os.getenv("SECRET_KEY")
|
DEFAULT_FROM_EMAIL = os.getenv("SECRET_KEY")
|
||||||
|
|
||||||
# Security and Debugging
|
# Security and Debugging
|
||||||
SECRET_KEY = os.getenv("SECRET_KEY")
|
SECRET_KEY = os.getenv("SECRET_KEY") or 'this is just for cron job dont judge me'
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||||
|
|
||||||
@@ -65,6 +65,7 @@ INSTALLED_APPS = [
|
|||||||
"rest_framework.authtoken",
|
"rest_framework.authtoken",
|
||||||
"import_export",
|
"import_export",
|
||||||
"django_jalali",
|
"django_jalali",
|
||||||
|
'django_crontab',
|
||||||
# Custom Apps
|
# Custom Apps
|
||||||
"product",
|
"product",
|
||||||
"account",
|
"account",
|
||||||
@@ -233,3 +234,12 @@ AWS_S3_OBJECT_PARAMETERS = {
|
|||||||
'ACL': 'public-read',
|
'ACL': 'public-read',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# django CRONJOBS
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
CRONJOBS = [
|
||||||
|
('* * * * *', 'product.cron.update_product_prices', f'>> {BASE_DIR}/logfile.log 2>&1'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ COPY . /app/
|
|||||||
|
|
||||||
CMD ["sh", "-c", "python manage.py makemigrations && \
|
CMD ["sh", "-c", "python manage.py makemigrations && \
|
||||||
python manage.py migrate && \
|
python manage.py migrate && \
|
||||||
|
python manage.py crontab add && \
|
||||||
python manage.py collectstatic --no-input && \
|
python manage.py collectstatic --no-input && \
|
||||||
gunicorn core.wsgi:application --bind 0.0.0.0:8000 --workers 3"]
|
gunicorn core.wsgi:application --bind 0.0.0.0:8000 --workers 3"]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.1.2 on 2025-03-08 18:54
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('order', '0010_alter_orderitemmodel_quantity'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderitemmodel',
|
||||||
|
name='price',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='قیمت'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -97,13 +97,14 @@ class OrderModel(models.Model):
|
|||||||
class OrderItemModel(models.Model):
|
class OrderItemModel(models.Model):
|
||||||
order = models.ForeignKey(OrderModel, on_delete=models.CASCADE, related_name='items', verbose_name='سفارش')
|
order = models.ForeignKey(OrderModel, on_delete=models.CASCADE, related_name='items', verbose_name='سفارش')
|
||||||
quantity = models.PositiveSmallIntegerField(verbose_name="تعداد")
|
quantity = models.PositiveSmallIntegerField(verbose_name="تعداد")
|
||||||
|
price = models.PositiveIntegerField(verbose_name='قیمت')
|
||||||
product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول")
|
product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول")
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'ایتم سبد خرید'
|
verbose_name = 'ایتم سبد خرید'
|
||||||
verbose_name_plural = 'ایتم های سبد خرید'
|
verbose_name_plural = 'ایتم های سبد خرید'
|
||||||
|
|
||||||
def total(self):
|
def total(self):
|
||||||
return self.quantity * self.product.get_toman_price()
|
return self.quantity * self.product.price
|
||||||
|
|
||||||
def total_with_discount(self):
|
def total_with_discount(self):
|
||||||
return self.quantity * self.product.get_toman_price_after_discount()
|
return self.quantity * self.product.get_toman_price_after_discount()
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from .models import OrderItemModel, OrderModel
|
from .models import OrderItemModel, OrderModel
|
||||||
|
from product.serializers import ProductVariantSerialzier
|
||||||
|
|
||||||
class OrderItemSerailzier(serializers.ModelSerializer):
|
class OrderItemSerailzier(serializers.ModelSerializer):
|
||||||
|
product = serializers.SerializerMethodField()
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderItemModel
|
model = OrderItemModel
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = ('order', 'product')
|
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):
|
class CartSerializer(serializers.ModelSerializer):
|
||||||
items = OrderItemSerailzier(many=True)
|
items = OrderItemSerailzier(many=True)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from django.contrib.postgres.fields import ArrayField
|
|||||||
from unfold.widgets import UnfoldAdminColorInputWidget
|
from unfold.widgets import UnfoldAdminColorInputWidget
|
||||||
from unfold.decorators import action, display
|
from unfold.decorators import action, display
|
||||||
from utils.admin import ModelAdmin
|
from utils.admin import ModelAdmin
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -140,8 +140,10 @@ class ProductVariantInLine(StackedInline):
|
|||||||
show_change_link = True
|
show_change_link = True
|
||||||
tab = True
|
tab = True
|
||||||
min_num = 1
|
min_num = 1
|
||||||
|
readonly_fields = ['price']
|
||||||
# inlines = [DetailModelInLine]
|
# inlines = [DetailModelInLine]
|
||||||
autocomplete_fields = ['product_attributes', 'in_pack_items', 'images', 'details']
|
autocomplete_fields = ['product_attributes', 'in_pack_items', 'images', 'details']
|
||||||
|
fields = ['images', 'video','input_price', 'min_price', 'currency', 'price', 'discount','in_stock', 'color', 'product_attributes', 'in_pack_items', 'details','sell']
|
||||||
# search_fields = ['']
|
# search_fields = ['']
|
||||||
|
|
||||||
|
|
||||||
@@ -156,6 +158,7 @@ class ProductVariantAdmin(ModelAdmin, ImportExportModelAdmin):
|
|||||||
export_form_class = ExportForm
|
export_form_class = ExportForm
|
||||||
autocomplete_fields = ['product_attributes', 'images', 'in_pack_items', 'details']
|
autocomplete_fields = ['product_attributes', 'images', 'in_pack_items', 'details']
|
||||||
warn_unsaved_form = True
|
warn_unsaved_form = True
|
||||||
|
readonly_fields = ['price']
|
||||||
# inlines = [DetailModelInLine]
|
# inlines = [DetailModelInLine]
|
||||||
|
|
||||||
@admin.register(ProductModel)
|
@admin.register(ProductModel)
|
||||||
@@ -169,6 +172,7 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin):
|
|||||||
autocomplete_fields = ['related_products', ]
|
autocomplete_fields = ['related_products', ]
|
||||||
# compressed_fields = True
|
# compressed_fields = True
|
||||||
warn_unsaved_form = True
|
warn_unsaved_form = True
|
||||||
|
actions_list = ['redirect_to_learn', 'update_products_price']
|
||||||
list_display = ['display_image', 'display_price', 'view', 'show', 'rating', 'category', ]
|
list_display = ['display_image', 'display_price', 'view', 'show', 'rating', 'category', ]
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('فیلد های اصلی', {'fields': ('name', 'description', 'category', 'related_products', 'show',), "classes": ["tab"],}),
|
('فیلد های اصلی', {'fields': ('name', 'description', 'category', 'related_products', 'show',), "classes": ["tab"],}),
|
||||||
@@ -188,7 +192,7 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin):
|
|||||||
|
|
||||||
def display_price(self, obj):
|
def display_price(self, obj):
|
||||||
if obj.variants.all().first():
|
if obj.variants.all().first():
|
||||||
return obj.variants.all().first().get_toman_price()
|
return obj.variants.all().first().price
|
||||||
display_price.short_description = 'قیمت تومانی'
|
display_price.short_description = 'قیمت تومانی'
|
||||||
|
|
||||||
@display(description='محصول', header=True)
|
@display(description='محصول', header=True)
|
||||||
@@ -209,6 +213,15 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin):
|
|||||||
# "squared": True,
|
# "squared": True,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@action(description=f"اپدیت قیمت ها")
|
||||||
|
def update_products_price(self, request):
|
||||||
|
print('from the button')
|
||||||
|
ProductVariant.update_all_prices()
|
||||||
|
messages.success(request, f"قیمت {ProductVariant.objects.all().count()} تنوع محصول اپدیت شد")
|
||||||
|
return redirect("admin:product_productmodel_changelist")
|
||||||
|
|
||||||
|
|
||||||
# @display(
|
# @display(
|
||||||
# description=("نمایش در صفحه ی اصلی"),
|
# description=("نمایش در صفحه ی اصلی"),
|
||||||
# label={
|
# label={
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from product.models import ProductVariant
|
||||||
|
|
||||||
|
def update_product_prices():
|
||||||
|
print('calling the update product prices from cron')
|
||||||
|
ProductVariant.update_all_prices()
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Generated by Django 5.1.2 on 2025-03-08 18:54
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('product', '0033_alter_productvariant_details'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='productvariant',
|
||||||
|
name='max_price',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='productvariant',
|
||||||
|
name='input_price',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='قیمت ورودی'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='productvariant',
|
||||||
|
name='color',
|
||||||
|
field=models.CharField(blank=True, max_length=7, null=True, verbose_name='رنگ'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='productvariant',
|
||||||
|
name='details',
|
||||||
|
field=models.ManyToManyField(related_name='product', to='product.productdetailmodel', verbose_name='جزییات محصول'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='productvariant',
|
||||||
|
name='price',
|
||||||
|
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='قیمت محاسبه شده'),
|
||||||
|
),
|
||||||
|
]
|
||||||
+36
-14
@@ -215,14 +215,13 @@ class ProductDetailModel(models.Model):
|
|||||||
# def __str__(self):
|
# def __str__(self):
|
||||||
# return f'جزيیات محصول {self.product}'
|
# return f'جزيیات محصول {self.product}'
|
||||||
|
|
||||||
|
|
||||||
class ProductVariant(models.Model):
|
class ProductVariant(models.Model):
|
||||||
product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='variants', verbose_name='محصول')
|
product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='variants', verbose_name='محصول')
|
||||||
product_attributes = models.ManyToManyField(AttributeValue, verbose_name='ویژگیها', related_name='variant')
|
product_attributes = models.ManyToManyField(AttributeValue, verbose_name='ویژگیها', related_name='variant')
|
||||||
in_stock = models.PositiveIntegerField(default=0, verbose_name='تعداد موجود')
|
in_stock = models.PositiveIntegerField(default=0, verbose_name='تعداد موجود')
|
||||||
price = models.PositiveIntegerField(default=0, verbose_name='قیمت')
|
price = models.PositiveIntegerField(verbose_name='قیمت محاسبه شده', blank=True, null=True)
|
||||||
|
input_price = models.PositiveIntegerField(default=0, verbose_name='قیمت ورودی')
|
||||||
min_price = models.PositiveIntegerField(verbose_name='قیمت کف', help_text='این قیمت برای کف قیمتی محصول در نظر گرفته میشود')
|
min_price = models.PositiveIntegerField(verbose_name='قیمت کف', help_text='این قیمت برای کف قیمتی محصول در نظر گرفته میشود')
|
||||||
max_price = models.PositiveIntegerField(verbose_name='قیمت سقف', help_text='این قیمت برای سقف قیمتی محصول در نظر گرفته میشود')
|
|
||||||
currency_type = (
|
currency_type = (
|
||||||
('dollor', 'دلار'),
|
('dollor', 'دلار'),
|
||||||
('toman', 'تومان'),
|
('toman', 'تومان'),
|
||||||
@@ -232,33 +231,56 @@ class ProductVariant(models.Model):
|
|||||||
sell = models.IntegerField(default=0, verbose_name='فروش')
|
sell = models.IntegerField(default=0, verbose_name='فروش')
|
||||||
currency = models.CharField(verbose_name='نوع ارز', max_length=20, choices=currency_type)
|
currency = models.CharField(verbose_name='نوع ارز', max_length=20, choices=currency_type)
|
||||||
discount = models.SmallIntegerField(default=0, verbose_name='تخفیف')
|
discount = models.SmallIntegerField(default=0, verbose_name='تخفیف')
|
||||||
color = models.CharField(verbose_name='رنک', max_length=7, blank=True, null=True)
|
color = models.CharField(verbose_name='رنگ', max_length=7, blank=True, null=True)
|
||||||
images = models.ManyToManyField(ProductImageModel, verbose_name='عکس ها')
|
images = models.ManyToManyField(ProductImageModel, verbose_name='عکس ها')
|
||||||
video = models.FileField(upload_to='product_videos/', blank=True, null=True, verbose_name='ویدیو')
|
video = models.FileField(upload_to='product_videos/', blank=True, null=True, verbose_name='ویدیو')
|
||||||
details = models.ManyToManyField(ProductDetailModel, verbose_name='جزيیات محصول', related_name='product')
|
details = models.ManyToManyField(ProductDetailModel, verbose_name='جزییات محصول', related_name='product')
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'تنوع محصول'
|
verbose_name = 'تنوع محصول'
|
||||||
verbose_name_plural = 'تنوعهای محصول'
|
verbose_name_plural = 'تنوعهای محصول'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.product.name} - {', '.join(str(attr) for attr in self.product_attributes.all())}"
|
return f"{self.product.name} - {', '.join(str(attr) for attr in self.product_attributes.all())}"
|
||||||
|
|
||||||
def get_toman_price(self, dollor_price=None):
|
def set_or_update_price(self, dollor_price=None):
|
||||||
if not dollor_price:
|
if not dollor_price:
|
||||||
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
|
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
|
||||||
dollor_price = dollor_object.price
|
dollor_price = dollor_object.price
|
||||||
|
|
||||||
dollar_to_dirham = 0.27
|
|
||||||
if dollor_price is None:
|
if dollor_price is None:
|
||||||
raise ValidationError({"dollor_price": "The 'dollor_price' must be provided in the context for dollar pricing."})
|
raise ValidationError({"dollor_price": "The 'dollor_price' must be provided in the context for dollar pricing."})
|
||||||
|
|
||||||
|
dollar_to_dirham = 0.27
|
||||||
|
|
||||||
if self.currency == 'toman':
|
if self.currency == 'toman':
|
||||||
toman_price = self.price
|
toman_price = self.input_price
|
||||||
elif self.currency == 'dollor':
|
elif self.currency == 'dollor':
|
||||||
toman_price = self.price * dollor_price
|
toman_price = self.input_price * dollor_price
|
||||||
elif self.currency == 'derham':
|
elif self.currency == 'derham':
|
||||||
toman_price = self.price * dollor_price * dollar_to_dirham
|
toman_price = self.input_price * dollor_price * dollar_to_dirham
|
||||||
toman_price = toman_price if toman_price > self.min_price else self.min_price
|
else:
|
||||||
return toman_price
|
toman_price = self.input_price
|
||||||
|
|
||||||
|
self.price = max(toman_price, self.min_price)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.set_or_update_price()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def get_toman_price_after_discount(self):
|
def get_toman_price_after_discount(self):
|
||||||
return self.get_toman_price() * ((100 - self.discount) / 100)
|
return self.price * ((100 - self.discount) / 100)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_all_prices(cls):
|
||||||
|
print('calling the update all prices ')
|
||||||
|
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
|
||||||
|
print(dollor_object.price)
|
||||||
|
dollor_object.update_price()
|
||||||
|
dollor_object.save()
|
||||||
|
dollor_price = dollor_object.price
|
||||||
|
print(dollor_object.price)
|
||||||
|
print('classmethod dollor price update ')
|
||||||
|
products = cls.objects.all()
|
||||||
|
for product in products:
|
||||||
|
product.set_or_update_price(dollor_price=dollor_price)
|
||||||
|
product.save()
|
||||||
@@ -46,18 +46,13 @@ class ProductImageSerailizer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class ProductVariantSerialzier(serializers.ModelSerializer):
|
class ProductVariantSerialzier(serializers.ModelSerializer):
|
||||||
product_attributes = AttributeValueSerialzier(many=True)
|
product_attributes = AttributeValueSerialzier(many=True)
|
||||||
price = serializers.SerializerMethodField()
|
|
||||||
in_pack_items = InPackItemsSerialzier(many=True)
|
in_pack_items = InPackItemsSerialzier(many=True)
|
||||||
images = ProductImageSerailizer(many=True)
|
images = ProductImageSerailizer(many=True)
|
||||||
details = ProductDetailSerializer(many=True, read_only=True)
|
details = ProductDetailSerializer(many=True, read_only=True)
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProductVariant
|
model = ProductVariant
|
||||||
exclude = ('min_price', 'max_price','sell', 'currency', 'product')
|
exclude = ('min_price', 'sell', 'currency', 'product', 'input_price')
|
||||||
|
|
||||||
def get_price(self, obj):
|
|
||||||
dollor_price = self.context.get('dollor_price')
|
|
||||||
toman_price = obj.get_toman_price(dollor_price=dollor_price)
|
|
||||||
return "{:,.0f} تومان".format(toman_price)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ django-cleanup==8.1.0
|
|||||||
django-colorfield==0.11.0
|
django-colorfield==0.11.0
|
||||||
django-cors-headers==4.4.0
|
django-cors-headers==4.4.0
|
||||||
django-cron==0.6.0
|
django-cron==0.6.0
|
||||||
|
django-crontab==0.7.1
|
||||||
django-dbbackup==4.2.1
|
django-dbbackup==4.2.1
|
||||||
django-dirtyfields==1.9.3
|
django-dirtyfields==1.9.3
|
||||||
django-filter==24.3
|
django-filter==24.3
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ services:
|
|||||||
[
|
[
|
||||||
"sh",
|
"sh",
|
||||||
"-c",
|
"-c",
|
||||||
"python manage.py migrate && python manage.py collectstatic --no-input && gunicorn core.wsgi:application --bind 0.0.0.0:8000 --workers 3",
|
"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",
|
||||||
]
|
]
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@utility input-outlined-error {
|
@utility input-outlined-error {
|
||||||
@apply text-danger-600 border-danger-600;
|
@apply text-danger-600 border-danger-600;
|
||||||
svg[class~=iconify] path {
|
svg[class~=iconify]:not(.static-icon) path {
|
||||||
@apply stroke-danger-600;
|
@apply stroke-danger-600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
@utility input-outlined-disabled {
|
@utility input-outlined-disabled {
|
||||||
@apply text-slate-300 border-slate-200;
|
@apply text-slate-300 border-slate-200;
|
||||||
svg[class~=iconify] path {
|
svg[class~=iconify]:not(.static-icon) path {
|
||||||
@apply stroke-slate-300;
|
@apply stroke-slate-300;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
&:focus-within {
|
&:focus-within {
|
||||||
@apply border-black text-black;
|
@apply border-black text-black;
|
||||||
|
|
||||||
svg[class~=iconify] path {
|
svg[class~=iconify]:not(.static-icon) path {
|
||||||
@apply stroke-black;
|
@apply stroke-black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
@utility input-outlined {
|
@utility input-outlined {
|
||||||
@apply text-slate-500 border-slate-200;
|
@apply text-slate-500 border-slate-200;
|
||||||
|
|
||||||
svg[class~=iconify] path {
|
svg[class~=iconify]:not(.static-icon) path {
|
||||||
@apply stroke-slate-500;
|
@apply stroke-slate-500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,11 @@
|
|||||||
@import "./fonts/yekan-bakh.css";
|
@import "./fonts/yekan-bakh.css";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
|
|
||||||
|
/* CONTAINER */
|
||||||
|
|
||||||
|
--app-container-padding: 20px;
|
||||||
|
|
||||||
/* COLORS */
|
/* COLORS */
|
||||||
--color-slate-50: hsl(210, 40%, 98%);
|
--color-slate-50: hsl(210, 40%, 98%);
|
||||||
--color-slate-100: hsl(210, 40%, 96%);
|
--color-slate-100: hsl(210, 40%, 96%);
|
||||||
@@ -128,6 +133,8 @@
|
|||||||
/* ANIMATIONS */
|
/* ANIMATIONS */
|
||||||
--animate-marquee: marquee 20s linear infinite;
|
--animate-marquee: marquee 20s linear infinite;
|
||||||
--animate-marquee-reverse: marquee 20s linear infinite reverse;
|
--animate-marquee-reverse: marquee 20s linear infinite reverse;
|
||||||
|
--animate-fade-in: fadeIn 350ms ease-in-out;
|
||||||
|
|
||||||
--animate-slide-down: slideDown 300ms ease-out;
|
--animate-slide-down: slideDown 300ms ease-out;
|
||||||
--animate-slide-up: slideUp 300ms ease-out;
|
--animate-slide-up: slideUp 300ms ease-out;
|
||||||
--animate-overlay-show: overlayShow 150ms ease-in;
|
--animate-overlay-show: overlayShow 150ms ease-in;
|
||||||
@@ -148,6 +155,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes slideDown {
|
@keyframes slideDown {
|
||||||
from {
|
from {
|
||||||
height: 0;
|
height: 0;
|
||||||
@@ -265,31 +282,7 @@
|
|||||||
/* CONTAINER */
|
/* CONTAINER */
|
||||||
|
|
||||||
@utility container {
|
@utility container {
|
||||||
@apply mx-auto px-6;
|
@apply mx-auto px-[var(--app-container-padding)];
|
||||||
|
|
||||||
@screen 2xs {
|
|
||||||
@apply px-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
@screen xs {
|
|
||||||
@apply px-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
@screen sm {
|
|
||||||
@apply px-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
@screen md {
|
|
||||||
@apply px-8;
|
|
||||||
}
|
|
||||||
|
|
||||||
@screen lg {
|
|
||||||
@apply px-12;
|
|
||||||
}
|
|
||||||
|
|
||||||
@screen xl {
|
|
||||||
@apply px-20;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer {
|
@layer {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
placeholder: "وارد نشده",
|
placeholder: "وارد نشده",
|
||||||
});
|
});
|
||||||
const { variant, message, error, disabled, modelValue } = toRefs(props);
|
const { variant, error, modelValue } = toRefs(props);
|
||||||
|
|
||||||
// emits
|
// emits
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ const inputRef = ref<HTMLInputElement | null>(null);
|
|||||||
|
|
||||||
const value = computed({
|
const value = computed({
|
||||||
get: () => modelValue.value ?? "",
|
get: () => modelValue.value ?? "",
|
||||||
set: (value) => emit("update:modelValue", value),
|
set: (value) => emit("update:modelValue", value)
|
||||||
});
|
});
|
||||||
|
|
||||||
const classes = computed(() => {
|
const classes = computed(() => {
|
||||||
@@ -41,8 +41,8 @@ const classes = computed(() => {
|
|||||||
"input-effects": !error.value,
|
"input-effects": !error.value,
|
||||||
[variant.value === "solid"
|
[variant.value === "solid"
|
||||||
? "input-solid-error"
|
? "input-solid-error"
|
||||||
: "input-outlined-error"]: error.value,
|
: "input-outlined-error"]: error.value
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
// types
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string,
|
||||||
|
products: ProductListItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// props
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="w-full flex flex-col gap-10 md:gap-[4rem] py-[5rem] container">
|
||||||
|
<div class="w-full flex justify-between items-center">
|
||||||
|
<span class="text-black typo-h-6 max-sm:text-xl md:typo-h-5 lg:typo-h-4">
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
<NuxtLink to="/products">
|
||||||
|
<Button variant="outlined" class="rounded-full max-sm:typo-label-sm max-sm:py-2" end-icon="ci:arrow-left">
|
||||||
|
نمایش همه
|
||||||
|
</Button>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-y-8 gap-5 sm:gap-8">
|
||||||
|
<ProductCard
|
||||||
|
v-for="product in products"
|
||||||
|
:key="product.id"
|
||||||
|
:id="product.id"
|
||||||
|
:title="product.name"
|
||||||
|
:picture="product.variants[0].images[0].image"
|
||||||
|
:colors="product.colors"
|
||||||
|
:price="product.variants[0].price"
|
||||||
|
:rate="product.rating"
|
||||||
|
:dark-layer="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -35,11 +35,11 @@ const highlights = ref<Highlight[]>([
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="w-full border-t-[0.5px] border-slate-200">
|
<section class="w-full border-t-[0.5px] border-slate-200">
|
||||||
<div class="w-full flex-center py-[5rem] gap-[1.25rem] container">
|
<div class="w-full py-[5rem] gap-12 xs:gap-8 sm:gap-12 xl:gap-0 container grid grid-cols-1 xs:grid-cols-2 lg:grid-cols-4">
|
||||||
<template v-for="(highlight, index) in highlights" :key="index">
|
<template v-for="(highlight, index) in highlights" :key="index">
|
||||||
<div class="flex flex-col-center gap-[.75rem] w-1/4 px-5">
|
<div class="flex flex-col-center gap-[.75rem] px-5">
|
||||||
|
|
||||||
<img :src="highlight.icon" class="size-[90px]" alt="" />
|
<img :src="highlight.icon" class="size-[70px] md:size-[90px]" alt="" />
|
||||||
|
|
||||||
<div class="w-full flex-col-center gap-[.25rem]">
|
<div class="w-full flex-col-center gap-[.25rem]">
|
||||||
<span class="typo-sub-h-md text-black text-center">
|
<span class="typo-sub-h-md text-black text-center">
|
||||||
@@ -51,10 +51,10 @@ const highlights = ref<Highlight[]>([
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<!-- <div-->
|
||||||
class="w-[1px] h-[5rem] bg-slate-200"
|
<!-- class="w-[1px] h-[5rem] bg-slate-200"-->
|
||||||
v-if="index + 1 != highlights.length"
|
<!-- v-if="index + 1 != highlights.length"-->
|
||||||
/>
|
<!-- />-->
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -33,60 +33,72 @@ const { colorObject } = useImageColor(`#product-image-${id.value}`);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLink :to="'/product/' + id">
|
<NuxtLink :to="'/product/' + id">
|
||||||
<div
|
<div class="@container">
|
||||||
class="group relative size-full aspect-square rounded-2xl bg-white brightness-[98%] overflow-hidden p-6"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
:id="`product-image-${id}`"
|
|
||||||
:src="picture"
|
|
||||||
class="group-hover:scale-105 transition-transform duration-200 size-full object-contain absolute inset-0"
|
|
||||||
alt="product-background"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="darkLayer"
|
class="group relative size-full aspect-square rounded-xl @[280px]:rounded-2xl bg-white brightness-[98%] overflow-hidden p-6"
|
||||||
class="bg-linear-to-t inset-0 from-black/50 to-transparent to-55% absolute z-10 size-full"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex justify-between items-center absolute px-6 pt-6 top-0 w-full inset-x-0"
|
|
||||||
>
|
|
||||||
<Rate v-if="rate" :rate="rate"/>
|
|
||||||
<Tag v-if="tag">
|
|
||||||
{{ tag }}
|
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
:class="
|
|
||||||
colorObject?.isLight && !darkLayer
|
|
||||||
? 'text-black'
|
|
||||||
: 'text-white'
|
|
||||||
"
|
|
||||||
class="absolute inset-x-0 bottom-0 pb-4 md:pb-6 px-4 md:px-6 flex flex-row-reverse justify-between items-end z-10"
|
|
||||||
>
|
>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2 items-start w-full">
|
<img
|
||||||
<!-- <span class="typo-p-sm md:typo-p-md !font-medium">-->
|
:id="`product-image-${id}`"
|
||||||
<!-- {{ brand }}-->
|
:src="picture"
|
||||||
<!-- </span>-->
|
class="group-hover:scale-105 transition-transform duration-200 size-full object-contain absolute inset-0"
|
||||||
<span class="typo-sub-h-md md:typo-sub-h-lg">
|
alt="product-background"
|
||||||
{{ title }}
|
/>
|
||||||
</span>
|
|
||||||
<div class="flex items-center justify-between w-full mt-1">
|
<div
|
||||||
<div class="flex items-center gap-2 md:mt-1">
|
v-if="darkLayer"
|
||||||
<ColorCircle
|
class="bg-linear-to-t inset-0 from-black/50 to-transparent to-55% absolute z-10 size-full"
|
||||||
v-for="color in colors"
|
/>
|
||||||
:key="color"
|
|
||||||
:style="{ backgroundColor: color }"
|
<div
|
||||||
class="!size-5 md:!size-6"
|
class="flex justify-between items-center absolute px-4 @[280px]:px-6 pt-4 @[280px]:pt-6 top-0 w-full inset-x-0"
|
||||||
/>
|
>
|
||||||
|
<Rate v-if="rate" :rate="rate" />
|
||||||
|
<Tag v-if="tag">
|
||||||
|
{{ tag }}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
colorObject?.isLight && !darkLayer
|
||||||
|
? 'text-black'
|
||||||
|
: 'text-white'
|
||||||
|
"
|
||||||
|
class="absolute inset-x-0 bottom-0 pb-4 @[280px]:pb-6 px-4 @[280px]:px-6 flex flex-row-reverse justify-between items-end z-10"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 items-start w-full">
|
||||||
|
<span class="@max-[280px]:hidden typo-sub-h-md @[280px]:typo-sub-h-lg truncate w-full">
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center justify-between w-full mt-1">
|
||||||
|
<div class="flex items-center gap-2 @[280px]:mt-1">
|
||||||
|
<ColorCircle
|
||||||
|
v-for="color in colors"
|
||||||
|
:key="color"
|
||||||
|
:style="{ backgroundColor: color }"
|
||||||
|
class="!size-5 @[280px]:!size-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="@max-[280px]:hidden typo-p-xs @[280px]:typo-p-md !font-semibold whitespace-nowrap">
|
||||||
|
{{ price }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="typo-p-sm md:typo-p-md font-medium whitespace-nowrap">
|
|
||||||
{{ price }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1 px-2 items-start w-full text-black mt-4 @[280px]:hidden">
|
||||||
|
<span class="typo-sub-h-sm w-full truncate">
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
<div class="@[280px]:hidden flex items-center justify-between w-full mt-1">
|
||||||
|
<span class="typo-p-xs !font-semibold whitespace-nowrap">
|
||||||
|
{{ price }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ onMounted(() => {
|
|||||||
end: "bottom top"
|
end: "bottom top"
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
const calculateOnResize = () => {
|
||||||
if (window.innerWidth > 768) {
|
if (window.innerWidth > 768) {
|
||||||
gsap.to("#header-navbar", {
|
gsap.to("#header-navbar", {
|
||||||
background: "transparent",
|
background: "transparent",
|
||||||
@@ -133,19 +133,18 @@ onMounted(() => {
|
|||||||
resetTimelineForMobile();
|
resetTimelineForMobile();
|
||||||
scrollTrigger.disable();
|
scrollTrigger.disable();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
calculateOnResize()
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
calculateOnResize()
|
||||||
|
}, 200);
|
||||||
|
|
||||||
window.addEventListener("resize", () => {
|
window.addEventListener("resize", () => {
|
||||||
if (window.innerWidth > 768) {
|
calculateOnResize()
|
||||||
gsap.to("#header-navbar", {
|
|
||||||
background: "transparent",
|
|
||||||
filter: "invert(100%)"
|
|
||||||
});
|
|
||||||
scrollTrigger.enable();
|
|
||||||
} else {
|
|
||||||
resetTimelineForMobile();
|
|
||||||
scrollTrigger.disable();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -170,7 +169,7 @@ onUnmounted(() => {
|
|||||||
:centered-slides="true"
|
:centered-slides="true"
|
||||||
:breakpoints="{
|
:breakpoints="{
|
||||||
768: {
|
768: {
|
||||||
spaceBetween : 40
|
spaceBetween : 40,
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
@swiper="onSwiper"
|
@swiper="onSwiper"
|
||||||
@@ -178,96 +177,97 @@ onUnmounted(() => {
|
|||||||
<SwiperSlide
|
<SwiperSlide
|
||||||
v-for="(slide, index) in homeData!.sliders"
|
v-for="(slide, index) in homeData!.sliders"
|
||||||
:key="slide.id"
|
:key="slide.id"
|
||||||
class="max-md:container"
|
|
||||||
>
|
>
|
||||||
<div
|
<div class="max-md:container">
|
||||||
class="header-slider-item relative w-full overflow-hidden max-md:rounded-[20px]"
|
|
||||||
>
|
|
||||||
<template v-if="!!slide.video">
|
|
||||||
<video
|
|
||||||
:id="`slide-video-${index}`"
|
|
||||||
:muted="swiper_instance?.realIndex !== index ? true : isMuted"
|
|
||||||
loop
|
|
||||||
class="slide-video absolute inset-0 size-full object-cover brightness-90"
|
|
||||||
:src="slide.video"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<img
|
|
||||||
v-else
|
|
||||||
class="absolute inset-0 size-full object-cover"
|
|
||||||
:src="slide.image!"
|
|
||||||
:alt="slide.title"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="size-full absolute z-10 bg-linear-to-t from-black/50 to-transparent" />
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
:class="swiper_instance?.realIndex !== index ? 'opacity-0' : ''"
|
class="header-slider-item relative w-full overflow-hidden max-md:rounded-[20px]"
|
||||||
class="w-full transition-opacity pb-6 xs:pb-10 lg:pb-16 px-6 xs:px-10 lg:px-16 gap-6 xs:gap-10 lg:gap-12 container flex flex-col h-full justify-end relative z-10"
|
|
||||||
>
|
>
|
||||||
|
<template v-if="!!slide.video">
|
||||||
|
<video
|
||||||
|
:id="`slide-video-${index}`"
|
||||||
|
:muted="swiper_instance?.realIndex !== index ? true : isMuted"
|
||||||
|
loop
|
||||||
|
class="slide-video absolute inset-0 size-full object-cover brightness-90"
|
||||||
|
:src="slide.video"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="header-slider-item-child w-full">
|
<img
|
||||||
<div class="border-b border-white/10 pb-6 flex flex-col gap-2 md:gap-4">
|
v-else
|
||||||
<div class="flex items-center gap-4 lg:gap-8">
|
class="absolute inset-0 size-full object-cover"
|
||||||
<div
|
:src="slide.image!"
|
||||||
class="hover:scale-110 size-[36px] md:size-[42px] lg:size-[50px] relative flex items-center justify-center">
|
:alt="slide.title"
|
||||||
<div class="size-full scale-75 bg-white absolute rounded-full animate-ping" />
|
/>
|
||||||
<button
|
|
||||||
@click="isMuted = !isMuted"
|
<div class="size-full absolute z-10 bg-linear-to-t from-black/50 to-transparent" />
|
||||||
class="transition-all cursor-pointer flex-center bg-white z-10 size-full rounded-full"
|
|
||||||
>
|
<div
|
||||||
<Icon
|
:class="swiper_instance?.realIndex !== index ? 'opacity-0' : ''"
|
||||||
:name="isMuted ? 'bi:volume-mute-fill' : 'bi:volume-up-fill'"
|
class="w-full transition-opacity pb-6 xs:pb-10 lg:pb-16 px-6 xs:px-10 lg:px-16 gap-6 xs:gap-10 lg:gap-12 container flex flex-col h-full justify-end relative z-10"
|
||||||
class="text-black size-4 md:size-[18px] lg:size-[24px]"
|
>
|
||||||
/>
|
|
||||||
</button>
|
<div class="header-slider-item-child w-full">
|
||||||
|
<div class="border-b border-white/10 pb-6 flex flex-col gap-2 md:gap-4">
|
||||||
|
<div class="flex items-center gap-4 lg:gap-8">
|
||||||
|
<div
|
||||||
|
class="hover:scale-110 size-[36px] md:size-[42px] lg:size-[50px] relative flex items-center justify-center">
|
||||||
|
<div class="size-full scale-75 bg-white absolute rounded-full animate-ping" />
|
||||||
|
<button
|
||||||
|
@click="isMuted = !isMuted"
|
||||||
|
class="transition-all cursor-pointer flex-center bg-white z-10 size-full rounded-full"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
:name="isMuted ? 'bi:volume-mute-fill' : 'bi:volume-up-fill'"
|
||||||
|
class="text-black size-4 md:size-[18px] lg:size-[24px]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<h3 class="typo-h-6 md:typo-h-4 lg:typo-h-1 tracking-[-2px] text-white">
|
||||||
|
{{ slide.title }}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="typo-h-6 md:typo-h-4 lg:typo-h-1 tracking-[-2px] text-white">
|
<div class="flex justify-between items-center">
|
||||||
{{ slide.title }}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<span class="truncate typo-p-xs md:typo-p-sm lg:typo-p-lg text-white">
|
<span class="truncate typo-p-xs md:typo-p-sm lg:typo-p-lg text-white">
|
||||||
{{ slide.description }}
|
{{ slide.description }}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
class="max-sm:hidden max-lg:typo-label-xs invert rounded-full hover:bg-transparent">
|
class="max-sm:hidden max-lg:typo-label-xs invert rounded-full hover:bg-transparent">
|
||||||
مشاهده
|
مشاهده
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="header-slider-pagination-child"
|
id="header-slider-pagination-child"
|
||||||
class="flex items-center justify-between"
|
class="flex items-center justify-between"
|
||||||
>
|
>
|
||||||
<button @click="swiper_instance?.slidePrev()">
|
<button @click="swiper_instance?.slidePrev()">
|
||||||
<Icon
|
<Icon
|
||||||
class="**:stroke-white cursor-pointer size-5 md:size-6"
|
class="**:stroke-white cursor-pointer size-5 md:size-6"
|
||||||
name="ci:arrow-right"
|
name="ci:arrow-right"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex items-center justify-center gap-3 text-white">
|
<div class="flex items-center justify-center gap-3 text-white">
|
||||||
<div
|
<div
|
||||||
v-for="(_slide, index) in homeData!.sliders"
|
v-for="(_slide, index) in homeData!.sliders"
|
||||||
:class="swiper_instance?.realIndex === index ? 'bg-white' : 'bg-transparent'"
|
:class="swiper_instance?.realIndex === index ? 'bg-white' : 'bg-transparent'"
|
||||||
class="border border-white size-2 md:size-3 rounded-full transition-all duration-200"
|
class="border border-white size-2 md:size-3 rounded-full transition-all duration-200"
|
||||||
>
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button>
|
||||||
|
<Icon
|
||||||
|
@click="swiper_instance?.slideNext()"
|
||||||
|
class="**:stroke-white cursor-pointer size-5 md:size-6"
|
||||||
|
name="ci:arrow-left"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button>
|
|
||||||
<Icon
|
|
||||||
@click="swiper_instance?.slideNext()"
|
|
||||||
class="**:stroke-white cursor-pointer size-5 md:size-6"
|
|
||||||
name="ci:arrow-left"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
</Swiper>
|
</Swiper>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ await suspense();
|
|||||||
<template>
|
<template>
|
||||||
<section class="mt-20 container">
|
<section class="mt-20 container">
|
||||||
<div class="flex items-center justify-between mb-20">
|
<div class="flex items-center justify-between mb-20">
|
||||||
<span class="typo-h-6 md:typo-h-5 lg:typo-h-4 text-black">
|
<span class="typo-h-6 max-sm:text-xl md:typo-h-5 lg:typo-h-4 text-black">
|
||||||
مقالات اخیر سایت
|
مقالات اخیر سایت
|
||||||
</span>
|
</span>
|
||||||
<NuxtLink to="/articles">
|
<NuxtLink to="/articles">
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ watch(
|
|||||||
(newValue) => {
|
(newValue) => {
|
||||||
const clientRect = previewContainerEl.value?.getBoundingClientRect()!;
|
const clientRect = previewContainerEl.value?.getBoundingClientRect()!;
|
||||||
const percent = clientRect.width / 100;
|
const percent = clientRect.width / 100;
|
||||||
const clipPercent = (newValue - clientRect.x - 8) / percent;
|
const clipPercent = ((newValue + draggableEl.value!.clientWidth / 2) - clientRect.x - 8) / percent;
|
||||||
if (clipPercent >= 5 && clipPercent <= 95) {
|
if (clipPercent >= 5 && clipPercent <= 95) {
|
||||||
clipPathPercent.value = clipPercent;
|
clipPathPercent.value = clipPercent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const { data : homeData } = useHomeData();
|
|||||||
const { $gsap: gsap, $ScrollTrigger: ScrollTrigger } = useNuxtApp();
|
const { $gsap: gsap, $ScrollTrigger: ScrollTrigger } = useNuxtApp();
|
||||||
|
|
||||||
let gsapTimeline: gsap.core.Timeline;
|
let gsapTimeline: gsap.core.Timeline;
|
||||||
|
let scrollTrigger: ScrollTrigger;
|
||||||
|
|
||||||
// lifecycle
|
// lifecycle
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ onMounted(() => {
|
|||||||
filter: "invert(0%)"
|
filter: "invert(0%)"
|
||||||
});
|
});
|
||||||
|
|
||||||
ScrollTrigger.create({
|
scrollTrigger = ScrollTrigger.create({
|
||||||
trigger: "#products-showcase-container",
|
trigger: "#products-showcase-container",
|
||||||
animation: gsapTimeline,
|
animation: gsapTimeline,
|
||||||
scrub: 1,
|
scrub: 1,
|
||||||
@@ -77,6 +78,10 @@ onMounted(() => {
|
|||||||
end: "bottom top"
|
end: "bottom top"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollTrigger.refresh()
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -89,7 +94,7 @@ onUnmounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
id="products-showcase-container"
|
id="products-showcase-container"
|
||||||
class="mt-80 mb-40 perspective-midrange w-full h-[125svh] bg-black flex items-center justify-center"
|
class="mb-40 perspective-midrange w-full h-[125svh] bg-black flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-for="slide in homeData!.show_case_slider"
|
v-for="slide in homeData!.show_case_slider"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||||
|
import type { GetArticleResponse } from "~/composables/api/blog/useGetArticle";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ export type GetHomeDataResponse = {
|
|||||||
"video": string | null
|
"video": string | null
|
||||||
}[],
|
}[],
|
||||||
"sub_categories": SubCategory[],
|
"sub_categories": SubCategory[],
|
||||||
"products": Product[],
|
"products": ProductListItem[],
|
||||||
"difreance_section": {
|
"difreance_section": {
|
||||||
"image1": string,
|
"image1": string,
|
||||||
"image2": string,
|
"image2": string,
|
||||||
|
|||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
FROM node:20-alpine AS build-stage
|
FROM node:20-alpine as build-stage
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install --force
|
RUN npm install --force
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM node:20-alpine AS production-stage
|
FROM node:20-alpine as production-stage
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build-stage /app /app
|
COPY --from=build-stage /app /app
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
@@ -31,7 +31,6 @@
|
|||||||
"gsap": "^3.12.7",
|
"gsap": "^3.12.7",
|
||||||
"isomorphic-dompurify": "^2.22.0",
|
"isomorphic-dompurify": "^2.22.0",
|
||||||
"jalali-ts": "^8.0.0",
|
"jalali-ts": "^8.0.0",
|
||||||
"masonry-layout": "^4.2.2",
|
|
||||||
"nuxt": "^3.15.4",
|
"nuxt": "^3.15.4",
|
||||||
"reka-ui": "^1.0.0-alpha.6",
|
"reka-ui": "^1.0.0-alpha.6",
|
||||||
"swiper": "^11.2.4",
|
"swiper": "^11.2.4",
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
// import
|
// import
|
||||||
|
|
||||||
import useHomeData from "~/composables/api/home/useHomeData";
|
import useHomeData from "~/composables/api/home/useHomeData";
|
||||||
|
import ProductsGrid from "~/components/global/ProductsGrid.vue";
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
const { suspense } = useHomeData();
|
const { data: homeData, suspense } = useHomeData();
|
||||||
|
|
||||||
// ssr
|
// ssr
|
||||||
|
|
||||||
@@ -27,7 +28,8 @@ if (response.isError) {
|
|||||||
<Hero />
|
<Hero />
|
||||||
<Preview />
|
<Preview />
|
||||||
<ProductsShowcase />
|
<ProductsShowcase />
|
||||||
<ProductsSlider title="محصولات پرفروش" />
|
<!-- <ProductsSlider title="محصولات پرفروش" />-->
|
||||||
|
<ProductsGrid title="محصولات پرفروش" :products="[...homeData!.products,...homeData!.products]"/>
|
||||||
<Categories />
|
<Categories />
|
||||||
<Brands />
|
<Brands />
|
||||||
<MostRecentComments />
|
<MostRecentComments />
|
||||||
|
|||||||
+22
-41
@@ -21,13 +21,12 @@ type LoginInfo = {
|
|||||||
// meta
|
// meta
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
|
layout: "none",
|
||||||
middleware: ["check-is-not-logged-in"]
|
middleware: ["check-is-not-logged-in"]
|
||||||
});
|
});
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
const { $gsap: gsap } = useNuxtApp();
|
|
||||||
|
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
|
|
||||||
const { updateToken, updateRefreshToken } = useAuth();
|
const { updateToken, updateRefreshToken } = useAuth();
|
||||||
@@ -150,21 +149,6 @@ const resetForm = () => {
|
|||||||
showOtp.value = false;
|
showOtp.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// const onMouseMove = (e: MouseEvent) => {
|
|
||||||
// const heylmzEyesElement = document.querySelector("#heylmz-eyes") as HTMLDivElement;
|
|
||||||
// const boundry = heylmzEyesElement.getBoundingClientRect();
|
|
||||||
//
|
|
||||||
// console.log(e.clientX, heylmzEyesElement.clientWidth, boundry.left, e.clientX - heylmzEyesElement.clientWidth);
|
|
||||||
//
|
|
||||||
// const moveX = e.clientX - heylmzEyesElement.clientWidth + 60;
|
|
||||||
// const moveY = e.clientY - heylmzEyesElement.clientHeight - 155;
|
|
||||||
//
|
|
||||||
// gsap.to("#heylmz-eyes", {
|
|
||||||
// x: moveX,
|
|
||||||
// y: moveY
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -179,8 +163,7 @@ const resetForm = () => {
|
|||||||
<div class="flex items-center justify-center flex-col size-full">
|
<div class="flex items-center justify-center flex-col size-full">
|
||||||
|
|
||||||
<img
|
<img
|
||||||
id="heylmz-eyes"
|
class="aspect-square w-[300px] translate-y-[100px] animate-fade-in"
|
||||||
class="aspect-square w-[300px] translate-y-[100px]"
|
|
||||||
src="/img/heymlz-seat.gif"
|
src="/img/heymlz-seat.gif"
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
@@ -188,30 +171,28 @@ const resetForm = () => {
|
|||||||
<div
|
<div
|
||||||
class="max-w-[600px] w-full p-6 h-[400px] flex flex-col items-center bg-white border shadow-black/10 justify-center border-slate-300 rounded-xl"
|
class="max-w-[600px] w-full p-6 h-[400px] flex flex-col items-center bg-white border shadow-black/10 justify-center border-slate-300 rounded-xl"
|
||||||
>
|
>
|
||||||
<h1 class="typo-hero-2 mt-8">فرم ورود</h1>
|
<h1 class="typo-h-5 mt-8">شماره خود را وارد کنید</h1>
|
||||||
|
|
||||||
<form @submit.prevent class="max-w-[500px] w-full mt-12">
|
<form @submit.prevent class="max-w-[500px] w-full mt-12">
|
||||||
<div v-if="!showOtp" class="flex items-center gap-2 w-full">
|
<Input
|
||||||
<Input
|
v-if="!showOtp"
|
||||||
data-testid="phone-input"
|
class="w-full tracking-[2px]"
|
||||||
class="w-full"
|
v-model="loginInfo.phone"
|
||||||
v-model="loginInfo.phone"
|
placeholder="9380123456"
|
||||||
placeholder="9380123456"
|
dir="ltr"
|
||||||
dir="ltr"
|
:error="formValidator$.phone.$error"
|
||||||
:error="formValidator$.phone.$error"
|
>
|
||||||
>
|
<template #startItem>
|
||||||
<template #startItem>
|
<div class="flex items-center gap-3">
|
||||||
<span class="text-slate-500"> +98 </span>
|
<Icon
|
||||||
</template>
|
class="translate-y-[-1px] static-icon"
|
||||||
</Input>
|
name="twemoji:flag-iran"
|
||||||
<div class="flex items-center gap-1">
|
size="24"
|
||||||
<Icon
|
/>
|
||||||
class="translate-y-[-1px]"
|
<span class="text-slate-500 typo-label-sm"> +98 </span>
|
||||||
name="twemoji:flag-iran"
|
</div>
|
||||||
size="24"
|
</template>
|
||||||
/>
|
</Input>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<OtpInput
|
<OtpInput
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ precacheAndRoute(self.__WB_MANIFEST);
|
|||||||
|
|
||||||
// Version
|
// Version
|
||||||
|
|
||||||
const VERSION = "1.0.9112";
|
const VERSION = "1.0.2";
|
||||||
|
|
||||||
// Service Worker Installation
|
// Service Worker Installation
|
||||||
self.addEventListener("install", (event) => {
|
self.addEventListener("install", (event) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user