Merge branch 'main' of https://github.com/Byeto-Company/hossein_por_shop
This commit is contained in:
+13
-1
@@ -28,4 +28,16 @@ SMS_API_KEY = ''
|
|||||||
|
|
||||||
VAPID_PRIVATE_KEY = 'NajogmGTsGsZ_dfURrjUpgsm5fui-s5AzruBQgMh_I4'
|
VAPID_PRIVATE_KEY = 'NajogmGTsGsZ_dfURrjUpgsm5fui-s5AzruBQgMh_I4'
|
||||||
|
|
||||||
OPENAI_API_KEY = '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'
|
OPENAI_API_KEY = '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'
|
||||||
|
|
||||||
|
|
||||||
|
# DATABASES = {
|
||||||
|
# 'default': {
|
||||||
|
# 'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
# 'NAME': 'hshop',
|
||||||
|
# 'USER': 'byeto',
|
||||||
|
# 'PASSWORD': 'vuhbyq-cypMu0-sirbon',
|
||||||
|
# 'HOST': '185.110.189.208',
|
||||||
|
# 'PORT': '5434',
|
||||||
|
# }
|
||||||
|
# }
|
||||||
@@ -239,6 +239,12 @@ UNFOLD = {
|
|||||||
"badge": "utils.admin.new_ticket_count",
|
"badge": "utils.admin.new_ticket_count",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": _("ارتباط با ما"),
|
||||||
|
"icon": "perm_phone_msg",
|
||||||
|
"link": reverse_lazy("admin:ticket_contactusmodel_changelist"),
|
||||||
|
"badge": "utils.admin.new_contact_us_count",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -262,6 +268,11 @@ UNFOLD = {
|
|||||||
"icon": "photo_prints",
|
"icon": "photo_prints",
|
||||||
"link": reverse_lazy("admin:product_productvariant_changelist"),
|
"link": reverse_lazy("admin:product_productvariant_changelist"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": _("بخش جزيیات محصول"),
|
||||||
|
"icon": "subject",
|
||||||
|
"link": reverse_lazy("admin:product_productdetailmodel_changelist"),
|
||||||
|
},
|
||||||
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.views.generic import RedirectView, TemplateView
|
from django.views.generic import RedirectView, TemplateView
|
||||||
from unfold.views import UnfoldModelAdminViewMixin
|
from unfold.views import UnfoldModelAdminViewMixin
|
||||||
from order.models import OrderModel
|
from order.models import OrderModel
|
||||||
from ticket.models import Ticket
|
from ticket.models import Ticket, ContactUsModel
|
||||||
from account.models import SecurityBreachAttemptModel
|
from account.models import SecurityBreachAttemptModel
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@@ -19,9 +19,11 @@ def dashboard_callback(request, context):
|
|||||||
|
|
||||||
pending_count = OrderModel.objects.filter(status='ADMIN_PENDING').count()
|
pending_count = OrderModel.objects.filter(status='ADMIN_PENDING').count()
|
||||||
open_tickets_count = Ticket.objects.filter(status__in=['open', 'in_progress']).count()
|
open_tickets_count = Ticket.objects.filter(status__in=['open', 'in_progress']).count()
|
||||||
|
open_contact_us_count = ContactUsModel.objects.filter(is_reviewed=False).count()
|
||||||
context.update(random_data())
|
context.update(random_data())
|
||||||
context.update({'pending_count': pending_count})
|
context.update({'pending_count': pending_count})
|
||||||
context.update({'open_tickets_count': open_tickets_count})
|
context.update({'open_tickets_count': open_tickets_count})
|
||||||
|
context.update({'open_contact_us_count': open_contact_us_count})
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
|
# from product.tasks import update_prices
|
||||||
from .models import *
|
from .models import *
|
||||||
from unfold.admin import TabularInline, StackedInline
|
from unfold.admin import TabularInline, StackedInline
|
||||||
from home.models import LearnVideoModel
|
from home.models import LearnVideoModel
|
||||||
@@ -122,7 +123,7 @@ class DetailModelAdmin(ModelAdmin, ImportExportModelAdmin):
|
|||||||
class ProductDetailModel1Admin(ModelAdmin, ImportExportModelAdmin):
|
class ProductDetailModel1Admin(ModelAdmin, ImportExportModelAdmin):
|
||||||
import_form_class = ImportForm
|
import_form_class = ImportForm
|
||||||
export_form_class = ExportForm
|
export_form_class = ExportForm
|
||||||
search_fields = ['detail_category__title']
|
search_fields = ['detail_category__title', 'name']
|
||||||
compressed_fields = True
|
compressed_fields = True
|
||||||
warn_unsaved_form = True
|
warn_unsaved_form = True
|
||||||
|
|
||||||
@@ -216,8 +217,7 @@ class ProductModelAdmin(ModelAdmin, ImportExportModelAdmin):
|
|||||||
|
|
||||||
@action(description=f"اپدیت قیمت ها")
|
@action(description=f"اپدیت قیمت ها")
|
||||||
def update_products_price(self, request):
|
def update_products_price(self, request):
|
||||||
print('from the button')
|
# update_prices()
|
||||||
ProductVariant.update_all_prices()
|
|
||||||
messages.success(request, f"قیمت {ProductVariant.objects.all().count()} تنوع محصول اپدیت شد")
|
messages.success(request, f"قیمت {ProductVariant.objects.all().count()} تنوع محصول اپدیت شد")
|
||||||
return redirect("admin:product_productmodel_changelist")
|
return redirect("admin:product_productmodel_changelist")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 5.1.2 on 2025-04-21 23:30
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('product', '0035_alter_attributetype_options_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='productdetailmodel',
|
||||||
|
name='detail_category',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='product.productdetailcategory', verbose_name='دسته بندی جزيات'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.1.2 on 2025-04-21 23:37
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('product', '0036_alter_productdetailmodel_detail_category'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='productdetailmodel',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(default=1, help_text='این متن فقط برای راحتی در استفاده از پنل ادمین میباشد', max_length=50, verbose_name='نام جزيیات'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -4,6 +4,7 @@ from account.models import User
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
import requests
|
import requests
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
class MainCategoryModel(models.Model):
|
class MainCategoryModel(models.Model):
|
||||||
name = models.CharField(max_length=50, verbose_name='نام دسته بندی')
|
name = models.CharField(max_length=50, verbose_name='نام دسته بندی')
|
||||||
@@ -212,14 +213,15 @@ class ProductImageModel(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class ProductDetailModel(models.Model):
|
class ProductDetailModel(models.Model):
|
||||||
detail_category = models.ForeignKey(ProductDetailCategory, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات', blank=True, null=True)
|
name = models.CharField(max_length=50, verbose_name='نام جزيیات', help_text='این متن فقط برای راحتی در استفاده از پنل ادمین میباشد')
|
||||||
|
detail_category = models.ForeignKey(ProductDetailCategory, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات')
|
||||||
detail = models.ManyToManyField(DetailModel, verbose_name='جزيات ها')
|
detail = models.ManyToManyField(DetailModel, verbose_name='جزيات ها')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'جزیات محصول'
|
verbose_name = 'جزیات محصول'
|
||||||
verbose_name_plural = 'جزیات محصول ها'
|
verbose_name_plural = 'جزیات محصول ها'
|
||||||
# def __str__(self):
|
def __str__(self):
|
||||||
# return f'جزيیات محصول {self.product}'
|
return f'جزيیات محصول {self.detail_category.title} - {self.name}'
|
||||||
|
|
||||||
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='محصول')
|
||||||
@@ -272,21 +274,3 @@ class ProductVariant(models.Model):
|
|||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
self.set_or_update_price()
|
self.set_or_update_price()
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def get_toman_price_after_discount(self):
|
|
||||||
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()
|
|
||||||
@@ -39,4 +39,25 @@
|
|||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if open_contact_us_count%}
|
||||||
|
|
||||||
|
|
||||||
|
<div dir='rtl' class="bg-base-50 border border-base-200 border-dashed flex flex-col gap-4 p-4 rounded dark:bg-white/[.02] dark:border-base-700 lg:flex-row lg:justify-between w-full shrink-0 lg:items-center" style="justify-content: space-between;">
|
||||||
|
<div class="flex flex-col lg:flex-row lg:items-center">
|
||||||
|
<h2 class="font-semibold text-font-important-light text-base dark:text-font-important-dark flex items-center">
|
||||||
|
<span class="material-symbols-outlined md-18 mr-3 w-4.5 align-middle">confirmation_number</span>
|
||||||
|
<span class="align-middle" style="margin-right: 8px;">ارتباط با ما جدید داری</span>
|
||||||
|
<span class="text-white bg-primary-800 py-1 px-2 rounded" style="margin-right: 8px;">{{ open_contact_us_count }} </span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex lg:flex-row lg:items-center">
|
||||||
|
{% component "unfold/components/flex.html" with class="flex-col gap-4 lg:flex-row" %}
|
||||||
|
{% component "unfold/components/button.html" with href="/secret-admin/ticket/contactusmodel/" %}
|
||||||
|
نمایش ارتباط با ما ها
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endcomponent %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ class MessageInline(TabularInline):
|
|||||||
model = Message
|
model = Message
|
||||||
extra = 1
|
extra = 1
|
||||||
|
|
||||||
|
@admin.register(ContactUsModel)
|
||||||
|
class ContactUsAdmin(ModelAdmin):
|
||||||
|
list_filter = ['type', 'is_reviewed']
|
||||||
|
list_display = ['full_name', 'phone', 'email', 'message', 'is_reviewed']
|
||||||
|
compressed_fields = True
|
||||||
|
warn_unsaved_form = True
|
||||||
|
readonly_fields = ['full_name', 'email', 'phone', 'type', 'message', ]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Ticket)
|
@admin.register(Ticket)
|
||||||
class TicketAdmin(ModelAdmin, ImportExportModelAdmin):
|
class TicketAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||||
import_form_class = ImportForm
|
import_form_class = ImportForm
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 5.1.2 on 2025-04-21 22:52
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ticket', '0019_alter_attachment_options'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ContactUsModel',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('full_name', models.CharField(max_length=50, verbose_name='نام و نام خانوادگی')),
|
||||||
|
('email', models.EmailField(max_length=254, verbose_name='ایمیل')),
|
||||||
|
('phone', models.CharField(max_length=30)),
|
||||||
|
('type', models.CharField(choices=[('ORDER', 'پیگیری سفارش'), ('SUGGESTION', 'پیشنهادات'), ('COMPLAINT', 'انتقادات')], max_length=20, verbose_name='نوع')),
|
||||||
|
('message', models.TextField(verbose_name='پیام')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.1.2 on 2025-04-21 22:53
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ticket', '0020_contactusmodel'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='contactusmodel',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('ORDER_FOLLOW_UP', 'پیگیری سفارش'), ('SUGGESTION', 'پیشنهادات'), ('COMPLAINT', 'انتقادات')], max_length=20, verbose_name='نوع'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 5.1.2 on 2025-04-21 22:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ticket', '0021_alter_contactusmodel_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='contactusmodel',
|
||||||
|
options={'verbose_name': 'ارتباط با ما', 'verbose_name_plural': 'ارتباط با ما ها '},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='contactusmodel',
|
||||||
|
name='is_reviewed',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.1.2 on 2025-04-21 23:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ticket', '0022_alter_contactusmodel_options_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='contactusmodel',
|
||||||
|
name='is_reviewed',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='بررسی شده'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='contactusmodel',
|
||||||
|
name='phone',
|
||||||
|
field=models.CharField(max_length=30, verbose_name='شماره تماس'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -73,4 +73,26 @@ class Message(models.Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'پیام تیکت'
|
verbose_name = 'پیام تیکت'
|
||||||
verbose_name_plural = 'پیام های تیکت'
|
verbose_name_plural = 'پیام های تیکت'
|
||||||
|
|
||||||
|
|
||||||
|
class ContactUsModel(models.Model):
|
||||||
|
full_name = models.CharField(max_length=50, verbose_name='نام و نام خانوادگی')
|
||||||
|
email = models.EmailField(max_length=254, verbose_name='ایمیل')
|
||||||
|
phone = models.CharField(max_length=30, verbose_name='شماره تماس')
|
||||||
|
FEEDBACK_TYPES = [
|
||||||
|
('ORDER_FOLLOW_UP', 'پیگیری سفارش'),
|
||||||
|
('SUGGESTION', 'پیشنهادات'),
|
||||||
|
('COMPLAINT', 'انتقادات'),
|
||||||
|
]
|
||||||
|
|
||||||
|
type = models.CharField(max_length=20, choices=FEEDBACK_TYPES, verbose_name='نوع')
|
||||||
|
message = models.TextField(verbose_name='پیام')
|
||||||
|
is_reviewed = models.BooleanField(default=False, verbose_name='بررسی شده')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.full_name} - {self.message[:15]}...'
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'ارتباط با ما'
|
||||||
|
verbose_name_plural = 'ارتباط با ما ها '
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from .models import Ticket, Message, Attachment
|
from .models import Ticket, Message, Attachment, ContactUsModel
|
||||||
from django.utils.timezone import localtime
|
from django.utils.timezone import localtime
|
||||||
from order.serializers import OrderListSerializer
|
from order.serializers import OrderListSerializer
|
||||||
from order.serializers import OrderModel
|
from order.serializers import OrderModel
|
||||||
@@ -88,4 +88,10 @@ class TicketListSerializer(serializers.ModelSerializer):
|
|||||||
return obj.get_status_display()
|
return obj.get_status_display()
|
||||||
|
|
||||||
def get_ticket_category(self, obj):
|
def get_ticket_category(self, obj):
|
||||||
return obj.get_ticket_category_display()
|
return obj.get_ticket_category_display()
|
||||||
|
|
||||||
|
|
||||||
|
class ContactUsSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ContactUsModel
|
||||||
|
fields = ['full_name', 'email', 'phone', 'type', 'message']
|
||||||
@@ -6,6 +6,7 @@ urlpatterns = [
|
|||||||
path('', views.TicketListView.as_view(), name='ticket-list'),
|
path('', views.TicketListView.as_view(), name='ticket-list'),
|
||||||
path('<int:pk>', views.TicketDetailView.as_view(), name='ticket-detail'),
|
path('<int:pk>', views.TicketDetailView.as_view(), name='ticket-detail'),
|
||||||
path('message/create', views.MessageCreateView.as_view(), name='message-create'),
|
path('message/create', views.MessageCreateView.as_view(), name='message-create'),
|
||||||
|
path('contact-us/create', views.CreateContactUsView.as_view(), name='contact-us-create'),
|
||||||
path('attachment/create', views.AttachmentUploadView.as_view(), name='attachment-upload'),
|
path('attachment/create', views.AttachmentUploadView.as_view(), name='attachment-upload'),
|
||||||
path('attachment/delete/<int:pk>', views.AttachmentDeleteView.as_view(), name='attachment-upload'),
|
path('attachment/delete/<int:pk>', views.AttachmentDeleteView.as_view(), name='attachment-upload'),
|
||||||
]
|
]
|
||||||
+14
-3
@@ -2,7 +2,7 @@ from rest_framework import generics, permissions
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from .models import Ticket, Message, Attachment
|
from .models import Ticket, Message, Attachment
|
||||||
from .serializers import TicketListSerializer, MessageSerializer, TicketSerializer, AttachmentSerializer
|
from .serializers import TicketListSerializer, MessageSerializer, TicketSerializer, AttachmentSerializer, ContactUsSerializer
|
||||||
from utils.pagination import StructurePagination
|
from utils.pagination import StructurePagination
|
||||||
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
|
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
@@ -56,7 +56,6 @@ class TicketCreateView(APIView):
|
|||||||
permission_classes = [permissions.IsAuthenticated]
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
new_ticket_ser = self.serializer_class(data=request.data, context={'request': request})
|
new_ticket_ser = self.serializer_class(data=request.data, context={'request': request})
|
||||||
message = request.data.get('message', None)
|
|
||||||
if new_ticket_ser.is_valid():
|
if new_ticket_ser.is_valid():
|
||||||
new_ticket_ser.save(customer=request.user)
|
new_ticket_ser.save(customer=request.user)
|
||||||
return Response(new_ticket_ser.data, status=status.HTTP_201_CREATED)
|
return Response(new_ticket_ser.data, status=status.HTTP_201_CREATED)
|
||||||
@@ -159,4 +158,16 @@ class UpdateTicketStatusView(APIView):
|
|||||||
return Response({"error": "Invalid status"}, status=400)
|
return Response({"error": "Invalid status"}, status=400)
|
||||||
ticket.status = new_status
|
ticket.status = new_status
|
||||||
ticket.save()
|
ticket.save()
|
||||||
return Response({"message": "Ticket status updated successfully"})
|
return Response({"message": "Ticket status updated successfully"})
|
||||||
|
|
||||||
|
|
||||||
|
class CreateContactUsView(APIView):
|
||||||
|
serializer_class = ContactUsSerializer
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
def post(self, request):
|
||||||
|
contact_us_ser = self.serializer_class(data=request.data, context={'request': request})
|
||||||
|
if contact_us_ser.is_valid():
|
||||||
|
contact_us_ser.save()
|
||||||
|
return Response(contact_us_ser.data, status=status.HTTP_201_CREATED)
|
||||||
|
else:
|
||||||
|
return Response(contact_us_ser.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from order.models import OrderModel
|
from order.models import OrderModel
|
||||||
from product.models import DollorModel, CommentModel
|
from product.models import DollorModel, CommentModel
|
||||||
from ticket.models import Ticket
|
from ticket.models import Ticket, ContactUsModel
|
||||||
from home.models import LearnVideoModel
|
from home.models import LearnVideoModel
|
||||||
from account.models import SecurityBreachAttemptModel
|
from account.models import SecurityBreachAttemptModel
|
||||||
|
|
||||||
@@ -24,6 +24,9 @@ def new_learn_video_count(request):
|
|||||||
def new_attck_count(request):
|
def new_attck_count(request):
|
||||||
return SecurityBreachAttemptModel.objects.filter(viewd=False).count()
|
return SecurityBreachAttemptModel.objects.filter(viewd=False).count()
|
||||||
|
|
||||||
|
def new_contact_us_count(request):
|
||||||
|
return ContactUsModel.objects.filter(is_reviewed=False).count()
|
||||||
|
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from unfold.admin import ModelAdmin
|
from unfold.admin import ModelAdmin
|
||||||
from home.models import LearnVideoModel
|
from home.models import LearnVideoModel
|
||||||
|
|||||||
@@ -150,9 +150,12 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="font-semibold typo-sub-h-sm lg:typo-sub-h-xl text-black">
|
<NuxtLink
|
||||||
|
:to="`product/${data.product.id}`"
|
||||||
|
class="font-semibold typo-sub-h-sm lg:typo-sub-h-xl text-black underline underline-offset-2"
|
||||||
|
>
|
||||||
{{ data.product.title }}
|
{{ data.product.title }}
|
||||||
</span>
|
</NuxtLink>
|
||||||
|
|
||||||
<div class="flex items-center justify-start gap-1.5">
|
<div class="flex items-center justify-start gap-1.5">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -12,11 +12,9 @@ const {} = toRefs(props);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative w-full flex flex-col justify-center min-h-[450px] h-svh">
|
<div class="relative w-full flex flex-col justify-center py-32 lg:py-48">
|
||||||
<div class="flex-col-center gap-6 mb-24 sm:mb-32 container">
|
<div class="flex-col-center gap-6 mb-24 sm:mb-32 container">
|
||||||
<span class="typo-h-6 max-sm:text-xl 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>
|
|
||||||
<p class="text-slate-500 text-center max-w-[750px] typo-p-sm md:typo-p-lg xl:typo-p-xl">
|
<p class="text-slate-500 text-center max-w-[750px] typo-p-sm md:typo-p-lg xl:typo-p-xl">
|
||||||
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و
|
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و
|
||||||
متون بلکه روزنامه و مجله در ستون و سطرآنچنان که
|
متون بلکه روزنامه و مجله در ستون و سطرآنچنان که
|
||||||
@@ -30,13 +28,19 @@ const {} = toRefs(props);
|
|||||||
<div class="text-[30px] lg:text-[40px] text-white whitespace-nowrap font-semibold opacity-85">
|
<div class="text-[30px] lg:text-[40px] text-white whitespace-nowrap font-semibold opacity-85">
|
||||||
HEYMLZ
|
HEYMLZ
|
||||||
</div>
|
</div>
|
||||||
<NuxtImg src="/img/heymlz/heymlz-logo.png" class="h-[25px] sm:h-[45px] invert opacity-85" />
|
<NuxtImg
|
||||||
|
src="/img/heymlz/heymlz-logo.png"
|
||||||
|
class="h-[25px] sm:h-[45px] invert opacity-85"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-for="i in 10">
|
<template v-for="i in 10">
|
||||||
<div class="text-[30px] lg:text-[40px] text-white whitespace-nowrap font-semibold opacity-85">
|
<div class="text-[30px] lg:text-[40px] text-white whitespace-nowrap font-semibold opacity-85">
|
||||||
HEYMLZ
|
HEYMLZ
|
||||||
</div>
|
</div>
|
||||||
<NuxtImg src="/img/heymlz/heymlz-logo.png" class="h-[25px] sm:h-[45px] invert opacity-85" />
|
<NuxtImg
|
||||||
|
src="/img/heymlz/heymlz-logo.png"
|
||||||
|
class="h-[25px] sm:h-[45px] invert opacity-85"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,32 +50,104 @@ const {} = toRefs(props);
|
|||||||
class="bg-slate-100/70 flex items-center pr-20 gap-12 sm:gap-20 w-max animate-marquee h-[90px] sm:h-[140px]"
|
class="bg-slate-100/70 flex items-center pr-20 gap-12 sm:gap-20 w-max animate-marquee h-[90px] sm:h-[140px]"
|
||||||
>
|
>
|
||||||
<template v-for="i in 1">
|
<template v-for="i in 1">
|
||||||
<NuxtImg src="/img/brands/brand-1.png" class="h-[25px] sm:h-[45px]" />
|
<NuxtImg
|
||||||
<NuxtImg src="/img/brands/brand-2.png" class="h-[25px] sm:h-[45px]" />
|
src="/img/brands/brand-1.png"
|
||||||
<NuxtImg src="/img/brands/brand-3.png" class="h-[25px] sm:h-[45px]" />
|
class="h-[25px] sm:h-[45px]"
|
||||||
<NuxtImg src="/img/brands/brand-4.png" class="h-[25px] sm:h-[45px]" />
|
/>
|
||||||
<NuxtImg src="/img/brands/brand-5.png" class="h-[25px] sm:h-[45px]" />
|
<NuxtImg
|
||||||
<NuxtImg src="/img/brands/brand-6.png" class="h-[25px] sm:h-[45px]" />
|
src="/img/brands/brand-2.png"
|
||||||
<NuxtImg src="/img/brands/brand-1.png" class="h-[25px] sm:h-[45px]" />
|
class="h-[25px] sm:h-[45px]"
|
||||||
<NuxtImg src="/img/brands/brand-2.png" class="h-[25px] sm:h-[45px]" />
|
/>
|
||||||
<NuxtImg src="/img/brands/brand-3.png" class="h-[25px] sm:h-[45px]" />
|
<NuxtImg
|
||||||
<NuxtImg src="/img/brands/brand-4.png" class="h-[25px] sm:h-[45px]" />
|
src="/img/brands/brand-3.png"
|
||||||
<NuxtImg src="/img/brands/brand-5.png" class="h-[25px] sm:h-[45px]" />
|
class="h-[25px] sm:h-[45px]"
|
||||||
<NuxtImg src="/img/brands/brand-6.png" class="h-[25px] sm:h-[45px]" />
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
src="/img/brands/brand-4.png"
|
||||||
|
class="h-[25px] sm:h-[45px]"
|
||||||
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
src="/img/brands/brand-5.png"
|
||||||
|
class="h-[25px] sm:h-[45px]"
|
||||||
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
src="/img/brands/brand-6.png"
|
||||||
|
class="h-[25px] sm:h-[45px]"
|
||||||
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
src="/img/brands/brand-1.png"
|
||||||
|
class="h-[25px] sm:h-[45px]"
|
||||||
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
src="/img/brands/brand-2.png"
|
||||||
|
class="h-[25px] sm:h-[45px]"
|
||||||
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
src="/img/brands/brand-3.png"
|
||||||
|
class="h-[25px] sm:h-[45px]"
|
||||||
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
src="/img/brands/brand-4.png"
|
||||||
|
class="h-[25px] sm:h-[45px]"
|
||||||
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
src="/img/brands/brand-5.png"
|
||||||
|
class="h-[25px] sm:h-[45px]"
|
||||||
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
src="/img/brands/brand-6.png"
|
||||||
|
class="h-[25px] sm:h-[45px]"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-for="i in 1">
|
<template v-for="i in 1">
|
||||||
<NuxtImg src="/img/brands/brand-1.png" class="h-[25px] sm:h-[45px]" />
|
<NuxtImg
|
||||||
<NuxtImg src="/img/brands/brand-2.png" class="h-[25px] sm:h-[45px]" />
|
src="/img/brands/brand-1.png"
|
||||||
<NuxtImg src="/img/brands/brand-3.png" class="h-[25px] sm:h-[45px]" />
|
class="h-[25px] sm:h-[45px]"
|
||||||
<NuxtImg src="/img/brands/brand-4.png" class="h-[25px] sm:h-[45px]" />
|
/>
|
||||||
<NuxtImg src="/img/brands/brand-5.png" class="h-[25px] sm:h-[45px]" />
|
<NuxtImg
|
||||||
<NuxtImg src="/img/brands/brand-6.png" class="h-[25px] sm:h-[45px]" />
|
src="/img/brands/brand-2.png"
|
||||||
<NuxtImg src="/img/brands/brand-1.png" class="h-[25px] sm:h-[45px]" />
|
class="h-[25px] sm:h-[45px]"
|
||||||
<NuxtImg src="/img/brands/brand-2.png" class="h-[25px] sm:h-[45px]" />
|
/>
|
||||||
<NuxtImg src="/img/brands/brand-3.png" class="h-[25px] sm:h-[45px]" />
|
<NuxtImg
|
||||||
<NuxtImg src="/img/brands/brand-4.png" class="h-[25px] sm:h-[45px]" />
|
src="/img/brands/brand-3.png"
|
||||||
<NuxtImg src="/img/brands/brand-5.png" class="h-[25px] sm:h-[45px]" />
|
class="h-[25px] sm:h-[45px]"
|
||||||
<NuxtImg src="/img/brands/brand-6.png" class="h-[25px] sm:h-[45px]" />
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
src="/img/brands/brand-4.png"
|
||||||
|
class="h-[25px] sm:h-[45px]"
|
||||||
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
src="/img/brands/brand-5.png"
|
||||||
|
class="h-[25px] sm:h-[45px]"
|
||||||
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
src="/img/brands/brand-6.png"
|
||||||
|
class="h-[25px] sm:h-[45px]"
|
||||||
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
src="/img/brands/brand-1.png"
|
||||||
|
class="h-[25px] sm:h-[45px]"
|
||||||
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
src="/img/brands/brand-2.png"
|
||||||
|
class="h-[25px] sm:h-[45px]"
|
||||||
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
src="/img/brands/brand-3.png"
|
||||||
|
class="h-[25px] sm:h-[45px]"
|
||||||
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
src="/img/brands/brand-4.png"
|
||||||
|
class="h-[25px] sm:h-[45px]"
|
||||||
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
src="/img/brands/brand-5.png"
|
||||||
|
class="h-[25px] sm:h-[45px]"
|
||||||
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
src="/img/brands/brand-6.png"
|
||||||
|
class="h-[25px] sm:h-[45px]"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,10 +18,16 @@ withDefaults(defineProps<Props>(), {
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full flex flex-col gap-2">
|
<div class="w-full flex flex-col gap-2">
|
||||||
<div class="flex items-center gap-1 ps-2">
|
<div class="flex items-center gap-1 ps-2">
|
||||||
<label :for="id" class="typo-label-xs lg:typo-label-sm">{{
|
<label
|
||||||
label
|
:for="id"
|
||||||
}}</label>
|
class="typo-label-xs lg:typo-label-sm"
|
||||||
<span v-if="!!required && required" class="text-danger-600">*</span>
|
>{{ label }}</label
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="!!required && required"
|
||||||
|
class="text-danger-600"
|
||||||
|
>*</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ defineProps<Props>();
|
|||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
const { data: account } = useGetAccount();
|
const { data: account } = useGetAccount();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
// emit
|
// emit
|
||||||
|
|
||||||
const emit = defineEmits(["update:modelValue"]);
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
@@ -29,13 +31,22 @@ const emit = defineEmits(["update:modelValue"]);
|
|||||||
const closeSideDrawer = () => {
|
const closeSideDrawer = () => {
|
||||||
emit("update:modelValue", false);
|
emit("update:modelValue", false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// watch
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.fullPath,
|
||||||
|
() => {
|
||||||
|
closeSideDrawer();
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div
|
<div
|
||||||
v-if="modelValue"
|
v-if="modelValue"
|
||||||
class="md:hidden fixed inset-0 h-svh z-999 size-full bg-black/50 cursor-pointer"
|
class="md:hidden fixed inset-0 min-h-svh z-1001 size-full bg-black/50 cursor-pointer"
|
||||||
@click="closeSideDrawer"
|
@click="closeSideDrawer"
|
||||||
/>
|
/>
|
||||||
</Transition>
|
</Transition>
|
||||||
@@ -43,61 +54,127 @@ const closeSideDrawer = () => {
|
|||||||
<div
|
<div
|
||||||
@click.stop
|
@click.stop
|
||||||
:class="modelValue ? 'translate-x-0' : 'translate-x-[100%]'"
|
:class="modelValue ? 'translate-x-0' : 'translate-x-[100%]'"
|
||||||
class="md:hidden cursor-default flex top-0 right-0 fixed z-999 transition-all duration-500 rounded-e-xl flex-col bg-white w-[300px] h-full gap-8 pt-12"
|
class="md:hidden cursor-default flex top-0 right-0 fixed z-1002 transition-all duration-500 flex-col bg-white w-[300px] h-full px-4"
|
||||||
>
|
>
|
||||||
<div class="flex items-center flex-col justify-end gap-[1.5rem]">
|
<div class="flex items-center flex-col justify-end gap-2 border-b border-slate-200 py-5">
|
||||||
<Tooltip v-if="!!account && !!token" title="حساب کاربری">
|
<NuxtLink
|
||||||
<NuxtLink
|
v-if="!!account && !!token"
|
||||||
:to="{ name: 'profile' }"
|
:to="{ name: 'profile' }"
|
||||||
class="flex items-center justify-center"
|
class="w-full flex items-center justify-between gap-3 p-2 transition-all"
|
||||||
>
|
active-class="bg-black rounded-md text-white **:stroke-white"
|
||||||
<Avatar
|
>
|
||||||
class="!size-7"
|
<div class="flex items-center gap-3">
|
||||||
:src="account.profile_photo"
|
<div class="size-5 flex-center">
|
||||||
:alt="
|
<Avatar
|
||||||
account.first_name && account.last_name
|
class="!size-5"
|
||||||
? `${account.first_name.charAt(
|
:src="account.profile_photo"
|
||||||
0
|
:alt="
|
||||||
)} ${account.last_name.charAt(0)}`
|
account.first_name && account.last_name
|
||||||
: 'بدون نام کاربری'
|
? `${account.first_name.charAt(0)} ${account.last_name.charAt(0)}`
|
||||||
"
|
: 'بدون نام کاربری'
|
||||||
/>
|
"
|
||||||
</NuxtLink>
|
/>
|
||||||
</Tooltip>
|
</div>
|
||||||
<Tooltip v-else title="ورود">
|
<span class="text-xs"> {{ account.first_name }} {{ account.last_name }} </span>
|
||||||
<NuxtLink to="/signin" class="flex-center">
|
</div>
|
||||||
<Icon
|
<Icon
|
||||||
name="ci:profile"
|
name="bi:chevron-left"
|
||||||
size="24px"
|
size="12"
|
||||||
class="**:stroke-black"
|
class="**:stroke-black/50 opacity-70"
|
||||||
/>
|
/>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="محصولات">
|
<NuxtLink
|
||||||
<NuxtLink to="/products" class="flex-center">
|
v-else
|
||||||
<Icon
|
to="/signin"
|
||||||
name="ci:search"
|
class="w-full flex items-center justify-between gap-4 p-2 transition-all"
|
||||||
size="21px"
|
active-class="bg-black rounded-md text-white **:stroke-white"
|
||||||
class="**:stroke-black"
|
>
|
||||||
/>
|
<div class="flex items-center gap-3">
|
||||||
</NuxtLink>
|
<div class="size-5 flex-center">
|
||||||
</Tooltip>
|
<Icon
|
||||||
<Tooltip title="سبد خرید">
|
name="ci:profile"
|
||||||
<NuxtLink to="/cart" class="flex-center">
|
size="18"
|
||||||
<Icon name="ci:cart" size="24px" class="**:stroke-black" />
|
class="**:stroke-black"
|
||||||
</NuxtLink>
|
/>
|
||||||
</Tooltip>
|
</div>
|
||||||
|
|
||||||
|
<span class="text-xs"> ورود به حساب </span>
|
||||||
|
</div>
|
||||||
|
<Icon
|
||||||
|
name="bi:chevron-left"
|
||||||
|
size="12"
|
||||||
|
class="**:stroke-black/50 opacity-70"
|
||||||
|
/>
|
||||||
|
</NuxtLink>
|
||||||
|
<!--
|
||||||
|
<NuxtLink
|
||||||
|
to="/products"
|
||||||
|
class="w-full flex items-center justify-between gap-4 p-2"
|
||||||
|
active-class="bg-black rounded-md text-white **:stroke-white"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="size-5 flex-center">
|
||||||
|
<Icon
|
||||||
|
name="ci:search"
|
||||||
|
size="18"
|
||||||
|
class="**:stroke-black"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs"> جست و جو </span>
|
||||||
|
</div>
|
||||||
|
<Icon
|
||||||
|
name="bi:chevron-left"
|
||||||
|
size="12"
|
||||||
|
class="**:stroke-black/50 opacity-70"
|
||||||
|
/>
|
||||||
|
</NuxtLink> -->
|
||||||
|
<NuxtLink
|
||||||
|
to="/cart"
|
||||||
|
class="w-full flex items-center justify-between gap-4 p-2 transition-all"
|
||||||
|
active-class="bg-black rounded-md text-white **:stroke-white"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="size-5 flex-center">
|
||||||
|
<Icon
|
||||||
|
name="ci:cart"
|
||||||
|
size="19"
|
||||||
|
class="**:stroke-black"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs"> سبد خرید </span>
|
||||||
|
</div>
|
||||||
|
<Icon
|
||||||
|
name="bi:chevron-left"
|
||||||
|
size="12"
|
||||||
|
class="**:stroke-black/50 opacity-70"
|
||||||
|
/>
|
||||||
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav
|
<nav class="flex-center flex-col gap-2 typo-label-sm font-light text-black/80 py-5">
|
||||||
class="flex-center flex-col gap-[2.5rem] typo-label-sm font-light text-black/80"
|
|
||||||
>
|
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-for="(link, index) in NAV_LINKS"
|
v-for="(link, index) in NAV_LINKS"
|
||||||
:key="index"
|
:key="index"
|
||||||
:to="link.path"
|
:to="link.path"
|
||||||
|
class="w-full flex items-center justify-between gap-3 p-2 transition-all"
|
||||||
|
active-class="bg-black rounded-md text-white **:stroke-white"
|
||||||
>
|
>
|
||||||
{{ link.title }}
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="size-5 flex-center">
|
||||||
|
<Icon
|
||||||
|
:name="link.icon"
|
||||||
|
size="18"
|
||||||
|
class="**:stroke-black"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs"> {{ link.title }}</span>
|
||||||
|
</div>
|
||||||
|
<Icon
|
||||||
|
name="bi:chevron-left"
|
||||||
|
size="12"
|
||||||
|
class="**:stroke-black/50 opacity-70"
|
||||||
|
/>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ const onSwiper = (swiper: SwiperClass) => {
|
|||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
ref="sectionTarget"
|
ref="sectionTarget"
|
||||||
class="flex flex-col justify-center gap-4 bg-black h-[110svh] sm:h-[150svh] relative overflow-hidden"
|
class="flex flex-col justify-center gap-4 bg-black sm:min-h-[110svh] relative overflow-hidden shrink-0 py-24 lg:py-32"
|
||||||
>
|
>
|
||||||
<div class="w-full relative translate-y-[-55px] sm:translate-y-[-130px] flex-center z-10 container">
|
<div class="w-full relative flex-center z-10 container">
|
||||||
<span class="text-white typo-h-6 md:typo-h-5 lg:typo-h-4"> دسته بندی ها </span>
|
<span class="text-white typo-h-6 md:typo-h-5 lg:typo-h-4"> دسته بندی ها </span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full my-20 relative">
|
<div class="w-full mt-44 lg:mt-64 relative">
|
||||||
<NuxtImg
|
<NuxtImg
|
||||||
class="aspect-square w-[240px] md:w-[300px] lg:w-[350px] translate-y-[-164px] md:translate-y-[-206px] lg:translate-y-[-240px] absolute left-1/2 -translate-x-1/2 z-10"
|
class="aspect-square w-[240px] md:w-[300px] lg:w-[350px] translate-y-[-164px] md:translate-y-[-206px] lg:translate-y-[-240px] absolute left-1/2 -translate-x-1/2 z-10"
|
||||||
:style="{
|
:style="{
|
||||||
@@ -89,7 +89,7 @@ const onSwiper = (swiper: SwiperClass) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-center items-center">
|
<div class="w-full flex justify-center items-center mt-14">
|
||||||
<NuxtLink to="/category">
|
<NuxtLink to="/category">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
import useGetArticles from "~/composables/api/blog/useGetArticles";
|
import useGetArticles from "~/composables/api/blog/useGetArticles";
|
||||||
@@ -10,22 +9,24 @@ const { data: articles, suspense } = useGetArticles(page);
|
|||||||
// ssr
|
// ssr
|
||||||
|
|
||||||
await suspense();
|
await suspense();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="mt-20 container">
|
<section class="container">
|
||||||
<div class="flex items-center justify-between mb-12 md:mb-20">
|
<div class="flex items-center justify-between mb-12 md:mb-20">
|
||||||
<span class="typo-h-6 max-sm:text-xl md:typo-h-5 lg:typo-h-4 text-black">
|
<span 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">
|
||||||
<Button variant="primary" class="rounded-full max-sm:typo-label-sm max-sm:py-2"
|
<Button
|
||||||
end-icon="ci:arrow-left">
|
variant="primary"
|
||||||
|
class="rounded-full max-sm:typo-label-sm max-sm:py-2"
|
||||||
|
end-icon="ci:arrow-left"
|
||||||
|
>
|
||||||
نمایش همه
|
نمایش همه
|
||||||
</Button>
|
</Button>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<ArticlesList :articles="[...articles!.results,...articles!.results,...articles!.results,...articles!.results,...articles!.results,...articles!.results]" />
|
<ArticlesList
|
||||||
|
:articles="[...articles!.results,...articles!.results,...articles!.results,...articles!.results,...articles!.results,...articles!.results]"
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container mb-40 lg:mb-80 mt-20">
|
<div class="container mb-40 lg:mb-40 max-lg:mt-20 lg:-mt-32">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col items-center gap-3 mb-10 lg:mb-16">
|
<div class="flex flex-col items-center gap-3 mb-10 lg:mb-16">
|
||||||
<span class="typo-p-sm md:typo-p-md text-slate-500"> مقایسه محصولات </span>
|
<span class="typo-p-sm md:typo-p-md text-slate-500"> مقایسه محصولات </span>
|
||||||
|
|||||||
@@ -85,14 +85,14 @@ onUnmounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
id="products-showcase-container"
|
id="products-showcase-container"
|
||||||
class="perspective-midrange relative z-[99999]"
|
class="perspective-midrange relative z-[999]"
|
||||||
>
|
>
|
||||||
<div class="w-full h-[102svh] bg-black">
|
<div class="w-full min-h-[120svh] lg:min-h-[102svh] bg-black">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-for="slide in homeData!.show_case_slider"
|
v-for="slide in homeData!.show_case_slider"
|
||||||
:key="slide.id"
|
:key="slide.id"
|
||||||
:to="slide.link"
|
:to="slide.link"
|
||||||
class="showcase-slide origin-bottom absolute size-full bg-transparent flex items-center justify-center"
|
class="showcase-slide origin-bottom absolute size-full bg-transparent flex items-center justify-center max-lg:-mt-16 lg:mt-5"
|
||||||
>
|
>
|
||||||
<NuxtImg
|
<NuxtImg
|
||||||
class="w-[280px] xs:w-[350px] lg:w-[500px] xl:w-[650px] z-20 mb-30 sm:mb-20 lg:mb-30"
|
class="w-[280px] xs:w-[350px] lg:w-[500px] xl:w-[650px] z-20 mb-30 sm:mb-20 lg:mb-30"
|
||||||
@@ -102,12 +102,8 @@ onUnmounted(() => {
|
|||||||
}"
|
}"
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
<div
|
<div class="flex flex-col items-center justify-center gap-6 text-center absolute z-20 mt-20">
|
||||||
class="flex flex-col items-center justify-center gap-6 text-center absolute z-20 mt-20"
|
<span class="text-white typo-h-6 sm:typo-h-5 lg:typo-h-4 xl:typo-h-3">
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="text-white typo-h-6 sm:typo-h-5 lg:typo-h-4 xl:typo-h-3"
|
|
||||||
>
|
|
||||||
{{ slide.title }}
|
{{ slide.title }}
|
||||||
</span>
|
</span>
|
||||||
<p
|
<p
|
||||||
@@ -115,10 +111,13 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
{{ slide.description }}
|
{{ slide.description }}
|
||||||
</p>
|
</p>
|
||||||
<NuxtLink :to="slide.link" class="relative">
|
<NuxtLink
|
||||||
|
:to="slide.link"
|
||||||
|
class="relative"
|
||||||
|
>
|
||||||
<NuxtImg
|
<NuxtImg
|
||||||
src="/img/heymlz/heymlz-falling.gif"
|
src="/img/heymlz/heymlz-falling.gif"
|
||||||
class="absolute top-[106px] sm:top-[105px] lg:top-[117px] left-1/2 -translate-1/2 w-[250px] drop-shadow-md"
|
class="absolute top-[101px] sm:top-[100px] lg:top-[117px] left-1/2 -translate-1/2 w-[200px] lg:w-[250px] drop-shadow-md"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
|||||||
@@ -94,17 +94,21 @@ export const NAV_LINKS = [
|
|||||||
{
|
{
|
||||||
title: "خانه",
|
title: "خانه",
|
||||||
path: "/",
|
path: "/",
|
||||||
|
icon: "ci:home",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "محصولات",
|
title: "محصولات",
|
||||||
path: "/products",
|
path: "/products",
|
||||||
|
icon: "ci:airdrop",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "دسته بندی ها",
|
title: "دسته بندی ها",
|
||||||
path: "/category",
|
path: "/category",
|
||||||
|
icon: "ci:delivery-boxes",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "ارتباط با ما",
|
title: "ارتباط با ما",
|
||||||
path: "/contact-us",
|
path: "/contact-us",
|
||||||
|
icon: "ci:call",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ onMounted(() => {
|
|||||||
<LoadingOverlay />
|
<LoadingOverlay />
|
||||||
<Hero class="mb-20 max-md:mt-[80px]" />
|
<Hero class="mb-20 max-md:mt-[80px]" />
|
||||||
<Preview />
|
<Preview />
|
||||||
<ProductsShowcase class="mb-40" />
|
<ProductsShowcase class="lg:mb-12" />
|
||||||
<ProductsGrid
|
<ProductsGrid
|
||||||
title="محصولات پرفروش"
|
title="محصولات پرفروش"
|
||||||
:products="[...homeData!.products,...homeData!.products]"
|
:products="[...homeData!.products,...homeData!.products]"
|
||||||
/>
|
/>
|
||||||
<Categories class="mt-40" />
|
<Categories class="mt-12" />
|
||||||
<Brands />
|
<Brands />
|
||||||
<LatestStories class="mb-20" />
|
<LatestStories class="mb-20" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+27
-23
@@ -1,9 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// import
|
// import
|
||||||
|
|
||||||
import useGetProducts, {
|
import useGetProducts, { type GetProductsFilters } from "~/composables/api/products/useGetProducts";
|
||||||
type GetProductsFilters,
|
|
||||||
} from "~/composables/api/products/useGetProducts";
|
|
||||||
import { PRODUCT_RANGE } from "~/constants";
|
import { PRODUCT_RANGE } from "~/constants";
|
||||||
|
|
||||||
// state
|
// state
|
||||||
@@ -59,12 +57,8 @@ watch(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full container flex flex-col">
|
<div class="w-full container flex flex-col">
|
||||||
<div
|
<div class="w-full flex flex-col lg:flex-row justify-end items-end py-[3.5rem] lg:py-[5rem] gap-10 lg:gap-5">
|
||||||
class="w-full flex flex-col lg:flex-row justify-end items-end py-[3.5rem] lg:py-[5rem] gap-10 lg:gap-5"
|
<div class="flex flex-col items-center lg:items-start gap-[1rem] lg:gap-[1.5rem] text-black w-full">
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex flex-col items-center lg:items-start gap-[1rem] lg:gap-[1.5rem] text-black w-full"
|
|
||||||
>
|
|
||||||
<!-- <div class="flex-center gap-[.75rem]">
|
<!-- <div class="flex-center gap-[.75rem]">
|
||||||
<span class="text-xs lg:text-sm">خانه</span>
|
<span class="text-xs lg:text-sm">خانه</span>
|
||||||
<span class="text-xs lg:text-sm">/</span>
|
<span class="text-xs lg:text-sm">/</span>
|
||||||
@@ -75,9 +69,7 @@ watch(
|
|||||||
<h1 class="typo-h-5 lg:typo-h-4">لیست محصولات</h1>
|
<h1 class="typo-h-5 lg:typo-h-4">لیست محصولات</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="w-full flex items-center justify-between lg:justify-end gap-4">
|
||||||
class="w-full flex items-center justify-between lg:justify-end gap-4"
|
|
||||||
>
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="جست و جو محصول ..."
|
placeholder="جست و جو محصول ..."
|
||||||
v-model="search"
|
v-model="search"
|
||||||
@@ -96,9 +88,7 @@ watch(
|
|||||||
<Suspense>
|
<Suspense>
|
||||||
<FilterButton />
|
<FilterButton />
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<Skeleton
|
<Skeleton class="!size-11 lg:!w-[10.35rem] lg:!h-[3.35rem] shrink-0 !rounded-xl" />
|
||||||
class="!size-11 lg:!w-[10.35rem] lg:!h-[3.35rem] shrink-0 !rounded-xl"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,24 +97,35 @@ watch(
|
|||||||
v-if="productsIsLoading"
|
v-if="productsIsLoading"
|
||||||
class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-y-8 gap-5 sm:gap-8 w-full"
|
class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-y-8 gap-5 sm:gap-8 w-full"
|
||||||
>
|
>
|
||||||
<div class="w-full flex flex-col gap-3" v-for="i in 8" :key="i">
|
<div
|
||||||
|
class="w-full flex flex-col gap-3"
|
||||||
|
v-for="i in 8"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
<Skeleton
|
<Skeleton
|
||||||
v-for="i in 3"
|
v-for="i in 3"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:class="{
|
:class="{
|
||||||
'!h-[11.75rem] lg:!h-[22.5rem] !rounded-2xl': i == 1,
|
'!h-[11.75rem] lg:!h-[22.5rem] !rounded-2xl': i == 1,
|
||||||
'!h-[1.35rem] lg:!h-[1.5rem] !rounded-sm lg:!hidden': [
|
'!h-[1.35rem] lg:!h-[1.5rem] !rounded-sm lg:!hidden': [2, 3].includes(i),
|
||||||
2, 3,
|
|
||||||
].includes(i),
|
|
||||||
'!w-1/2': i == 2,
|
'!w-1/2': i == 2,
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
<div v-else class="w-full h-max">
|
<div
|
||||||
<div v-if="!products!.length" class="flex flex-grow w-full">
|
v-else
|
||||||
<Placeholder title="محصولی یافت نشد :(" icon="bi:search" />
|
class="w-full h-max"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!products!.length"
|
||||||
|
class="flex flex-grow w-full"
|
||||||
|
>
|
||||||
|
<Placeholder
|
||||||
|
title="محصولی یافت نشد :("
|
||||||
|
icon="bi:search"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ProductsGrid
|
<ProductsGrid
|
||||||
:with-header="false"
|
:with-header="false"
|
||||||
@@ -135,7 +136,10 @@ watch(
|
|||||||
v-if="data && paginationData && data.count > 10"
|
v-if="data && paginationData && data.count > 10"
|
||||||
class="w-full flex-center py-10"
|
class="w-full flex-center py-10"
|
||||||
>
|
>
|
||||||
<Pagination :items="paginationData" :total="data.count" />
|
<Pagination
|
||||||
|
:items="paginationData"
|
||||||
|
:total="data.count"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+20
-34
@@ -39,10 +39,7 @@ const formRules = computed(() => {
|
|||||||
return {
|
return {
|
||||||
phone: {
|
phone: {
|
||||||
required: helpers.withMessage("Phone is required", required),
|
required: helpers.withMessage("Phone is required", required),
|
||||||
phoneValidator: helpers.withMessage(
|
phoneValidator: helpers.withMessage("شماره تلفن وارد شده معتبر نمی باشد", helpers.regex(/^[1-9][0-9]{9}$/)),
|
||||||
"شماره تلفن وارد شده معتبر نمی باشد",
|
|
||||||
helpers.regex(/^[1-9][0-9]{9}$/)
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -63,11 +60,7 @@ const {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: sendOtp, isPending: sendOtpIsPending } = useOtp();
|
const { mutateAsync: sendOtp, isPending: sendOtpIsPending } = useOtp();
|
||||||
const {
|
const { mutateAsync: signIn, isPending: signInIsPending, status: signInStatus } = useSignIn();
|
||||||
mutateAsync: signIn,
|
|
||||||
isPending: signInIsPending,
|
|
||||||
status: signInStatus,
|
|
||||||
} = useSignIn();
|
|
||||||
|
|
||||||
// computed
|
// computed
|
||||||
|
|
||||||
@@ -152,9 +145,7 @@ const resetForm = () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full flex h-svh items-center relative container">
|
<div class="w-full flex h-svh items-center relative container">
|
||||||
<div class="pattern -z-10 size-full fixed inset-0" />
|
<div class="pattern -z-10 size-full fixed inset-0" />
|
||||||
<div
|
<div class="flex items-center justify-center flex-col size-full translate-y-[-100px]">
|
||||||
class="flex items-center justify-center flex-col size-full translate-y-[-100px]"
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
class="aspect-square w-[250px] sm:w-[325px] translate-y-[90px] sm:translate-y-[120px] animate-fade-in"
|
class="aspect-square w-[250px] sm:w-[325px] translate-y-[90px] sm:translate-y-[120px] animate-fade-in"
|
||||||
src="/img/heymlz/heymlz-signin.gif"
|
src="/img/heymlz/heymlz-signin.gif"
|
||||||
@@ -166,11 +157,12 @@ const resetForm = () => {
|
|||||||
<div
|
<div
|
||||||
class="max-w-[600px] w-full p-6 h-[350px] sm:h-[400px] flex flex-col items-center bg-white border shadow-black/10 justify-center border-slate-300 rounded-3xl"
|
class="max-w-[600px] w-full p-6 h-[350px] sm:h-[400px] flex flex-col items-center bg-white border shadow-black/10 justify-center border-slate-300 rounded-3xl"
|
||||||
>
|
>
|
||||||
<h1 class="typo-h-6 sm:typo-h-5 mt-8">
|
<h1 class="typo-h-6 sm:typo-h-5 mt-8">شماره خود را وارد کنید</h1>
|
||||||
شماره خود را وارد کنید
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<form @submit.prevent class="max-w-[500px] w-full mt-12">
|
<form
|
||||||
|
@submit.prevent
|
||||||
|
class="max-w-[500px] w-full mt-12"
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
v-if="!showOtp"
|
v-if="!showOtp"
|
||||||
class="w-full tracking-[3px] persian-number"
|
class="w-full tracking-[3px] persian-number"
|
||||||
@@ -186,9 +178,7 @@ const resetForm = () => {
|
|||||||
name="twemoji:flag-iran"
|
name="twemoji:flag-iran"
|
||||||
size="24"
|
size="24"
|
||||||
/>
|
/>
|
||||||
<span class="text-slate-500 typo-label-sm">
|
<span class="text-slate-500 typo-label-sm"> +۹۸ </span>
|
||||||
+۹۸
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Input>
|
</Input>
|
||||||
@@ -196,13 +186,7 @@ const resetForm = () => {
|
|||||||
<OtpInput
|
<OtpInput
|
||||||
v-else
|
v-else
|
||||||
v-model="otpCode"
|
v-model="otpCode"
|
||||||
:status="
|
:status="signInStatus === 'success' ? 'success' : signInStatus === 'error' ? 'error' : 'idle'"
|
||||||
signInStatus === 'success'
|
|
||||||
? 'success'
|
|
||||||
: signInStatus === 'error'
|
|
||||||
? 'error'
|
|
||||||
: 'idle'
|
|
||||||
"
|
|
||||||
:disabled="signInIsPending || sendOtpIsPending"
|
:disabled="signInIsPending || sendOtpIsPending"
|
||||||
:autofocus="true"
|
:autofocus="true"
|
||||||
@complete="handleLogin"
|
@complete="handleLogin"
|
||||||
@@ -220,7 +204,10 @@ const resetForm = () => {
|
|||||||
ارسال کد
|
ارسال کد
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div v-else class="flex items-center w-full gap-4 mt-4">
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex items-center w-full gap-4 mt-4"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
class="rounded-full w-full mt-4 max-sm:h-[45px]"
|
class="rounded-full w-full mt-4 max-sm:h-[45px]"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -235,11 +222,7 @@ const resetForm = () => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
@click="resendOtp"
|
@click="resendOtp"
|
||||||
:loading="signInIsPending || sendOtpIsPending"
|
:loading="signInIsPending || sendOtpIsPending"
|
||||||
:disabled="
|
:disabled="signInIsPending || isResendOtpBlocked || sendOtpIsPending"
|
||||||
signInIsPending ||
|
|
||||||
isResendOtpBlocked ||
|
|
||||||
sendOtpIsPending
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
ارسال مجدد کد
|
ارسال مجدد کد
|
||||||
{{ isResendOtpBlocked ? otpBlockerTimePassed : "" }}
|
{{ isResendOtpBlocked ? otpBlockerTimePassed : "" }}
|
||||||
@@ -250,8 +233,11 @@ const resetForm = () => {
|
|||||||
to="/"
|
to="/"
|
||||||
class="flex items-center gap-2 justify-center mt-6"
|
class="flex items-center gap-2 justify-center mt-6"
|
||||||
>
|
>
|
||||||
<span> بازگشت به فروشگاه </span>
|
<Icon
|
||||||
<Icon name="ci:left-rotation" size="24" />
|
name="ci:left-rotation"
|
||||||
|
class="lg:text-xl"
|
||||||
|
/>
|
||||||
|
<span class="text-xs lg:text-sm"> بازگشت به فروشگاه </span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user