Merge branch 'main' of https://github.com/Byeto-Company/hossein_por_shop
This commit is contained in:
@@ -2,6 +2,9 @@ from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, Permis
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import random
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from datetime import datetime, timedelta
|
||||
from django.utils import timezone
|
||||
from rest_framework_simplejwt.token_blacklist.models import BlacklistedToken, OutstandingToken
|
||||
@@ -113,7 +116,7 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
for token in tokens:
|
||||
BlacklistedToken.objects.get_or_create(token=token)
|
||||
except Exception as e:
|
||||
print(f"block list error: {e}")
|
||||
logger.error(f"block list error: {e}")
|
||||
|
||||
|
||||
def __str__(self):
|
||||
@@ -200,7 +203,6 @@ class PushSubscription(models.Model):
|
||||
"icon": 'https://api.heymlz.com' + icon,
|
||||
"image": 'https://api.heymlz.com' + icon,
|
||||
}
|
||||
print(payload)
|
||||
try:
|
||||
webpush(
|
||||
subscription_info={
|
||||
@@ -214,7 +216,7 @@ class PushSubscription(models.Model):
|
||||
}
|
||||
)
|
||||
except WebPushException as ex:
|
||||
print("Failed to send notification:", ex)
|
||||
logger.error(f"Failed to send notification: {ex}")
|
||||
|
||||
@classmethod
|
||||
def send_group_notification(cls, user, title, body):
|
||||
@@ -240,7 +242,7 @@ class PushSubscription(models.Model):
|
||||
}
|
||||
)
|
||||
except WebPushException as ex:
|
||||
print(f"Failed to send notification to {sub.user}:", ex)
|
||||
logger.error(f"Failed to send notification to {sub.user}: {ex}")
|
||||
|
||||
|
||||
|
||||
@@ -277,10 +279,10 @@ def get_location_from_ip(ip_address):
|
||||
if data["status"] == "success":
|
||||
return data['country'], data['regionName'], data['city'], data.get('zip', 'ناموجود'), data['lat'], data['lon'], data['isp']
|
||||
else:
|
||||
print("Error fetching data: ", data["message"])
|
||||
logger.error(f"Error fetching data: {data['message']}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {e}")
|
||||
logger.error(f"An error occurred: {e}")
|
||||
return None
|
||||
|
||||
class SecurityBreachAttemptModel(models.Model):
|
||||
|
||||
@@ -7,7 +7,6 @@ urlpatterns = [
|
||||
path('profile', views.ProfileView.as_view()),
|
||||
path('verify', views.TokenVerifyView.as_view(), name='jwt-verify'),
|
||||
path('send_otp', views.SendOTPView.as_view(), name='send-otp-view'),
|
||||
path('yee_token_bedeeee', views.KonGhoshadToken.as_view()),
|
||||
path('address/create', views.CreateAddressView.as_view(), name='create-address'),
|
||||
path('address/edit/<int:pk>', views.EditAddressView.as_view(), name='edit-address'),
|
||||
path('address/delete/<int:pk>', views.DeleteAddressView.as_view(), name='delete-address'),
|
||||
|
||||
@@ -71,10 +71,10 @@ class SendOTPView(APIView):
|
||||
|
||||
|
||||
if response['statusCode'] == 200:
|
||||
return Response({'detail': f'OTP sent successfully {otp}'}, status=status.HTTP_200_OK)
|
||||
return Response({'detail': f'OTP sent successfully'}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
print(response)
|
||||
return Response({f'detail': f'مشکلی در ارسال کد رخ داد {otp}'}, status=status.HTTP_200_OK)
|
||||
logger.error(f"OTP send failed with response: {response}")
|
||||
return Response({f'detail': f'مشکلی در ارسال کد رخ داد '}, status=status.HTTP_200_OK)
|
||||
# return Response({'detail': response, 'otp_code': otp}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
except User.DoesNotExist:
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from django.db import models
|
||||
from account.models import User
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from product.models import ProductModel
|
||||
from django.conf import settings
|
||||
import openai
|
||||
@@ -55,7 +58,7 @@ class ProductChatModel(models.Model):
|
||||
self.thread = thread.id
|
||||
|
||||
except Exception as e:
|
||||
print(f'error in chat class: {e}')
|
||||
logger.error(f'error in chat class: {e}')
|
||||
raise ValueError(f"Error creating OpenAI thread: {e}")
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
@@ -136,7 +136,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
]
|
||||
|
||||
LANGUAGE_CODE = "fa"
|
||||
TIME_ZONE = "UTC"
|
||||
TIME_ZONE = "Asia/Tehran"
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
USE_TZ = True
|
||||
|
||||
@@ -21,7 +21,7 @@ STATIC_ROOT = 'app/static'
|
||||
|
||||
USE_X_FORWARDED_HOST = True
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', DOMAIN, API_DOMAIN, '0.0.0.0', "185.110.189.208", 'www.google.com', 'google.com']
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
|
||||
@@ -171,6 +171,18 @@ UNFOLD = {
|
||||
"link": reverse_lazy("admin:order_discountcode_changelist"),
|
||||
"permission": lambda request: request.user.is_superuser,
|
||||
},
|
||||
{
|
||||
"title": _("نظرات کاربران"),
|
||||
"icon": "comment",
|
||||
"link": reverse_lazy("admin:product_commentmodel_changelist"),
|
||||
"permission": lambda request: request.user.is_superuser,
|
||||
},
|
||||
{
|
||||
"title": _("امتیاز کاربران"),
|
||||
"icon": "star_rate_half",
|
||||
"link": reverse_lazy("admin:product_productrating_changelist"),
|
||||
"permission": lambda request: request.user.is_superuser,
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import json
|
||||
import random
|
||||
import logging
|
||||
from functools import lru_cache
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from django.contrib.humanize.templatetags.humanize import intcomma
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -274,9 +277,7 @@ class FakeAdminLoginView(View):
|
||||
ip = x_forwarded_for.split(',')[0]
|
||||
else:
|
||||
ip = request.META.get("REMOTE_ADDR")
|
||||
print(ip)
|
||||
print(len(ip))
|
||||
print(type(ip))
|
||||
logger.info(f"Honeypot GET request from IP: {ip}, length: {len(ip)}, type: {type(ip).__name__}")
|
||||
hacker, created = SecurityBreachAttemptModel.objects.get_or_create(ip_address=ip)
|
||||
return render(request, 'admin/fake_login.html', self.get_context(request))
|
||||
|
||||
@@ -286,7 +287,7 @@ class FakeAdminLoginView(View):
|
||||
ip = x_forwarded_for.split(',')[0]
|
||||
else:
|
||||
ip = request.META.get("REMOTE_ADDR")
|
||||
print(ip)
|
||||
logger.warning(f"Honeypot POST request from IP: {ip}")
|
||||
hacker, created = SecurityBreachAttemptModel.objects.get_or_create(ip_address=ip)
|
||||
hacker.trys += 1
|
||||
hacker.save()
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from django.contrib import admin, messages
|
||||
from .models import *
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from unfold.admin import TabularInline, StackedInline
|
||||
from unfold.contrib.inlines.admin import NonrelatedTabularInline
|
||||
from django.db.models import Q
|
||||
@@ -35,8 +38,7 @@ class OrderItemAdmin(ModelAdmin):
|
||||
|
||||
if not hasattr(request.user, 'shop'):
|
||||
return False
|
||||
print(obj.product.product.shop)
|
||||
print(request.user.shop)
|
||||
logger.debug(f"Shop permissions check: obj.shop={obj.product.product.shop}, user.shop={request.user.shop}")
|
||||
return request.user.shop == obj.product.product.shop
|
||||
|
||||
|
||||
|
||||
@@ -55,6 +55,10 @@ def generate_order_invoice(order_id):
|
||||
qr_code_path = os.path.join(template_dir, 'qr-code.png')
|
||||
|
||||
# Use stored model fields for accuracy and consistency
|
||||
# Format the Jalali date for display
|
||||
jalali_date = to_jalali(order.created_at)
|
||||
created_at_jalali_str = jalali_date.strftime('%Y-%m-%d %H:%M') if jalali_date else '---'
|
||||
|
||||
context = {
|
||||
'order': order,
|
||||
'order_number': order.pk,
|
||||
@@ -63,7 +67,7 @@ def generate_order_invoice(order_id):
|
||||
'user': order.user,
|
||||
'address': order.address,
|
||||
'discount_code': order.discount_code,
|
||||
'created_at_jalali': to_jalali(order.created_at),
|
||||
'created_at_jalali': created_at_jalali_str,
|
||||
'total_items': sum(item.quantity for item in items),
|
||||
'subtotal': order.cart_total or 0, # Stored field: total before any discounts
|
||||
'items_discount_amount': total_items_discount,
|
||||
@@ -157,6 +161,10 @@ def generate_shop_order_invoice(shop_order_id):
|
||||
# Calculate subtotal (cart total before discounts)
|
||||
subtotal = shop_order.subtotal or 0
|
||||
|
||||
# Format the Jalali date for display
|
||||
jalali_date = jdatetime.datetime.fromgregorian(datetime=shop_order.order_created_at) if shop_order.order_created_at else None
|
||||
created_at_jalali_str = jalali_date.strftime('%Y-%m-%d %H:%M') if jalali_date else '---'
|
||||
|
||||
# Resolve image paths for the template (absolute file paths for WeasyPrint)
|
||||
import os
|
||||
template_dir = os.path.join(settings.BASE_DIR, 'templates', 'order')
|
||||
@@ -181,7 +189,7 @@ def generate_shop_order_invoice(shop_order_id):
|
||||
'address_recipient_name': shop_order.address_recipient_name,
|
||||
'items': items,
|
||||
'items_with_discount': items_with_discount,
|
||||
'created_at_jalali': jdatetime.datetime.fromgregorian(datetime=shop_order.order_created_at) if shop_order.order_created_at else None,
|
||||
'created_at_jalali': created_at_jalali_str,
|
||||
'total_items': shop_order.items_count,
|
||||
'subtotal': subtotal, # Total after product discount
|
||||
'items_discount_amount': total_items_discount, # Product discount amount
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.2 on 2026-05-12 05:46
|
||||
|
||||
import django.utils.timezone
|
||||
import django_jalali.db.models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0043_shopdailyreport_is_settled_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ordermodel',
|
||||
name='created_at',
|
||||
field=django_jalali.db.models.jDateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='تاریخ ثبت سفارش'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2 on 2026-05-12 05:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0044_alter_ordermodel_created_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ordermodel',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ثبت سفارش'),
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,8 @@
|
||||
from account.models import SpecialDiscountCode
|
||||
from django.db import models, transaction
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from account.models import User, UserAddressModel, PushSubscription
|
||||
from product.models import ProductModel, ProductVariant, ProductImageModel
|
||||
from django.utils import timezone
|
||||
@@ -33,7 +36,7 @@ class DiscountCode(models.Model):
|
||||
elif not self.quantity > 0:
|
||||
return 'این کد تخفیف تمام شده است'
|
||||
else:
|
||||
print('log later bug')
|
||||
logger.warning('Discount code validity check failed')
|
||||
|
||||
|
||||
class Cart(models.Model):
|
||||
@@ -168,8 +171,8 @@ class OrderModel(models.Model):
|
||||
null=True, related_name='orders', verbose_name='کاربر')
|
||||
address = models.ForeignKey(UserAddressModel, on_delete=models.SET_NULL,
|
||||
related_name='orders', null=True, verbose_name='ادرس')
|
||||
created_at = jmodels.jDateField(
|
||||
blank=True, null=True, verbose_name="تاریخ ثبت سفارش")
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="تاریخ ثبت سفارش")
|
||||
is_paid = models.BooleanField(default=False, verbose_name="وضعیت پرداخت")
|
||||
discount_code = models.ForeignKey(
|
||||
DiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف")
|
||||
@@ -227,9 +230,7 @@ class OrderModel(models.Model):
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
# Log the error if you have logging setup
|
||||
# logger.error(f"Failed to rollback stock for order {self.pk}: {e}")
|
||||
logger.error(f"Failed to rollback stock for order {self.pk}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -264,7 +265,7 @@ class OrderItemModel(models.Model):
|
||||
# @property
|
||||
def price_after_special_discount(self):
|
||||
all_discounts = (self.special_discount_amount or 0) + self.total_product_discount_amount()
|
||||
print(all_discounts)
|
||||
logger.debug(f"Total discounts calculated: {all_discounts}")
|
||||
return self.total_price_before_discount() - all_discounts
|
||||
|
||||
def unit_price(self):
|
||||
|
||||
@@ -40,8 +40,8 @@ def send_change_status_notif(instance_pk, new_status):
|
||||
for user_sub in user_subs:
|
||||
try:
|
||||
user_sub.send_notif(f'سفارش شما به {new_status} تغییر کرد', f'سفارش شما به {new_status} تغییر کرد', ProductImageModel.objects.all().first().image.url)
|
||||
except:
|
||||
print('log later send notif error')
|
||||
except Exception as e:
|
||||
logger.error('Error sending status notification: ' + str(e))
|
||||
|
||||
@shared_task
|
||||
def send_change_status_sms(instance_pk, new_status):
|
||||
@@ -77,7 +77,7 @@ def generate_daily_shop_reports():
|
||||
from .models import ShopOrderModel, ShopDailyReport
|
||||
|
||||
target_date = (timezone.now() - timedelta(days=1)).date()
|
||||
print(f'Generating shop reports for {target_date}')
|
||||
logging.info(f'Generating shop reports for {target_date}')
|
||||
|
||||
shop_orders = ShopOrderModel.objects.filter(created_at__date=target_date)
|
||||
if not shop_orders.exists():
|
||||
|
||||
@@ -390,13 +390,13 @@ class PaymentView(APIView):
|
||||
})
|
||||
|
||||
except AZBankGatewaysException as e:
|
||||
print(f"Payment gateway error: {e}")
|
||||
logger.error(f"Payment gateway error: {e}")
|
||||
return Response({
|
||||
'error': 'خطا در اتصال به درگاه پرداخت'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Order creation error: {e}")
|
||||
logger.error(f"Order creation error: {e}")
|
||||
return Response({
|
||||
'error': 'خطا در ثبت سفارش'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from django.contrib import admin, messages
|
||||
from django import forms
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# from product.tasks import update_prices
|
||||
from .models import *
|
||||
from unfold.admin import TabularInline, StackedInline
|
||||
@@ -263,13 +266,13 @@ class ProductDetailModel1Admin(ModelAdmin, ImportExportModelAdmin):
|
||||
def get_queryset(self, request):
|
||||
|
||||
if request.user.is_superuser:
|
||||
print('here')
|
||||
logger.info('Returning all ProductDetailModels for superuser')
|
||||
return ProductDetailModel.objects.all()
|
||||
|
||||
if not hasattr(request.user, 'shop'):
|
||||
print(' in here 2')
|
||||
logger.info('User has no shop, returning empty queryset')
|
||||
return ProductDetailModel.objects.none()
|
||||
print('in here 3')
|
||||
logger.info('Filtering ProductDetailModels by shop')
|
||||
queryset = ProductDetailModel.objects.filter(product__product__shop__id=request.user.shop.id)
|
||||
return queryset
|
||||
|
||||
@@ -543,6 +546,20 @@ class CommentAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||
return obj.content[0:35] + '...'
|
||||
display_content.short_description = 'محتوای کامنت'
|
||||
|
||||
def has_view_permission(self, request, obj = ...):
|
||||
return request.user.is_superuser
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return request.user.is_superuser
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return request.user.is_superuser
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return request.user.is_superuser
|
||||
|
||||
|
||||
|
||||
@admin.register(DollorModel)
|
||||
class DollorAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||
import_form_class = ImportForm
|
||||
@@ -558,4 +575,27 @@ class DollorAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||
"widget": ArrayWidget,
|
||||
}
|
||||
}
|
||||
readonly_fields = ('price',)
|
||||
readonly_fields = ('price',)
|
||||
|
||||
@admin.register(ProductRating)
|
||||
class ProductRatingAdmin(ModelAdmin):
|
||||
list_display = ('product', 'user', 'rating', 'created_at')
|
||||
list_filter = ('rating', 'created_at')
|
||||
search_fields = ('product__name', 'user__phone', 'user__first_name', 'user__last_name')
|
||||
readonly_fields = ('product', 'user', 'created_at', 'updated_at')
|
||||
date_hierarchy = 'created_at'
|
||||
ordering = ('-created_at',)
|
||||
compressed_fields = True
|
||||
warn_unsaved_form = True
|
||||
|
||||
def has_view_permission(self, request, obj = ...):
|
||||
return request.user.is_superuser
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return request.user.is_superuser
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return request.user.is_superuser
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return request.user.is_superuser
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.2 on 2026-05-12 04:59
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0072_remove_productmodel_product_show_idx_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProductRating',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('rating', models.PositiveIntegerField(choices=[(1, '1 - بسیار ضعیف'), (2, '2 - ضعیف'), (3, '3 - متوسط'), (4, '4 - خوب'), (5, '5 - عالی')], verbose_name='امتیاز')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='تاریخ آپدیت')),
|
||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ratings', to='product.productmodel', verbose_name='محصول')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='product_ratings', to=settings.AUTH_USER_MODEL, verbose_name='کاربر')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'امتیاز محصول',
|
||||
'verbose_name_plural': 'امتیازات محصول',
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [models.Index(fields=['product'], name='rating_product_idx'), models.Index(fields=['user'], name='rating_user_idx'), models.Index(fields=['rating'], name='rating_rating_idx')],
|
||||
'unique_together': {('product', 'user')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,8 @@
|
||||
from django.db import models
|
||||
from django.utils.text import slugify
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from account.models import User
|
||||
from django.urls import reverse
|
||||
import requests
|
||||
@@ -148,7 +151,7 @@ class DollorModel(models.Model):
|
||||
data = response.json()
|
||||
price = int(data["lastTradePrice"])
|
||||
price_in_usd = price / 10.0
|
||||
print('\n\nprice from api \n\n')
|
||||
logger.info('Price fetched from API')
|
||||
except Exception as e:
|
||||
return self.defualt_price
|
||||
|
||||
@@ -493,3 +496,37 @@ class ProductVariant(DirtyFieldsMixin, models.Model):
|
||||
def save(self, *args, **kwargs):
|
||||
self.set_or_update_price()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class ProductRating(models.Model):
|
||||
RATING_CHOICES = (
|
||||
(1, '1 - بسیار ضعیف'),
|
||||
(2, '2 - ضعیف'),
|
||||
(3, '3 - متوسط'),
|
||||
(4, '4 - خوب'),
|
||||
(5, '5 - عالی'),
|
||||
)
|
||||
|
||||
product = models.ForeignKey(
|
||||
ProductModel, on_delete=models.CASCADE, related_name='ratings', verbose_name='محصول')
|
||||
user = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE, related_name='product_ratings', verbose_name='کاربر')
|
||||
rating = models.PositiveIntegerField(
|
||||
choices=RATING_CHOICES, verbose_name='امتیاز')
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True, verbose_name='تاریخ ایجاد')
|
||||
updated_at = models.DateTimeField(
|
||||
auto_now=True, verbose_name='تاریخ آپدیت')
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'امتیاز محصول'
|
||||
verbose_name_plural = 'امتیازات محصول'
|
||||
unique_together = ('product', 'user')
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['product'], name='rating_product_idx'),
|
||||
models.Index(fields=['user'], name='rating_user_idx'),
|
||||
models.Index(fields=['rating'], name='rating_rating_idx'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user} - {self.product} ({self.rating}⭐)"
|
||||
@@ -158,7 +158,8 @@ class DynamicProductSerializer(serializers.ModelSerializer):
|
||||
main_image = serializers.SerializerMethodField()
|
||||
customer_pickup_title = serializers.SerializerMethodField()
|
||||
customer_pickup_description = serializers.SerializerMethodField()
|
||||
|
||||
average_rating = serializers.SerializerMethodField()
|
||||
user_rating = serializers.SerializerMethodField()
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
view_type = self.context.get('view_type', 'all')
|
||||
@@ -174,12 +175,23 @@ class DynamicProductSerializer(serializers.ModelSerializer):
|
||||
model = ProductModel
|
||||
fields = "__all__"
|
||||
view_type = {
|
||||
'list': ['id', 'name', 'rating', 'slug', 'category', 'colors', 'image', 'best_deal_price_before_discount', 'best_deal_price_after_discount', 'best_deal_discount', 'main_image'],
|
||||
'slider': ['id', 'name', 'rating', 'slug', 'category', 'variants', 'colors', 'image', 'best_deal_price_before_discount', 'best_deal_price_after_discount', 'best_deal_discount', ],
|
||||
'instance': ['id', 'name', 'description', 'rating', 'slug', 'meta_description', 'meta_keywords', 'meta_rating', 'category', 'related_products', 'in_pack_items', 'variants', 'colors', 'added_to_favorites', 'image', 'customer_pickup_title', 'customer_pickup_description'],
|
||||
'list': ['id', 'name', 'rating', 'slug', 'category', 'colors', 'image', 'best_deal_price_before_discount', 'best_deal_price_after_discount', 'best_deal_discount', 'main_image', 'average_rating'],
|
||||
'slider': ['id', 'name', 'rating', 'slug', 'category', 'variants', 'colors', 'image', 'best_deal_price_before_discount', 'best_deal_price_after_discount', 'best_deal_discount', 'average_rating'],
|
||||
'instance': ['id', 'name', 'description', 'rating', 'slug', 'meta_description', 'meta_keywords', 'meta_rating', 'category', 'related_products', 'in_pack_items', 'variants', 'colors', 'added_to_favorites', 'image', 'customer_pickup_title', 'customer_pickup_description', 'average_rating', 'user_rating'],
|
||||
'chat': ['id', 'name', 'description', 'variants', 'image']
|
||||
}
|
||||
|
||||
|
||||
def get_user_rating(self, obj):
|
||||
request = self.context.get('request')
|
||||
if not request.user.is_authenticated:
|
||||
return None
|
||||
product_ratings = obj.ratings.all()
|
||||
if product_ratings.filter(user=request.user).exists():
|
||||
return product_ratings.filter(user=request.user).first().rating
|
||||
else:
|
||||
return None
|
||||
|
||||
def _get_best_deal_variant(self, obj):
|
||||
"""Get best deal variant from prefetched variants (pre-ordered by discount/price)"""
|
||||
if not hasattr(self, '_best_deal_cache'):
|
||||
@@ -241,6 +253,39 @@ class DynamicProductSerializer(serializers.ModelSerializer):
|
||||
# Use exists() with filter instead of fetching all products
|
||||
return UserFavorites.objects.filter(user=request.user, products=obj).exists()
|
||||
|
||||
def get_average_rating(self, obj):
|
||||
"""Get cached average rating for product - optimized with prefetch"""
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Avg
|
||||
|
||||
cache_key = f'product_avg_rating_{obj.id}'
|
||||
avg_rating = cache.get(cache_key)
|
||||
|
||||
if avg_rating is None:
|
||||
# Try to use prefetched ratings if available (no query needed)
|
||||
try:
|
||||
prefetched_ratings = obj._prefetched_objects_cache.get('ratings')
|
||||
if prefetched_ratings is not None:
|
||||
# Use prefetched data - no database query
|
||||
ratings_list = [r.rating for r in prefetched_ratings]
|
||||
if ratings_list:
|
||||
avg_rating = round(sum(ratings_list) / len(ratings_list), 2)
|
||||
else:
|
||||
avg_rating = 0
|
||||
else:
|
||||
# Fall back to aggregation query if not prefetched
|
||||
avg_rating = obj.ratings.aggregate(Avg('rating'))['rating__avg'] or 0
|
||||
avg_rating = round(avg_rating, 2)
|
||||
except (AttributeError, KeyError):
|
||||
# Fall back to aggregation query
|
||||
avg_rating = obj.ratings.aggregate(Avg('rating'))['rating__avg'] or 0
|
||||
avg_rating = round(avg_rating, 2)
|
||||
|
||||
# Cache for 1 hour
|
||||
cache.set(cache_key, avg_rating, 3600)
|
||||
|
||||
return avg_rating
|
||||
|
||||
def get_variants(self, obj):
|
||||
view_type = self.context.get('view_type')
|
||||
if view_type == 'slider':
|
||||
@@ -292,3 +337,25 @@ class BotProductSerializer(serializers.ModelSerializer):
|
||||
'pk',
|
||||
'name'
|
||||
]
|
||||
|
||||
|
||||
class ProductRatingSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ProductRating
|
||||
fields = ['rating']
|
||||
|
||||
def validate_rating(self, value):
|
||||
if value not in [1, 2, 3, 4, 5]:
|
||||
raise serializers.ValidationError("امتیاز باید بین 1 تا 5 باشد")
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
product_id = self.context.get('product_id')
|
||||
user = self.context.get('request').user
|
||||
|
||||
rating = ProductRating.objects.create(
|
||||
product_id=product_id,
|
||||
user=user,
|
||||
rating=validated_data['rating']
|
||||
)
|
||||
return rating
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
from django.urls import path, re_path
|
||||
from .views import AllCategories, ProductView, AllProductsView, CommentView, ShowCaseProductsView, ShowCaseCategoryListView, BotProductsView,BotProductDetailView,BotCategoryView ,AllCategoriesV2
|
||||
from .views import (
|
||||
AllCategories, ProductView, AllProductsView, CommentView,
|
||||
ShowCaseProductsView, ShowCaseCategoryListView, BotProductsView,
|
||||
BotProductDetailView, BotCategoryView, AllCategoriesV2,
|
||||
ProductRatingView
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('slider_category', ShowCaseProductsView.as_view(), name='category-products'),
|
||||
@@ -10,6 +15,7 @@ urlpatterns = [
|
||||
path('categories/bot', BotCategoryView.as_view(), name='bot-categories'),
|
||||
path('slider_categories', ShowCaseCategoryListView.as_view(), name='all-categories'),
|
||||
re_path(r'^comments/(?P<slug>[\w\u0600-\u06FF\-]+)$', CommentView.as_view(), name='comment-views'),
|
||||
re_path(r'^(?P<slug>[\w\u0600-\u06FF\-]+)/rating/$', ProductRatingView.as_view(), name='product-rating'),
|
||||
re_path(r'^(?P<slug>[\w\u0600-\u06FF\-]+)/$', ProductView.as_view(), name='product-detail'),
|
||||
path('', AllProductsView.as_view(), name='category-products'),
|
||||
]
|
||||
]
|
||||
|
||||
@@ -134,7 +134,15 @@ class ProductView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
# authentication_classes = []
|
||||
|
||||
@extend_schema(
|
||||
responses={
|
||||
200: DynamicProductSerializer(context={'view_type': 'instance'}),
|
||||
404: OpenApiTypes.OBJECT,
|
||||
},
|
||||
)
|
||||
def get(self, request, slug):
|
||||
from django.db.models import F
|
||||
|
||||
# Optimize query with select_related and prefetch_related to avoid N+1 queries
|
||||
product = get_object_or_404(
|
||||
ProductModel.objects.select_related(
|
||||
@@ -147,10 +155,15 @@ class ProductView(APIView):
|
||||
'variants__details__detail_category',
|
||||
'related_products__variants__product_attributes',
|
||||
'related_products__category',
|
||||
'ratings',
|
||||
),
|
||||
slug=slug
|
||||
)
|
||||
|
||||
# Increment product view count atomically using F expressions to avoid race conditions
|
||||
ProductModel.objects.filter(id=product.id).update(view=F('view') + 1)
|
||||
product.view += 1 # Update in-memory instance for response
|
||||
|
||||
if request.user.is_authenticated:
|
||||
cart_obj, _ = Cart.objects.get_or_create(user=request.user)
|
||||
# Optimize cart items query - prefetch all related data
|
||||
@@ -662,3 +675,51 @@ class BotCategoryView(APIView):
|
||||
"success": False,
|
||||
"categories": []
|
||||
})
|
||||
|
||||
|
||||
class ProductRatingView(APIView):
|
||||
"""
|
||||
API endpoint to submit/update a product rating
|
||||
POST: /api/products/<slug>/rating/
|
||||
"""
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
serializer_class = ProductRatingSerializer
|
||||
def post(self, request, slug):
|
||||
if not request.user.is_authenticated:
|
||||
return Response(
|
||||
{'detail': 'احراز هویت الزامی است'},
|
||||
status=status.HTTP_401_UNAUTHORIZED
|
||||
)
|
||||
|
||||
product = get_object_or_404(ProductModel, slug=slug)
|
||||
|
||||
# Check if user already rated this product
|
||||
existing_rating = ProductRating.objects.filter(
|
||||
product=product,
|
||||
user=request.user
|
||||
).exists()
|
||||
|
||||
if existing_rating:
|
||||
return Response(
|
||||
{'detail': 'شما قبلا این محصول را امتیاز دادید'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
serializer = ProductRatingSerializer(
|
||||
data=request.data,
|
||||
context={'product_id': product.id, 'request': request}
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
|
||||
# Invalidate cache for this product
|
||||
from django.core.cache import cache
|
||||
cache.delete(f'product_avg_rating_{product.id}')
|
||||
|
||||
return Response(
|
||||
{'detail': 'امتیاز شما با موفقیت ثبت شد'},
|
||||
status=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -124,14 +124,17 @@
|
||||
.info-flex {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 12px;
|
||||
gap: 6px 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
white-space: nowrap;
|
||||
font-size: 9px;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.info-item span:first-child {
|
||||
@@ -310,22 +313,22 @@
|
||||
<span>فرستنده :</span><span>فروشگاه هی ملز</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>شناسه ملی :</span><span>---</span>
|
||||
<span>شناسه ملی :</span><span>14014423086</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>شماره ثبت :</span><span>---</span>
|
||||
<span>شماره ثبت :</span><span>64852</span>
|
||||
</div>
|
||||
{% comment %} <div class="info-item">
|
||||
<span>شماره مجوز :</span><span>-</span>
|
||||
</div> {% endcomment %}
|
||||
<div class="info-item">
|
||||
<span>شماره مجوز :</span><span>---</span>
|
||||
<span>نشانی شرکت :</span><span>شیراز بلوار گلستان بلوار سما خیابان طهماسبی پلاک ۱۹۸</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>نشانی شرکت :</span><span>---</span>
|
||||
<div class="info-item" style="flex-basis: 100%;">
|
||||
<span>کد پستی :</span><span>7145746584</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>کد پستی :</span><span>---</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>تلفن و فکس :</span><span>---</span>
|
||||
<div class="info-item" style="flex-basis: 100%;">
|
||||
<span>تلفن و فکس :</span><span>02193111026</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -492,6 +495,10 @@
|
||||
<div class="footer">
|
||||
<p>با تشکر از خرید شما</p>
|
||||
<p>این فاکتور به صورت الکترونیکی صادر شده و نیازی به امضا و مهر ندارد</p>
|
||||
<p>«هرگونه عودت یا ارسال کالا، صرفاً پس از هماهنگی با واحد پشتیبانی شرکت امکانپذیر است.»</p>
|
||||
<p>«مشتری موظف است پیش از هرگونه مرجوعی یا ارسال، با پشتیبانی شرکت هماهنگی لازم را انجام دهد.»</p>
|
||||
<p>«عودت، تعویض یا ارسال کالا بدون هماهنگی قبلی با شرکت مورد پذیرش نخواهد بود.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -371,25 +371,22 @@
|
||||
<div class="info-panel">
|
||||
<div class="info-flex">
|
||||
<div class="info-item">
|
||||
<span>فرستنده :</span><span>{{ shop.shop_name }}</span>
|
||||
<span>فرستنده :</span><span>فروشگاه هی ملز</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>شناسه ملی :</span><span>---</span>
|
||||
<span>شناسه ملی :</span><span>14014423086</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>شماره ثبت :</span><span>---</span>
|
||||
<span>شماره ثبت :</span><span>64852</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>شماره مجوز :</span><span>---</span>
|
||||
<span>نشانی شرکت :</span><span>شیراز بلوار گلستان بلوار سما خیابان طهماسبی پلاک ۱۹۸</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>نشانی شرکت :</span><span>---</span>
|
||||
<div class="info-item" style="flex-basis: 100%;">
|
||||
<span>کد پستی :</span><span>7145746584</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>کد پستی :</span><span>---</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span>تلفن و فکس :</span><span>---</span>
|
||||
<div class="info-item" style="flex-basis: 100%;">
|
||||
<span>تلفن و فکس :</span><span>02193111026</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -324,8 +324,8 @@ watch(
|
||||
:to="{ name: nextPage?.name, query: { gw: nextPage?.query } }"
|
||||
>
|
||||
<Button
|
||||
start-icon="arrow-right"
|
||||
class="w-full rounded-100"
|
||||
start-icon="ci:arrow-right"
|
||||
class="w-full rounded-100 py-2 lg:typo-p-sm"
|
||||
variant="primary"
|
||||
>
|
||||
{{ nextPage?.label }}
|
||||
|
||||
@@ -59,7 +59,7 @@ const handleDeleteFromCart = () => {
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -106,9 +106,9 @@ watch(
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -117,8 +117,12 @@ watch(
|
||||
class="flex flex-col items-center w-full gap-4 p-4 border lg:flex-row border-slate-200 rounded-xl bg-slate-50 overflow-hidden relative"
|
||||
>
|
||||
<div class="flex items-start justify-start w-full gap-2.5 lg:gap-4">
|
||||
<Skeleton
|
||||
v-if="cartImageIsLoading"
|
||||
class="size-[4rem] lg:!size-[12rem] aspect-square shrink-0 !rounded-xl border border-slate-200"
|
||||
/>
|
||||
<div
|
||||
v-if="!cartImageIsLoading"
|
||||
v-else
|
||||
class="size-[4rem] lg:size-[12rem] aspect-square shrink-0 rounded-xl border border-slate-200 overflow-hidden"
|
||||
>
|
||||
<NuxtImg
|
||||
@@ -129,10 +133,6 @@ watch(
|
||||
alt="product"
|
||||
/>
|
||||
</div>
|
||||
<Skeleton
|
||||
v-else
|
||||
class="!size-[12rem] aspect-square shrink-0 !rounded-xl border border-slate-200"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col w-full gap-3 lg:gap-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
@@ -142,7 +142,7 @@ watch(
|
||||
<div class="w-max flex-center gap-2">
|
||||
<div
|
||||
v-if="data.discount > 0"
|
||||
class="text-white bg-blue-500 px-3 lg:px-4 py-1.5 lg:py-2 text-[10px] lg:text-xs rounded-full flex items-center gap-1"
|
||||
class="text-white whitespace-pre bg-blue-500 px-3 lg:px-4 py-1.5 lg:py-2 text-[8px] lg:text-xs rounded-full flex items-center gap-1"
|
||||
>
|
||||
{{ data.discount_amount }}
|
||||
تخفیف
|
||||
@@ -164,7 +164,7 @@ watch(
|
||||
{{ data.product.title }}
|
||||
</NuxtLink>
|
||||
|
||||
<div class="flex items-center justify-start gap-1.5">
|
||||
<div class="flex items-center justify-start flex-wrap gap-1.5">
|
||||
<div
|
||||
v-if="!!data.product.color"
|
||||
class="px-3 py-1 rounded-full border border-slate-200 text-xs lg:text-sm flex-center gap-1.5"
|
||||
@@ -182,7 +182,7 @@ watch(
|
||||
v-if="data.product.product_attributes.length > 0"
|
||||
v-for="(variant, index) in data.product.product_attributes"
|
||||
:index="index"
|
||||
class="px-3 py-1 rounded-full border border-slate-200 text-xs lg:text-sm"
|
||||
class="px-3 py-1 whitespace-pre rounded-full border border-slate-200 text-xs lg:text-sm"
|
||||
>
|
||||
{{ variant.value }}
|
||||
</span>
|
||||
@@ -305,7 +305,7 @@ watch(
|
||||
<div class="flex flex-col items-end">
|
||||
<span
|
||||
v-if="data.discount > 0"
|
||||
class="typo-p-xs relative flex-center w-fit line-through text-slate-400"
|
||||
class="text-[10px] lg:typo-p-xs relative flex-center w-fit line-through text-slate-400"
|
||||
>
|
||||
{{ data.price }}
|
||||
</span>
|
||||
@@ -315,7 +315,7 @@ watch(
|
||||
>
|
||||
تخفیف ویژه: {{ data.special_discount_amount }}
|
||||
</span>
|
||||
<span class="typo-p-md relative flex-center w-fit font-medium">
|
||||
<span class="typo-p-sm lg:typo-p-md relative flex-center w-fit font-medium">
|
||||
{{ data.final_price }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -54,8 +54,7 @@ const handleSubmit = () => {
|
||||
>
|
||||
<template #trigger>
|
||||
<Button
|
||||
class="rounded-full shrink-0 whitespace-pre"
|
||||
end-icon="ci:bi-trash"
|
||||
class="rounded-full shrink-0 whitespace-pre max-lg:text-[10px]! max-lg:py-1"
|
||||
size="md"
|
||||
>
|
||||
حذف همه
|
||||
|
||||
@@ -50,50 +50,44 @@ const isShow = computed({
|
||||
class="bg-black/50 backdrop-blur-sm data-[state=open]:animate-overlay-show fixed inset-0 z-999"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="fixed inset-0 w-full h-svh z-9999 flex-center"
|
||||
v-if="isShow"
|
||||
<DialogContent
|
||||
class="fixed inset-0 z-9999 flex items-start justify-center overflow-y-auto overscroll-contain data-[state=open]:animate-content-show focus:outline-none"
|
||||
style="touch-action: pan-y"
|
||||
>
|
||||
<div
|
||||
:class="contectClass"
|
||||
class="overflow-y-auto max-h-svh absolute left-[50%] py-10 w-fit max-w-[50rem] translate-x-[-50%]"
|
||||
class="relative w-fit max-w-[50rem] my-auto py-10"
|
||||
>
|
||||
<DialogContent
|
||||
class="data-[state=open]:animate-content-show text-black font-iran-yekan-x focus:outline-none z-[100]"
|
||||
<div
|
||||
class="rounded-2xl bg-white text-black font-iran-yekan-x shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px]"
|
||||
>
|
||||
<div
|
||||
class="rounded-2xl bg-white shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px]"
|
||||
class="w-full flex items-center px-6 justify-between py-[1.5rem] border-b border-slate-200"
|
||||
>
|
||||
<div
|
||||
class="w-full flex items-center px-6 justify-between py-[1.5rem] border-b border-slate-200"
|
||||
<DialogClose
|
||||
class="inline-flex size-8 items-center justify-center transition-all rounded-full bg-gray-50 border border-slate-200 hover:border-black focus:outline-none"
|
||||
aria-label="Close"
|
||||
>
|
||||
<DialogClose
|
||||
class="inline-flex size-8 items-center justify-center transition-all rounded-full bg-gray-50 border border-slate-200 hover:border-black focus:outline-none"
|
||||
aria-label="Close"
|
||||
>
|
||||
<Icon
|
||||
name="ci:bi-x-lg"
|
||||
class="**:fill-black"
|
||||
/>
|
||||
</DialogClose>
|
||||
<DialogTitle
|
||||
class="typo-sub-h-md lg:typo-sub-h-xl font-semibold flex items-center gap-3"
|
||||
>
|
||||
{{ title }}
|
||||
<Icon
|
||||
v-if="!!icon"
|
||||
:name="icon"
|
||||
:size="iconSize"
|
||||
/>
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<div class="w-full px-6">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
<Icon
|
||||
name="ci:bi-x-lg"
|
||||
class="**:fill-black"
|
||||
/>
|
||||
</DialogClose>
|
||||
<DialogTitle class="typo-sub-h-md lg:typo-sub-h-xl font-semibold flex items-center gap-3">
|
||||
{{ title }}
|
||||
<Icon
|
||||
v-if="!!icon"
|
||||
:name="icon"
|
||||
:size="iconSize"
|
||||
/>
|
||||
</DialogTitle>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<div class="w-full px-6">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
</template>
|
||||
|
||||
@@ -124,7 +124,7 @@ const resetAvatarFile = async () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex-center gap-4 max-w-[500px] flex-wrap">
|
||||
<!-- <div class="w-full flex-center gap-4 max-w-[500px] flex-wrap">
|
||||
<button
|
||||
class="size-6 lg:size-8 rounded-full bg-orange-100 whitespace-nowrap ring-2 hover:ring-black ring-slate-200 ring-offset-3"
|
||||
/>
|
||||
@@ -140,7 +140,7 @@ const resetAvatarFile = async () => {
|
||||
<button
|
||||
class="size-6 lg:size-8 rounded-full bg-amber-800 whitespace-nowrap ring-2 hover:ring-black ring-slate-200 ring-offset-3"
|
||||
/>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="w-full flex-col-center gap-5">
|
||||
<Avatar
|
||||
|
||||
@@ -40,7 +40,7 @@ const hasCartItem = computed(() => !!cart.value && cart.value.items.length! > 0)
|
||||
class="flex items-center gap-2 text-sm lg:text-[1rem] font-medium"
|
||||
>
|
||||
<Icon
|
||||
name="ci:bi-arrow-right"
|
||||
name="ci:arrow-right"
|
||||
class="**:stroke-blue-500"
|
||||
/>
|
||||
<span class="text-blue-500">
|
||||
@@ -49,7 +49,7 @@ const hasCartItem = computed(() => !!cart.value && cart.value.items.length! > 0)
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<h1 class="w-full text-center lg:w-6/12 typo-h-5 lg:typo-h-4">
|
||||
<h1 v-if="pageTitle" class="w-full text-center lg:w-6/12 typo-h-5 lg:typo-h-4">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ const hasCartItem = computed(() => !!cart.value && cart.value.items.length! > 0)
|
||||
<div class="w-full flex flex-col gap-4 lg:gap-6">
|
||||
<div
|
||||
v-if="hasCartItem"
|
||||
class="flex items-center justify-between w-full gap-3 px-4 py-4 rounded-xl bg-slate-50 border border-slate-200"
|
||||
class="flex items-center justify-between w-full gap-3 px-4 py-2 lg:py-4 rounded-xl bg-slate-50 border border-slate-200"
|
||||
>
|
||||
<Skeleton
|
||||
v-if="cartIsLoading"
|
||||
|
||||
Reference in New Issue
Block a user