This commit is contained in:
marzban-dev
2025-04-19 21:08:15 +03:30
26 changed files with 519 additions and 483 deletions
+15 -13
View File
@@ -1,7 +1,7 @@
from django.contrib import admin, messages from django.contrib import admin, messages
from .models import * from .models import *
from unfold.admin import TabularInline, StackedInline from unfold.admin import TabularInline, StackedInline
from django.db.models import Q
from import_export.admin import ImportExportModelAdmin from import_export.admin import ImportExportModelAdmin
from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm from unfold.contrib.import_export.forms import ExportForm, ImportForm, SelectableFieldsExportForm
from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget
@@ -47,11 +47,11 @@ class BankRecordInline(StackedInline):
class OrderAdmin(ModelAdmin, ImportExportModelAdmin): class OrderAdmin(ModelAdmin, ImportExportModelAdmin):
import_form_class = ImportForm import_form_class = ImportForm
export_form_class = ExportForm export_form_class = ExportForm
search_fields = ['order_id', 'user__phone', 'user__first_name', 'user__last_name', 'user__email'] search_fields = ['user__phone', 'user__first_name', 'user__last_name', 'user__email']
list_filter = ['is_paid', 'status'] list_filter = ['is_paid', 'status']
actions_list = ['redirect_to_learn', 'udpate_bank_status'] actions_list = ['redirect_to_learn', 'udpate_bank_status']
list_display = ['order_id', 'user', 'is_paid', 'status', 'discount_code', 'address',] list_display = ['order_id', 'user', 'is_paid', 'status', 'discount_code', 'address',]
readonly_fields = ('created_at', 'order_id', 'tax', 'final_price', 'cart_total', 'discount', 'discount_code', 'user', 'address', 'is_paid') readonly_fields = ('created_at', 'tax', 'final_price', 'cart_total', 'discount_amount', 'discount_code', 'user', 'address', 'is_paid')
compressed_fields = True compressed_fields = True
warn_unsaved_form = True warn_unsaved_form = True
# exclude = ('bank_records',) # exclude = ('bank_records',)
@@ -61,19 +61,21 @@ class OrderAdmin(ModelAdmin, ImportExportModelAdmin):
} }
} }
inlines = [OrderItemModelInline, BankRecordInline] inlines = [OrderItemModelInline, BankRecordInline]
# def bank_links(self, obj): def order_id(self, obj):
# banks = obj.bank_records.all() return f"سفارش {obj.pk + 1000}"
order_id.short_description = "شماره سفارش"
# if not banks.exists():
# return "-"
# return format_html_join(
# "",
# '<a style="padding-bottom:10px;display:block;" href="/secret-admin/azbankgateways/bank/{}/change/" class="text-primary-600 dark:text-primary-500">{}</a>',
# [(bank.id, bank.tracking_code) for bank in banks]
# ) or "-"
# bank_links.short_description = "Bank Records" def get_search_results(self, request, queryset, search_term):
queryset, use_distinct = super().get_search_results(request, queryset, search_term)
if search_term.isdigit():
order_id_search = int(search_term) - 1000
queryset |= self.model.objects.filter(Q(pk=order_id_search))
return queryset, use_distinct
@action(description='اپدیت وضعیت رکورد های بانکی') @action(description='اپدیت وضعیت رکورد های بانکی')
def udpate_bank_status(self, request): def udpate_bank_status(self, request):
@@ -0,0 +1,23 @@
# Generated by Django 5.1.2 on 2025-03-29 13:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0025_alter_ordermodel_order_id'),
]
operations = [
migrations.AlterField(
model_name='orderitemmodel',
name='discount',
field=models.SmallIntegerField(verbose_name='درصد تخفیف'),
),
migrations.AlterField(
model_name='orderitemmodel',
name='price',
field=models.PositiveIntegerField(verbose_name='قیمت'),
),
]
@@ -0,0 +1,32 @@
# Generated by Django 5.1.2 on 2025-03-30 15:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0026_alter_orderitemmodel_discount_and_more'),
]
operations = [
migrations.RenameField(
model_name='orderitemmodel',
old_name='discount',
new_name='discount_percent',
),
migrations.RenameField(
model_name='ordermodel',
old_name='discount',
new_name='discount_amount',
),
migrations.RemoveField(
model_name='ordermodel',
name='order_id',
),
migrations.AlterField(
model_name='ordermodel',
name='status',
field=models.CharField(choices=[('CART', 'در سبد خرید'), ('ADMIN_PENDING', 'در انتظار تایید'), ('PENDING', 'درحال پردازش'), ('POSTED', 'ارسال شده'), ('RECEIVED', 'تحویل شده'), ('CANCELED', 'لغو شده'), ('REFUNDED', 'مرجوع شده')], default='CART', max_length=20, verbose_name='وضعیت سفارش'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-03-30 15:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0027_rename_discount_orderitemmodel_discount_percent_and_more'),
]
operations = [
migrations.AlterField(
model_name='orderitemmodel',
name='price',
field=models.BigIntegerField(verbose_name='قیمت'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-03-30 16:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0028_alter_orderitemmodel_price'),
]
operations = [
migrations.AlterField(
model_name='ordermodel',
name='discount_amount',
field=models.BigIntegerField(blank=True, null=True, verbose_name='مقدار کد تخفیف'),
),
]
+52 -42
View File
@@ -1,9 +1,10 @@
from django.db import models from django.db import models, transaction
from account.models import User, UserAddressModel, PushSubscription from account.models import User, UserAddressModel, PushSubscription
from product.models import ProductModel, ProductVariant, ProductImageModel from product.models import ProductModel, ProductVariant, ProductImageModel
from django.utils import timezone from django.utils import timezone
from django_jalali.db import models as jmodels from django_jalali.db import models as jmodels
from django.core.exceptions import ValidationError
from django.conf import settings
class DiscountCode(models.Model): class DiscountCode(models.Model):
code = models.CharField(max_length=50, verbose_name='کد تخفیف') code = models.CharField(max_length=50, verbose_name='کد تخفیف')
@@ -44,20 +45,19 @@ class OrderModel(models.Model):
('CANCELED', 'لغو شده'), ('CANCELED', 'لغو شده'),
('REFUNDED', 'مرجوع شده'), ('REFUNDED', 'مرجوع شده'),
] ]
order_id = models.PositiveIntegerField(unique=True, null=True, blank=True, verbose_name='شماره سفارش')
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='orders', verbose_name='کاربر') user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='orders', verbose_name='کاربر')
address = models.ForeignKey(UserAddressModel, on_delete=models.SET_NULL, related_name='orders', null=True, 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 = jmodels.jDateField(blank=True, null=True, verbose_name="تاریخ ثبت سفارش")
is_paid = models.BooleanField(default=False, 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="کدتخفیف") discount_code = models.ForeignKey(DiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف")
status = models.CharField(max_length=20, choices=STATUS_CHOICES, verbose_name="وضعیت سفارش") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='CART', verbose_name="وضعیت سفارش")
discount = models.BigIntegerField(null=True, blank=True, verbose_name='کل تخقیف') discount_amount = models.BigIntegerField(null=True, blank=True, verbose_name='مقدار کد تخفیف')
tax = models.BigIntegerField(null=True, blank=True, verbose_name='مالیات') tax = models.BigIntegerField(null=True, blank=True, verbose_name='مالیات')
final_price = models.BigIntegerField(null=True, blank=True, verbose_name='قیمت نهایی') final_price = models.BigIntegerField(null=True, blank=True, verbose_name='قیمت نهایی')
cart_total = models.BigIntegerField(null=True, blank=True, verbose_name='کل سبد خرید') cart_total = models.BigIntegerField(null=True, blank=True, verbose_name='کل سبد خرید')
def __str__(self): def __str__(self):
return f'سفارش: {self.id + 1000}' return f'سفارش: {self.pk + 1000}'
class Meta: class Meta:
verbose_name = 'سفارش' verbose_name = 'سفارش'
@@ -65,58 +65,68 @@ class OrderModel(models.Model):
def save(self, *args, **kwargs): def _cal_discount_amount(self, cart_total):
# genrate order id discount_percent = self.discount_code.percent if self.discount_code else 0
if not self.pk: return int(cart_total * discount_percent / 100)
last_instance = self.__class__.objects.order_by("pk").last()
self.order_id = (last_instance.pk + 1001) if last_instance else 1001
super().save(*args, **kwargs)
def _cal_tax(self, cart_total, discount_amount):
tax_rate = getattr(settings, 'DEFAULT_TAX_RATE', 20)
def cal_discount(self): return int((cart_total - discount_amount) * tax_rate / 100)
# total_with_item_discount = sum(item.total_with_discount() for item in self.items.all())
# discount_percent = self.discount_code.percent
# return total_with_item_discount * ((100 - discount_percent) / 100)
pass
def cal_tax(self):
return self.total_without_tax() * 0.2
def _cal_cart_total(self):
from django.db.models import Sum, F, FloatField
return self.items.aggregate(
total=Sum(F('price') * (1 - F('discount_percent')/100) * F('quantity'),
output_field=FloatField()
)).get('total') or 0
def cal_total(self): def _cal_final_price(self, cart_total, discount_amount, tax):
pass return cart_total - discount_amount + tax
return self.total_with_discount() + self.tax()
def cal_final_price(self):
pass
def update_order(self):
if self.status == 'CART':
cart_total = self._cal_cart_total()
discount_amount = self._cal_discount_amount(cart_total)
self.discount_amount = discount_amount
self.cart_total = cart_total
tax = self._cal_tax(cart_total, discount_amount)
self.tax = tax
self.final_price = self._cal_final_price(cart_total, discount_amount, tax)
self.save()
else:
pass
class OrderItemModel(models.Model): class OrderItemModel(models.Model):
order = models.ForeignKey(OrderModel, on_delete=models.CASCADE, related_name='items', verbose_name='سفارش') order = models.ForeignKey(OrderModel, on_delete=models.CASCADE, related_name='items', verbose_name='سفارش')
quantity = models.PositiveSmallIntegerField(verbose_name="تعداد") quantity = models.PositiveSmallIntegerField(verbose_name="تعداد")
price = models.PositiveIntegerField(verbose_name='قیمت', default=0) price = models.BigIntegerField(verbose_name='قیمت')
product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول") product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول")
discount = models.SmallIntegerField(default=0, verbose_name='تخفیف') discount_percent = models.SmallIntegerField(verbose_name='درصد تخفیف')
class Meta: class Meta:
verbose_name = 'ایتم سبد خرید' verbose_name = 'ایتم سبد خرید'
verbose_name_plural = 'ایتم های سبد خرید' verbose_name_plural = 'ایتم های سبد خرید'
# def total(self):
# return self.quantity * self.product.price
# def total_with_discount(self): def set_or_update_fields(self):
# return self.quantity * self.product.get_toman_price_after_discount() self.price = self.product.price
self.discount_percent = self.product.discount
def update_fields(self):
pass
def __str__(self): def __str__(self):
return f'({self.product}) - ({self.order.user})' return f'({self.product}) - ({self.order.user})'
def save(self, *args, **kwargs):
self.clean()
self.set_or_update_fields()
super().save(*args, **kwargs)
self.order.update_order()
def delete(self, *args, **kwargs):
self.clean()
order = self.order
super().delete(*args, **kwargs)
order.update_order()
def clean(self):
if self.pk and self.order.status != "CART":
raise ValidationError("ایتم ها فقط در حالت سبد خرید قابل ادیت هستند")
+7 -3
View File
@@ -53,7 +53,7 @@ class OrderItemSerailzier(serializers.ModelSerializer):
class Meta: class Meta:
model = OrderItemModel model = OrderItemModel
exclude = ('order',) exclude = ('order',)
read_only_fields = ('order', 'product',) read_only_fields = ('order', 'product', 'discount_percent')
def get_product(self, obj): def get_product(self, obj):
return ProductVariantSerialzier(instance=obj.product, context={'request': self.context.get('request')}).data return ProductVariantSerialzier(instance=obj.product, context={'request': self.context.get('request')}).data
@@ -79,9 +79,10 @@ class CartSerializer(serializers.ModelSerializer):
tax = serializers.SerializerMethodField() tax = serializers.SerializerMethodField()
final_price = serializers.SerializerMethodField() final_price = serializers.SerializerMethodField()
discount_code = serializers.SerializerMethodField() discount_code = serializers.SerializerMethodField()
address = UserAddressSerializer()
class Meta: class Meta:
model = OrderModel model = OrderModel
fields = [ 'discount_code', 'items', 'cart_total', 'tax', 'final_price'] fields = [ 'discount_code', 'items', 'cart_total', 'tax', 'final_price', 'address']
def get_discount_code(self, obj): def get_discount_code(self, obj):
@@ -109,6 +110,7 @@ class OrderListSerializer(serializers.ModelSerializer):
count = serializers.SerializerMethodField() count = serializers.SerializerMethodField()
images = serializers.SerializerMethodField() images = serializers.SerializerMethodField()
verbose_status = serializers.SerializerMethodField() verbose_status = serializers.SerializerMethodField()
order_id = serializers.SerializerMethodField()
class Meta: class Meta:
model = OrderModel model = OrderModel
fields = ['created_at', 'status', "images", "count", "id", 'final_price', 'order_id', 'verbose_status'] fields = ['created_at', 'status', "images", "count", "id", 'final_price', 'order_id', 'verbose_status']
@@ -119,6 +121,8 @@ class OrderListSerializer(serializers.ModelSerializer):
def get_count(self, obj): def get_count(self, obj):
return obj.items.all().count() return obj.items.all().count()
def get_order_id(self, obj):
return obj.pk + 1000
def get_images(self, obj): def get_images(self, obj):
image_list = [ image_list = [
self.context.get('request').build_absolute_uri(image.image.url) self.context.get('request').build_absolute_uri(image.image.url)
@@ -138,7 +142,7 @@ class OrderGetSerializer(serializers.ModelSerializer):
discount_code = DiscountCodeSerializer() discount_code = DiscountCodeSerializer()
class Meta: class Meta:
model = OrderModel model = OrderModel
fields = ['created_at', 'status', "images", "count", "id", 'final_price', 'order_id', 'verbose_status', 'address', 'items', 'tax' , 'cart_total', 'discount_code', 'discount'] fields = ['created_at', 'status', "images", "count", "id", 'final_price', 'order_id', 'verbose_status', 'address', 'items', 'tax' , 'cart_total', 'discount_code', 'discount_amount']
def get_verbose_status(self, obj): def get_verbose_status(self, obj):
return obj.get_status_display() return obj.get_status_display()
+11 -5
View File
@@ -1,7 +1,7 @@
from django.db.models.signals import pre_save from django.db.models.signals import pre_save
from django.dispatch import receiver from django.dispatch import receiver
from .models import OrderModel from .models import OrderModel
from account.models import PushSubscription from account.models import PushSubscription, UserAddressModel
import ghasedak_sms import ghasedak_sms
from .tasks import send_change_status_notif, send_change_status_sms from .tasks import send_change_status_notif, send_change_status_sms
@@ -13,8 +13,8 @@ def order_status_changed(sender, instance, **kwargs):
if previous.status != instance.status: if previous.status != instance.status:
new_status = instance.get_status_display() new_status = instance.get_status_display()
send_change_status_notif.delay(instance.pk, new_status) # send_change_status_notif.delay(instance.pk, new_status)
send_change_status_sms.delay(instance.pk, new_status) # send_change_status_sms.delay(instance.pk, new_status)
if previous.status == 'CART' and instance.status == 'ADMIN_PENDING': if previous.status == 'CART' and instance.status == 'ADMIN_PENDING':
# update_cart_price_fields() # update_cart_price_fields()
@@ -23,11 +23,17 @@ def order_status_changed(sender, instance, **kwargs):
pass pass
@receiver(pre_save, sender=OrderModel)
def set_default_address(sender, instance, **kwargs):
if instance.address is None and instance.user:
default_address = UserAddressModel.objects.filter(user=instance.user, is_main=True).first()
if default_address:
instance.address = default_address
def update_cart_price_fields(order): # def update_cart_price_fields(order):
pass # pass
def update_sell_data(order): def update_sell_data(order):
pass pass
+1 -1
View File
@@ -297,7 +297,7 @@ class BankCallbackSerializer(serializers.ModelSerializer):
PaymentStatus.REDIRECT_TO_BANK, PaymentStatus.REDIRECT_TO_BANK,
PaymentStatus.RETURN_FROM_BANK, PaymentStatus.RETURN_FROM_BANK,
}: }:
return "waiting" return "pending"
elif obj.status in { elif obj.status in {
PaymentStatus.CANCEL_BY_USER, PaymentStatus.CANCEL_BY_USER,
PaymentStatus.EXPIRE_GATEWAY_TOKEN, PaymentStatus.EXPIRE_GATEWAY_TOKEN,
+7 -5
View File
@@ -5,8 +5,7 @@ import { VueQueryDevtools } from "@tanstack/vue-query-devtools";
// state // state
const { $updateAvailable: updateAvailable, $handleUpdate: handleUpdate } = const { $updateAvailable: updateAvailable, $handleUpdate: handleUpdate } = useNuxtApp();
useNuxtApp();
const closeModal = () => { const closeModal = () => {
updateAvailable.value = false; updateAvailable.value = false;
@@ -16,7 +15,7 @@ const closeModal = () => {
<template> <template>
<div> <div>
<LoadingIndicator /> <LoadingIndicator />
<NuxtPwaManifest /> <NuxtPwaManifest />
<UpdatePwaModal <UpdatePwaModal
@@ -32,10 +31,13 @@ const closeModal = () => {
<ToastProvider> <ToastProvider>
<ToastContainer /> <ToastContainer />
<ToastViewport <ToastViewport
class="[--viewport-padding:_25px] fixed bottom-0 left-0 flex flex-col p-[var(--viewport-padding)] gap-[10px] w-[390px] max-w-[100vw] m-0 list-none z-[9999999999999999] outline-none" class="[--viewport-padding:_25px] fixed bottom-0 left-1/2 -translate-x-1/2 flex flex-col p-[var(--viewport-padding)] gap-[10px] w-[390px] max-w-[100vw] m-0 list-none z-[9999999999999999] outline-none"
/> />
</ToastProvider> </ToastProvider>
<VueQueryDevtools dir="ltr" buttonPosition="top-right"/> <VueQueryDevtools
dir="ltr"
buttonPosition="top-right"
/>
</div> </div>
</template> </template>
+8 -4
View File
@@ -215,22 +215,22 @@
@keyframes toastSlideIn { @keyframes toastSlideIn {
from { from {
opacity: 0; opacity: 0;
transform: translateX(calc(100% + var(--viewport-padding))); transform: translateY(calc(100% + var(--viewport-padding)));
} }
to { to {
opacity: 1; opacity: 1;
transform: translateX(0); transform: translateY(0);
} }
} }
@keyframes toastSlideOut { @keyframes toastSlideOut {
from { from {
opacity: 1; opacity: 1;
transform: translateX(var(--reka-toast-swipe-end-x)); transform: translateY(var(--reka-toast-swipe-end-x));
} }
to { to {
opacity: 0; opacity: 0;
transform: translateX(calc(100% + var(--viewport-padding))); transform: translateY(calc(100% + var(--viewport-padding)));
} }
} }
@@ -281,6 +281,10 @@
/* CONTAINER */ /* CONTAINER */
* {
scroll-behavior: smooth !important;
}
@utility container { @utility container {
@apply mx-auto px-[var(--app-container-padding)] w-full max-sm:max-w-[var(--breakpoint-xs)] max-md:max-w-[var(--breakpoint-sm)] max-lg:max-w-[var(--breakpoint-md)] max-xl:max-w-[var(--breakpoint-lg)] max-w-[var(--breakpoint-2xl)]; @apply mx-auto px-[var(--app-container-padding)] w-full max-sm:max-w-[var(--breakpoint-xs)] max-md:max-w-[var(--breakpoint-sm)] max-lg:max-w-[var(--breakpoint-md)] max-xl:max-w-[var(--breakpoint-lg)] max-w-[var(--breakpoint-2xl)];
} }
@@ -2,6 +2,7 @@
// imports // imports
import useDeleteAddress from "~/composables/api/account/useDeleteAddress"; import useDeleteAddress from "~/composables/api/account/useDeleteAddress";
import useSetOrderAddress from "~/composables/api/orders/useSetOrderAddress";
import { useToast } from "~/composables/global/useToast"; import { useToast } from "~/composables/global/useToast";
import { QUERY_KEYS } from "~/constants"; import { QUERY_KEYS } from "~/constants";
@@ -15,13 +16,11 @@ type Props = {
// props // props
withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
selectable: true, selectable: true,
}); });
// emit const { address } = toRefs(props);
const emit = defineEmits(["select"]);
// state // state
@@ -31,19 +30,48 @@ const { addToast } = useToast();
// queries // queries
const { mutateAsync: deleteAddress, isPending: deleteAddressIsPending } = const { mutateAsync: deleteAddress, isPending: deleteAddressIsPending } = useDeleteAddress();
useDeleteAddress();
const { mutateAsync: setOrderAddress, isPending: setOrderAddressIsPending } = useSetOrderAddress();
// methods // methods
const handleSelectAddress = () => {
setOrderAddress(
{ address_id: address.value?.id! },
{
onSettled: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.cart],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.addresses],
});
},
onError: () => {
addToast({
message: "در انتخاب آدرس خطایی رخ داد",
options: {
description: "لطفا مجدد تلاش کنید",
},
});
},
}
);
};
const handleDeleteAddress = (id: number) => { const handleDeleteAddress = (id: number) => {
deleteAddress( deleteAddress(
{ id }, { id },
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.cart],
});
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.addresses], queryKey: [QUERY_KEYS.addresses],
}); });
addToast({ addToast({
message: "آدرس با موفقیت حذف شد", message: "آدرس با موفقیت حذف شد",
options: { options: {
@@ -66,29 +94,30 @@ const handleDeleteAddress = (id: number) => {
<template> <template>
<button <button
@click.prevent=" @click.prevent="!!address && selectable ? handleSelectAddress() : null"
!!address && selectable ? emit('select', address) : null :class="isSelected ? 'border-transparent ring-2 ring-offset-2 ring-blue-500' : 'border-slate-200'"
"
:class="
isSelected
? 'border-transparent ring-2 ring-offset-2 ring-blue-500'
: 'border-slate-200'
"
class="flex flex-col items-center transition-all relative cursor-pointer w-full group gap-2 lg:gap-4 p-4 border rounded-xl bg-slate-50 overflow-hidden" class="flex flex-col items-center transition-all relative cursor-pointer w-full group gap-2 lg:gap-4 p-4 border rounded-xl bg-slate-50 overflow-hidden"
> >
<div v-if="deleteAddressIsPending" class="absolute inset-0"> <div
v-if="deleteAddressIsPending"
class="absolute inset-0"
>
<Skeleton class="!size-full !rounded-xl" /> <Skeleton class="!size-full !rounded-xl" />
</div> </div>
<span class="flex items-center justify-between w-full gap-3"> <span class="flex items-center justify-between w-full gap-3">
<div <div class="flex items-center gap-3 max-lg:text-sm font-semibold text-slate-900">
class="flex items-center gap-3 max-lg:text-sm font-semibold text-slate-900"
>
{{ !!address ? address.name : "آدرس" }} {{ !!address ? address.name : "آدرس" }}
<span <span
v-if="isSelected" v-if="isSelected || setOrderAddressIsPending"
class="bg-blue-500 rounded-lg px-3 py-2 text-slate-200 text-[10px] lg:text-xs" class="bg-blue-500 rounded-lg px-3 py-2 text-slate-200 text-[10px] lg:text-xs"
> >
انتخاب شده <span v-if="setOrderAddressIsPending">
<Icon
name="svg-spinners:3-dots-bounce"
class="**:fill-white"
/>
</span>
<span v-else-if="isSelected && !setOrderAddressIsPending"> انتخاب شده </span>
</span> </span>
</div> </div>
@@ -97,13 +126,14 @@ const handleDeleteAddress = (id: number) => {
@click.stop="handleDeleteAddress(address.id!)" @click.stop="handleDeleteAddress(address.id!)"
class="size-8 bg-slate-200/50 rounded-sm flex-center me-2 opacity-0 group-hover:opacity-100 transition-opacity" class="size-8 bg-slate-200/50 rounded-sm flex-center me-2 opacity-0 group-hover:opacity-100 transition-opacity"
> >
<Icon name="bi:trash" class="**:fill-red-500" /> <Icon
name="bi:trash"
class="**:fill-red-500"
/>
</button> </button>
</span> </span>
<div <div class="flex flex-col items-center justify-between w-full gap-3 lg:gap-8 lg:flex-row">
class="flex flex-col items-center justify-between w-full gap-3 lg:gap-8 lg:flex-row"
>
<div class="w-full lg:w-9/12 overflow-hidden"> <div class="w-full lg:w-9/12 overflow-hidden">
<div <div
class="w-full overflow-hidden overflow-ellipsis gap-5 text-start whitespace-pre text-xs lg:text-sm text-slate-700" class="w-full overflow-hidden overflow-ellipsis gap-5 text-start whitespace-pre text-xs lg:text-sm text-slate-700"
@@ -5,6 +5,8 @@ import useCreateOrUpdateAddress from "~/composables/api/account/useCreateOrUpdat
import useGetAccount from "~/composables/api/account/useGetAccount"; import useGetAccount from "~/composables/api/account/useGetAccount";
import { QUERY_KEYS } from "~/constants"; import { QUERY_KEYS } from "~/constants";
import { useToast } from "~/composables/global/useToast"; import { useToast } from "~/composables/global/useToast";
import useVuelidate from "@vuelidate/core";
import { helpers, required, minLength } from "@vuelidate/validators";
// types // types
@@ -42,6 +44,35 @@ const addressData = ref({
is_main: address.value?.is_main ?? false, is_main: address.value?.is_main ?? false,
}); });
// computed
const formRules = computed(() => {
return {
province: {
required: helpers.withMessage("فیلد استان سکونت الزامی می باشد", required),
minLength: helpers.withMessage("فیلد استان سکونت حداقل 2 کرکتر می باشد", minLength(2)),
},
city: {
required: helpers.withMessage("فیلد شهر سکونت الزامی می باشد", required),
minLength: helpers.withMessage("فیلد شهر سکونت حداقل 2 کرکتر می باشد", minLength(2)),
},
postal_code: {
required: helpers.withMessage("فیلد کد پستی الزامی می باشد", required),
minLength: helpers.withMessage("فیلد کد پستی حداقل 10 کرکتر می باشد", minLength(10)),
},
address: {
required: helpers.withMessage("فیلد آدرس کامل الزامی می باشد", required),
minLength: helpers.withMessage("فیلد آدرس کامل حداقل 2 کرکتر می باشد", minLength(2)),
},
phone: {
required: helpers.withMessage("فیلد تلفن همراه الزامی می باشد", required),
minLength: helpers.withMessage("فیلد تلفن همراه حداقل 10 کرکتر می باشد", minLength(10)),
},
};
});
const formValidator$ = useVuelidate(formRules, addressData);
// queries // queries
const { data: account } = useGetAccount(); const { data: account } = useGetAccount();
@@ -64,35 +95,42 @@ const closeModal = () => {
is_main: false, is_main: false,
}; };
} }
formValidator$.value.$reset();
isShow.value = false; isShow.value = false;
}; };
const addNew = () => { const handleSubmit = async () => {
createOrUpdateAddress( await formValidator$.value.$validate();
{ ...addressData.value }, if (!formValidator$.value.$errors.length) {
{ createOrUpdateAddress(
onSuccess: () => { { ...addressData.value },
queryClient.invalidateQueries({ {
queryKey: [QUERY_KEYS.addresses], onSuccess: () => {
}); queryClient.invalidateQueries({
closeModal(); queryKey: [QUERY_KEYS.addresses],
addToast({ });
message: isEditing.value ? "آدرس با موفقیت ویرایش شد" : "آدرس با موفقیت اضافه شد", queryClient.invalidateQueries({
options: { queryKey: [QUERY_KEYS.cart],
status: "success", });
}, closeModal();
}); addToast({
}, message: isEditing.value ? "آدرس با موفقیت ویرایش شد" : "آدرس با موفقیت اضافه شد",
onError: () => { options: {
addToast({ status: "success",
message: isEditing.value ? "آدرس با موفقیت ویرایش شد" : "مشکلی در افزودن آدرس رخ داد", },
options: { });
status: "error", },
}, onError: () => {
}); addToast({
}, message: isEditing.value ? "آدرس با موفقیت ویرایش شد" : "مشکلی در افزودن آدرس رخ داد",
} options: {
); status: "error",
},
});
},
}
);
}
}; };
watch( watch(
@@ -122,9 +160,9 @@ watch(
<Button <Button
:end-icon="!!address ? 'bi:pen' : 'ci:plus'" :end-icon="!!address ? 'bi:pen' : 'ci:plus'"
size="md" size="md"
class="rounded-full" class="rounded-full transition-all"
:variant="!!address ? 'ghost' : 'solid'" :variant="!!address ? 'ghost' : 'solid'"
:class="!!address ? '!bg-transparent !underline' : ''" :class="!!address ? '!bg-transparent !underline underline-offset-4' : ''"
> >
<span class="whitespace-pre max-lg:text-xs"> <span class="whitespace-pre max-lg:text-xs">
{{ !!address ? "ویرایش" : "افزودن آدرس" }} {{ !!address ? "ویرایش" : "افزودن آدرس" }}
@@ -138,115 +176,111 @@ watch(
dir="rtl" dir="rtl"
> >
<div class="grid w-full grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"> <div class="grid w-full grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
<div class="flex flex-col gap-2"> <DataField
<label id="name"
for="name" label="نام پیش فرض"
class="text-xs font-semibold lg:text-sm text-gray-900" >
>نام پیش فرض <span class="text-sm text-red-500">*</span></label
>
<Input <Input
id="name" id="name"
type="text" type="text"
placeholder="اینجا وارد کنید ..." placeholder="اینجا وارد کنید ..."
v-model="addressData.name!" v-model="addressData.name!"
/> />
</div> </DataField>
<div class="flex flex-col gap-2">
<label
for="province"
class="text-xs font-semibold lg:text-sm text-gray-900"
>
آدرس شما؟
<span class="text-sm text-red-500"> * </span>
</label>
<DataField
id="for_me"
label="آدرس شما؟"
:required="true"
>
<Select <Select
id="for_me"
:options="['بله', 'خیر']" :options="['بله', 'خیر']"
placeholder="انتخاب کنید" placeholder="انتخاب کنید"
v-model="addressData.for_me as string" v-model="addressData.for_me as string"
/> />
</div> </DataField>
<div class="flex flex-col gap-2">
<label
for="phone"
class="text-xs font-semibold lg:text-sm text-gray-900"
>شماره تلفن <span class="text-sm text-red-500">*</span></label
>
<DataField
id="phone"
label="تلفن همراه"
:required="true"
:error="formValidator$.phone"
>
<Input <Input
id="phone" id="phone"
type="text" type="text"
placeholder="اینجا وارد کنید ..." placeholder="اینجا وارد کنید ..."
:error="formValidator$.phone.$error"
v-model="addressData.phone!" v-model="addressData.phone!"
/> />
</div> </DataField>
<div class="flex flex-col gap-2">
<label
for="province"
class="text-xs font-semibold lg:text-sm text-gray-900"
>استان
<span class="text-sm text-red-500">*</span>
</label>
<DataField
id="province"
label="استان"
:required="true"
:error="formValidator$.province"
>
<Input <Input
id="province" id="province"
type="text" type="text"
placeholder="اینجا وارد کنید ..." placeholder="اینجا وارد کنید ..."
:error="formValidator$.province.$error"
v-model="addressData.province!" v-model="addressData.province!"
/> />
</div> </DataField>
<div class="flex flex-col gap-2"> <DataField
<label id="city"
for="city" label="شهر"
class="text-xs font-semibold lg:text-sm text-gray-900" :required="true"
>شهر <span class="text-sm text-red-500">*</span></label :error="formValidator$.city"
> >
<Input <Input
id="city" id="city"
type="text" type="text"
placeholder="اینجا وارد کنید ..." placeholder="اینجا وارد کنید ..."
:error="formValidator$.city.$error"
v-model="addressData.city!" v-model="addressData.city!"
/> />
</div> </DataField>
<div class="flex flex-col gap-2"> <DataField
<label id="postal_code"
for="post" label="کد پستی"
class="text-xs font-semibold lg:text-sm text-gray-900" :required="true"
>کد پستی <span class="text-sm text-red-500">*</span></label :error="formValidator$.postal_code"
> >
<Input <Input
id="post" id="postal_code"
type="text" type="text"
placeholder="اینجا وارد کنید ..." placeholder="اینجا وارد کنید ..."
:error="formValidator$.postal_code.$error"
v-model="addressData.postal_code!" v-model="addressData.postal_code!"
/> />
</div> </DataField>
</div> </div>
<div class="flex flex-col w-full gap-2"> <DataField
<label id="address"
for="address" label="آدرس کامل"
class="text-xs font-semibold lg:text-sm text-gray-900" :required="true"
>آدرس کامل <span class="text-sm text-red-500">*</span></label :error="formValidator$.address"
> >
<textarea <Textarea
id="address" id="address"
placeholder="آدرس خود را بنویسید" placeholder="آدرس خود را بنویسید"
v-model="addressData.address" v-model="addressData.address"
class="flex items-center field-sizing-content resize-none bg-slate-50 border-slate-200 hover:border-black focus:border-black max-h-[10rem] text-black justify-between cursor-text transition-all border-[1.5px] gap-3 typo-label-md px-4 py-1.5 lg:py-3.5 selection:bg-slate-100 rounded-md lg:rounded-100 outline-none flex-1 text-xs lg:!text-sm placeholder-slate-400 placeholder:text-xs lg:placeholder:text-sm placeholder:font-normal" :error="formValidator$.address.$error"
></textarea> class="flex items-center field-sizing-content resize-none bg-slate-50 border-slate-200 hover:border-black focus:border-black max-h-[10rem] text-black justify-between cursor-text transition-all border-[1.5px] gap-3 typo-label-md px-4 py-2.5 lg:py-3 leading-[175%] selection:bg-slate-100 rounded-md lg:rounded-100 outline-none max-lg:h-[5rem] lg:flex-1 text-xs lg:!text-sm placeholder-slate-400 placeholder:text-xs lg:placeholder:text-sm placeholder:font-normal"
</div> ></Textarea>
</DataField>
<div class="flex items-center justify-between w-full gap-2"> <div class="flex items-center justify-between w-full gap-2">
<label <label
for="is_main" for="is_main"
class="text-xs font-semibold lg:text-sm text-gray-900" class="text-xs font-medium lg:text-sm text-gray-900"
> >
به عنوان آدرس پیش فرض ثبت شود؟ به عنوان آدرس پیش فرض ثبت شود؟
</label> </label>
@@ -260,7 +294,7 @@ watch(
<div class="py-6 border-t border-slate-200 flex gap-3"> <div class="py-6 border-t border-slate-200 flex gap-3">
<Button <Button
:disabled="createAddressIsPending" :disabled="createAddressIsPending"
@click="addNew" @click="handleSubmit"
class="rounded-full px-10" class="rounded-full px-10"
size="md" size="md"
> >
@@ -13,12 +13,8 @@ const { data: cart, isLoading: cartIsLoading } = useGetCartOrders();
</script> </script>
<template> <template>
<div <div class="flex flex-col items-center w-full gap-4 p-4 border lg:gap-6 border-slate-200 rounded-xl bg-gray-50">
class="flex flex-col items-center w-full gap-4 p-4 border lg:gap-6 border-gray-300 rounded-xl bg-gray-50" <span class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-gray-900">
>
<span
class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-gray-900"
>
خلاصه سفارش خلاصه سفارش
</span> </span>
@@ -52,7 +48,10 @@ const { data: cart, isLoading: cartIsLoading } = useGetCartOrders();
class="gap-2 flex-center" class="gap-2 flex-center"
> >
<span class="text-sm text-black"> مشاهده بیشتر </span> <span class="text-sm text-black"> مشاهده بیشتر </span>
<Icon name="bi:chevron-down" class="**:stroke-black" /> <Icon
name="bi:chevron-down"
class="**:stroke-black"
/>
</button> </button>
</div> </div>
</template> </template>
+31 -47
View File
@@ -26,7 +26,7 @@ const { $queryClient: queryClient } = useNuxtApp();
const { addToast } = useToast(); const { addToast } = useToast();
const counter = ref(data.value.quantity); const counter = ref(data.value.quantity);
const debouncedCounter = refDebounced(counter, 700); const debouncedCounter = refDebounced(counter, 500);
const { isLoading: cartImageIsLoading } = useImage({ const { isLoading: cartImageIsLoading } = useImage({
src: data.value.product.image, src: data.value.product.image,
@@ -34,8 +34,7 @@ const { isLoading: cartImageIsLoading } = useImage({
// queries // queries
const { mutateAsync: deleteCartItem, isPending: deleteCartItemIsPending } = const { mutateAsync: deleteCartItem, isPending: deleteCartItemIsPending } = useDeleteCartItem();
useDeleteCartItem();
const { mutateAsync: addCartItem } = useAddCartItem(); const { mutateAsync: addCartItem } = useAddCartItem();
@@ -96,6 +95,7 @@ watch(
{ {
onSuccess: () => { onSuccess: () => {
invalidateCart(); invalidateCart();
queryClient.refetchQueries({ queryKey: [QUERY_KEYS.product, data.value.product.id] });
}, },
onError: () => { onError: () => {
invalidateCart(); invalidateCart();
@@ -134,24 +134,23 @@ watch(
<div class="flex flex-col w-full gap-3 lg:gap-4"> <div class="flex flex-col w-full gap-3 lg:gap-4">
<div class="flex items-center justify-between gap-3"> <div class="flex items-center justify-between gap-3">
<span <span class="font-semibold typo-sub-h-xs lg:typo-sub-h-sm text-slate-600">
class="font-semibold typo-sub-h-xs lg:typo-sub-h-sm text-slate-600"
>
{{ data.product.category }} {{ data.product.category }}
</span> </span>
<div <div
v-if="data.discount > 0" 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 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"
> >
<Icon name="bi:percent" class="size-4" /> <Icon
name="bi:percent"
class="size-4"
/>
{{ data.discount }} {{ data.discount }}
تخفیف تخفیف
</div> </div>
</div> </div>
<span <span class="font-semibold typo-sub-h-sm lg:typo-sub-h-xl text-black">
class="font-semibold typo-sub-h-sm lg:typo-sub-h-xl text-black"
>
{{ data.product.title }} {{ data.product.title }}
</span> </span>
@@ -171,8 +170,7 @@ watch(
</div> </div>
<span <span
v-if="data.product.product_attributes.length > 0" v-if="data.product.product_attributes.length > 0"
v-for="(variant, index) in data.product v-for="(variant, index) in data.product.product_attributes"
.product_attributes"
:index="index" :index="index"
class="px-3 py-1 rounded-full border border-slate-200 text-xs lg:text-sm" class="px-3 py-1 rounded-full border border-slate-200 text-xs lg:text-sm"
> >
@@ -180,20 +178,17 @@ watch(
</span> </span>
</div> </div>
<div <div class="items-center justify-between hidden w-full lg:flex -mt-1">
class="items-center justify-between hidden w-full lg:flex -mt-1"
>
<div class="flex items-center"> <div class="flex items-center">
<button <button
@click="handleIncreaseQuantity" @click="handleIncreaseQuantity"
class="border size-10 flex-center rounded-100 border-slate-300" class="border size-10 flex-center rounded-100 border-slate-300"
:class=" :class="deleteCartItemIsPending ? 'pointer-events-none' : ''"
deleteCartItemIsPending
? 'pointer-events-none'
: ''
"
> >
<Icon name="bi:plus" class="**:stroke-slate-800" /> <Icon
name="bi:plus"
class="**:stroke-slate-800"
/>
</button> </button>
<div class="size-10 flex-center">{{ counter }}</div> <div class="size-10 flex-center">{{ counter }}</div>
@@ -201,19 +196,11 @@ watch(
<button <button
@click="handleDecreaseQuantity" @click="handleDecreaseQuantity"
class="border size-10 flex-center rounded-100 border-slate-300" class="border size-10 flex-center rounded-100 border-slate-300"
:class=" :class="deleteCartItemIsPending ? 'pointer-events-none' : ''"
deleteCartItemIsPending
? 'pointer-events-none'
: ''
"
> >
<Icon <Icon
v-if="counter == 1" v-if="counter == 1"
:name=" :name="deleteCartItemIsPending ? 'svg-spinners:3-dots-bounce' : 'bi:trash'"
deleteCartItemIsPending
? 'svg-spinners:3-dots-bounce'
: 'bi:trash'
"
class="**:fill-red-700" class="**:fill-red-700"
/> />
<Icon <Icon
@@ -232,9 +219,7 @@ watch(
> >
{{ data.price }} {{ data.price }}
</span> </span>
<span <span class="typo-p-xl relative flex-center w-fit font-medium">
class="typo-p-xl relative flex-center w-fit font-medium"
>
{{ data.final_price }} {{ data.final_price }}
</span> </span>
</div> </div>
@@ -248,11 +233,12 @@ watch(
<button <button
@click="handleIncreaseQuantity" @click="handleIncreaseQuantity"
class="border size-7 p-1 lg:size-10 flex-center rounded-50 border-slate-300" class="border size-7 p-1 lg:size-10 flex-center rounded-50 border-slate-300"
:class=" :class="deleteCartItemIsPending ? 'pointer-events-none' : ''"
deleteCartItemIsPending ? 'pointer-events-none' : ''
"
> >
<Icon name="bi:plus" class="**:stroke-slate-800" /> <Icon
name="bi:plus"
class="**:stroke-slate-800"
/>
</button> </button>
<div class="size-10 text-sm flex-center"> <div class="size-10 text-sm flex-center">
@@ -262,20 +248,18 @@ watch(
<button <button
@click="handleDecreaseQuantity" @click="handleDecreaseQuantity"
class="border size-7 lg:size-10 p-1 flex-center rounded-50 border-slate-300" class="border size-7 lg:size-10 p-1 flex-center rounded-50 border-slate-300"
:class=" :class="deleteCartItemIsPending ? 'pointer-events-none' : ''"
deleteCartItemIsPending ? 'pointer-events-none' : ''
"
> >
<Icon <Icon
v-if="counter == 1" v-if="counter == 1"
:name=" :name="deleteCartItemIsPending ? 'svg-spinners:3-dots-bounce' : 'bi:trash'"
deleteCartItemIsPending
? 'svg-spinners:3-dots-bounce'
: 'bi:trash'
"
class="**:fill-red-700" class="**:fill-red-700"
/> />
<Icon v-else name="bi:dash" class="**:stroke-slate-800" /> <Icon
v-else
name="bi:dash"
class="**:stroke-slate-800"
/>
</button> </button>
</div> </div>
+28 -37
View File
@@ -9,19 +9,13 @@
}" }"
/> />
<div <div class="flex flex-col gap-4 items-center justify-center relative z-20">
class="flex flex-col gap-4 items-center justify-center relative z-20" <div class="flex items-center flex-col gap-8 pb-[10px] pt-[80px] lg:pt-[100px] lg:pb-[50px] justify-center">
>
<div
class="flex items-center flex-col gap-8 pb-[10px] pt-[80px] lg:pt-[150px] lg:pb-[50px] justify-center"
>
<img <img
src="/img/heymlz/heymlz-small-idle.gif" src="/img/heymlz/heymlz-small-idle.gif"
class="size-[150px] lg:size-[220px] rounded-full drop-shadow-2xl" class="size-[150px] lg:size-[220px] rounded-full drop-shadow-2xl"
/> />
<span <span class="font-bold text-2xl lg:text-5xl text-gradient bg-gradient-to-l from-blue-500 to-blue-700">
class="font-bold text-2xl lg:text-5xl text-gradient bg-gradient-to-l from-blue-500 to-blue-700"
>
فروشگاه هی ملز فروشگاه هی ملز
</span> </span>
</div> </div>
@@ -30,44 +24,49 @@
class="w-full flex max-lg:flex-col justify-between py-[64px] max-lg:gap-16 container items-center lg:items-start relative z-20" class="w-full flex max-lg:flex-col justify-between py-[64px] max-lg:gap-16 container items-center lg:items-start relative z-20"
> >
<div class="flex flex-col gap-4 max-w-[300px]"> <div class="flex flex-col gap-4 max-w-[300px]">
<h3 <h3 class="font-bold text-lg xl:text-3xl max-lg:text-center text-white">
class="font-bold text-lg xl:text-3xl max-lg:text-center text-white"
>
با ما در ارتباط باشید... با ما در ارتباط باشید...
</h3> </h3>
<p <p class="text-md font-thin leading-[175%] mt-4 max-lg:text-center text-slate-300 max-lg:text-xs">
class="text-md font-thin leading-[175%] mt-4 max-lg:text-center text-slate-300 max-lg:text-xs" لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنگی با تولید سادگی نامفهوم از صنعت چاپ و با
> استفاده از طراحان گرافیک است. چاپگرها
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنگی با
تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان
گرافیک است. چاپگرها
</p> </p>
<div <div class="flex items-center gap-4 mt-6 max-lg:justify-center">
class="flex items-center gap-4 mt-6 max-lg:justify-center" <NuxtLink
> to="#"
<NuxtLink to="#" class="flex-center size-[1.5rem]"> class="flex-center size-[1.5rem]"
>
<Icon <Icon
name="ci:instagram" name="ci:instagram"
class="**:fill-white" class="**:fill-white"
size="24" size="24"
/> />
</NuxtLink> </NuxtLink>
<NuxtLink to="#" class="flex-center size-[1.5rem]"> <NuxtLink
to="#"
class="flex-center size-[1.5rem]"
>
<Icon <Icon
name="ci:facebook" name="ci:facebook"
class="**:fill-white **:stroke-white" class="**:fill-white **:stroke-white"
size="20" size="20"
/> />
</NuxtLink> </NuxtLink>
<NuxtLink to="#" class="flex-center size-[1.5rem]"> <NuxtLink
to="#"
class="flex-center size-[1.5rem]"
>
<Icon <Icon
name="ci:tiktok" name="ci:tiktok"
class="**:fill-white **:stroke-white" class="**:fill-white **:stroke-white"
size="20" size="20"
/> />
</NuxtLink> </NuxtLink>
<NuxtLink to="#" class="flex-center size-[1.5rem]"> <NuxtLink
to="#"
class="flex-center size-[1.5rem]"
>
<Icon <Icon
name="ci:youtube" name="ci:youtube"
class="**:fill-white" class="**:fill-white"
@@ -79,9 +78,7 @@
<div class="flex justify-center lg:justify-end flex-1"> <div class="flex justify-center lg:justify-end flex-1">
<div class="flex flex-col gap-6 max-lg:text-center"> <div class="flex flex-col gap-6 max-lg:text-center">
<h3 class="font-bold text-white">لینک های مفید</h3> <h3 class="font-bold text-white">لینک های مفید</h3>
<ul <ul class="flex flex-col gap-4 font-thin text-slate-300 max-lg:text-xs">
class="flex flex-col gap-4 font-thin text-slate-300 max-lg:text-xs"
>
<li>از طراحان گرافیک است</li> <li>از طراحان گرافیک است</li>
<li>تولید نامفهوم</li> <li>تولید نامفهوم</li>
<li>ستون و سطرآنچنان که لازم</li> <li>ستون و سطرآنچنان که لازم</li>
@@ -92,9 +89,7 @@
<div class="flex justify-end flex-1"> <div class="flex justify-end flex-1">
<div class="flex flex-col gap-6 max-lg:text-center"> <div class="flex flex-col gap-6 max-lg:text-center">
<h3 class="font-bold text-white">لینک های مفید</h3> <h3 class="font-bold text-white">لینک های مفید</h3>
<ul <ul class="flex flex-col gap-4 font-thin text-slate-300 max-lg:text-xs">
class="flex flex-col gap-4 font-thin text-slate-300 max-lg:text-xs"
>
<li>از طراحان گرافیک است</li> <li>از طراحان گرافیک است</li>
<li>تولید نامفهوم</li> <li>تولید نامفهوم</li>
<li>ستون و سطرآنچنان که لازم</li> <li>ستون و سطرآنچنان که لازم</li>
@@ -104,12 +99,8 @@
</div> </div>
<div class="flex justify-end flex-1"> <div class="flex justify-end flex-1">
<div class="flex flex-col gap-6 max-lg:text-center"> <div class="flex flex-col gap-6 max-lg:text-center">
<h3 class="font-bold w-full text-white"> <h3 class="font-bold w-full text-white">لینک های مفید</h3>
لینک های مفید <ul class="flex flex-col gap-4 font-thin text-slate-300 max-lg:text-xs">
</h3>
<ul
class="flex flex-col gap-4 font-thin text-slate-300 max-lg:text-xs"
>
<li>از طراحان گرافیک است</li> <li>از طراحان گرافیک است</li>
<li>تولید نامفهوم</li> <li>تولید نامفهوم</li>
<li>ستون و سطرآنچنان که لازم</li> <li>ستون و سطرآنچنان که لازم</li>
+1 -1
View File
@@ -39,7 +39,7 @@ const classes = computed(() => {
"input-solid": variant.value === "solid", "input-solid": variant.value === "solid",
"input-outlined": variant.value === "outlined", "input-outlined": variant.value === "outlined",
"input-effects": !error.value, "input-effects": !error.value,
[variant.value === "solid" ? "input-solid-error" : "input-outlined-error"]: error.value, [variant.value === "solid" ? "!input-solid-error" : "!input-outlined-error"]: error.value,
}, },
]; ];
}); });
@@ -40,17 +40,17 @@ const statusIcon = computed(() => {
case "success": case "success":
return { return {
name: "duo-icons:check-circle", name: "duo-icons:check-circle",
class: "**:fill-success-500 [filter:drop-shadow(0_0_10px_var(--color-success-500))]", class: "**:fill-success-500 [filter:drop-shadow(0_0_20px_var(--color-success-500))]",
}; };
case "error": case "error":
return { return {
name: "duo-icons:alert-triangle", name: "duo-icons:alert-triangle",
class: "**:fill-danger-500 [filter:drop-shadow(0_0_10px_var(--color-danger-500))]", class: "**:fill-danger-500 [filter:drop-shadow(0_0_20px_var(--color-danger-500))]",
}; };
case "info": case "info":
return { return {
name: "duo-icons:info", name: "duo-icons:info",
class: "**:fill-cyan-500 [filter:drop-shadow(0_0_10px_var(--color-cyan-500))]", class: "**:fill-cyan-500 [filter:drop-shadow(0_0_20px_var(--color-cyan-500))]",
}; };
case "warning": case "warning":
return { return {
@@ -88,20 +88,25 @@ onMounted(() => {
:duration="options.duration ?? 4000" :duration="options.duration ?? 4000"
@swipeEnd="onSwipeEnd" @swipeEnd="onSwipeEnd"
v-model:open="open" v-model:open="open"
class="w-full bg-white shadow-md justify-items-start shadow-black/3 border-t-[0.5px] border-slate-200 p-4 grid [grid-template-areas:_'title_action'_'description_action'] grid-cols-[auto_max-content] gap-x-[15px] items-center data-[state=open]:animate-toast-in data-[state=closed]:animate-toast-hide data-[swipe=move]:translate-x-[var(--reka-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=cancel]:transition-[transform_200ms_ease-out] data-[swipe=end]:animate-toast-out" class="w-full bg-white shadow-md justify-items-start shadow-black/3 border-[0.5px] flex flex-col border-slate-300 p-4 gap-x-[15px] items-center data-[state=open]:animate-toast-in data-[state=closed]:animate-toast-hide data-[swipe=move]:translate-x-[var(--reka-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=cancel]:transition-[transform_200ms_ease-out] data-[swipe=end]:animate-toast-out"
:class="options.description ? 'rounded-150' : 'rounded-full'" :class="options.description ? 'rounded-150' : 'rounded-full'"
> >
<ToastTitle <ToastTitle
:class="[{ 'mb-1.5': options.description }]" :class="[{ 'mb-1': options.description }]"
class="w-full justify-items-start [grid-area:_title] font-medium text-slate-600 text-sm flex items-center justify-between gap-2" class="w-full justify-items-start font-medium text-slate-600 text-sm flex items-center justify-between gap-2"
> >
<Icon :name="statusIcon.name" :class="statusIcon.class" size="24" /> <Icon
<span class="text-start -me-2">{{ message }}</span> :name="statusIcon.name"
:class="statusIcon.class"
size="24"
/>
<span class="text-start">{{ message }}</span>
</ToastTitle> </ToastTitle>
<ToastDescription v-if="options.description" as-child> <ToastDescription
<div v-if="options.description"
class="[grid-area:_description] m-0 mr-8 text-slate-500 typo-p-sm text-start" as-child
> >
<div class="text-slate-400 typo-p-xs font-medium flex items-center justify-end w-full">
{{ options.description }} {{ options.description }}
</div> </div>
</ToastDescription> </ToastDescription>
+6 -16
View File
@@ -8,9 +8,7 @@ const route = useRoute();
// computed // computed
const pageTitle = computed(() => route.meta.pageTitle); const pageTitle = computed(() => route.meta.pageTitle);
const prevPage = computed( const prevPage = computed(() => route.meta.prevPage as { name: string; label: string } | undefined);
() => route.meta.prevPage as { name: string; label: string } | undefined
);
// queries // queries
@@ -20,9 +18,7 @@ await suspense();
// computed // computed
const hasCartItem = computed( const hasCartItem = computed(() => !!cart.value && cart.value.items.length! > 0);
() => !!cart.value && cart.value.items.length! > 0
);
</script> </script>
<template> <template>
@@ -32,20 +28,16 @@ const hasCartItem = computed(
> >
<Header /> <Header />
<main <main class="w-full overflow-x-hidden flex flex-col gap-[5rem] lg:max-w-[85vw]">
class="w-full overflow-x-hidden flex flex-col gap-[5rem] lg:max-w-[85vw]"
>
<div class="w-full flex flex-col container"> <div class="w-full flex flex-col container">
<div <div
class="flex flex-col items-center justify-center py-[3.5rem] lg:py-[5rem] gap-5 lg:gap-0 lg:flex-row" class="flex flex-col items-center justify-center py-[3.5rem] lg:py-[5rem] gap-5 lg:gap-0 lg:flex-row"
> >
<div <div class="flex items-center justify-start w-full lg:w-3/12">
class="flex items-center justify-start w-full lg:w-3/12"
>
<NuxtLink <NuxtLink
v-if="prevPage" v-if="prevPage"
:to="{ name: prevPage?.name }" :to="{ name: prevPage?.name }"
class="flex items-center gap-2 text-sm lg:text-[1rem]" class="flex items-center gap-2 text-sm lg:text-[1rem] font-medium"
> >
<Icon <Icon
name="bi:arrow-right" name="bi:arrow-right"
@@ -57,9 +49,7 @@ const hasCartItem = computed(
</NuxtLink> </NuxtLink>
</div> </div>
<h1 <h1 class="w-full text-center lg:w-6/12 typo-h-5 lg:typo-h-4">
class="w-full text-center lg:w-6/12 typo-h-5 lg:typo-h-4"
>
{{ pageTitle }} {{ pageTitle }}
</h1> </h1>
+5
View File
@@ -0,0 +1,5 @@
export default defineNuxtRouteMiddleware((to, from) => {
if (to.path !== from.path && process.client) {
window.scrollTo(0, 0);
}
});
+3 -9
View File
@@ -80,18 +80,12 @@ onMounted(() => {
<template> <template>
<div class="flex flex-col w-full gap-5"> <div class="flex flex-col w-full gap-5">
<div <div class="flex flex-col items-center w-full gap-4 p-4 border border-slate-200 rounded-xl bg-gray-50">
class="flex flex-col items-center w-full gap-4 p-4 border border-gray-300 rounded-xl bg-gray-50" <span class="flex items-center justify-start w-full text-[1.125rem] font-semibold text-gray-900">
>
<span
class="flex items-center justify-start w-full text-[1.125rem] font-semibold text-gray-900"
>
روش پرداخت روش پرداخت
</span> </span>
<div <div class="w-full grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
class="w-full grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4"
>
<Gateway <Gateway
v-for="(gateway, index) in paymentGateways" v-for="(gateway, index) in paymentGateways"
:index="index" :index="index"
+27 -76
View File
@@ -2,9 +2,8 @@
// imports // imports
import useGetAllAddress from "~/composables/api/account/useGetAllAddress"; import useGetAllAddress from "~/composables/api/account/useGetAllAddress";
import useSetOrderAddress from "~/composables/api/orders/useSetOrderAddress";
import { useToast } from "~/composables/global/useToast"; import { useToast } from "~/composables/global/useToast";
import { QUERY_KEYS } from "~/constants"; import useGetCartOrders from "~/composables/api/orders/useGetCartOrders";
// meta // meta
@@ -29,10 +28,9 @@ type DeliveryData = {
// queries // queries
const { data: addresses, isLoading: addressesIsLoading } = useGetAllAddress(); const { data: cart, isLoading: cartIsLoading } = useGetCartOrders();
const { mutateAsync: setOrderAddress, isPending: setOrderAddressIsPending } = const { data: addresses, isLoading: addressesIsLoading } = useGetAllAddress();
useSetOrderAddress();
// computed // computed
@@ -53,41 +51,6 @@ const deliveryData = ref<DeliveryData>({
tipax: false, tipax: false,
}, },
}); });
// methods
const handleSelectAddress = (address: Address) => {
deliveryData.value.address = { ...address };
};
// watch
whenever(
() => deliveryData.value.address,
(nv) => {
setOrderAddress(
{ address_id: nv.id! },
{
onSettled: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.addresses],
});
},
onError: () => {
addToast({
message: "در انتخاب آدرس خطایی رخ داد",
options: {
description: "لطفا مجدد تلاش کنید",
},
});
},
}
);
},
{
deep: true,
}
);
</script> </script>
<template> <template>
@@ -95,17 +58,20 @@ whenever(
<AddressItem /> <AddressItem />
<div class="flex flex-col w-full gap-6"> <div class="flex flex-col w-full gap-6">
<div class="flex items-center gap-3 py-3"> <div class="flex items-center gap-3 py-3">
<NuxtImg src="/img/location.gif" class="size-12 pb-1 -mr-3" /> <NuxtImg
<span class="typo-sub-h-xl -mr-3"> آدرس های شما </span> src="/img/location.gif"
<Icon class="size-12 pb-1 -mr-3"
name="svg-spinners:180-ring-with-bg"
size="20"
v-if="setOrderAddressIsPending"
class="pb-0.5"
/> />
<span class="typo-sub-h-xl -mr-3"> آدرس های شما </span>
</div> </div>
<div v-if="addressesIsLoading" class="flex flex-col gap-6 w-full"> <div
<Skeleton v-for="i in 3" class="w-full !h-[8rem] !rounded-xl" /> v-if="addressesIsLoading"
class="flex flex-col gap-6 w-full"
>
<Skeleton
v-for="i in 3"
class="w-full !h-[8rem] !rounded-xl"
/>
</div> </div>
<template v-else> <template v-else>
<div <div
@@ -120,34 +86,28 @@ whenever(
/> />
</div> </div>
<div v-else class="flex flex-col gap-6 w-full"> <div
v-else
class="flex flex-col gap-6 w-full"
>
<AddressItem <AddressItem
v-for="(address, index) in addresses" v-for="(address, index) in addresses"
:key="index" :key="index"
:isSelected="address.id === cart?.address.id"
:address="address" :address="address"
@select="handleSelectAddress"
:isSelected="address.id == deliveryData.address?.id"
/> />
</div> </div>
</template> </template>
</div> </div>
<div <div class="flex flex-col items-center w-full gap-4 my-3 p-4 border border-slate-200 rounded-xl bg-slate-50">
class="flex flex-col items-center w-full gap-4 my-3 p-4 border border-slate-200 rounded-xl bg-slate-50" <span class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-slate-900">
>
<span
class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-slate-900"
>
شیوه ارسال شیوه ارسال
</span> </span>
<label <label
@click="deliveryData.deliveryMethod.pishtaz = true" @click="deliveryData.deliveryMethod.pishtaz = true"
:class=" :class="deliveryData.deliveryMethod.pishtaz ? 'ring-black ring-offset-2 ring-2' : ''"
deliveryData.deliveryMethod.pishtaz
? 'ring-black ring-offset-2 ring-2'
: ''
"
class="flex flex-col select-none w-full gap-2 p-3 transition-all border cursor-pointer delivery-option focus-within:ring-2 ring-blue-500 ring-offset-2 focus-within:border-blue-500 rounded-100 border-slate-200 bg-slate-50" class="flex flex-col select-none w-full gap-2 p-3 transition-all border cursor-pointer delivery-option focus-within:ring-2 ring-blue-500 ring-offset-2 focus-within:border-blue-500 rounded-100 border-slate-200 bg-slate-50"
> >
<div class="flex items-center justify-between w-full"> <div class="flex items-center justify-between w-full">
@@ -161,15 +121,10 @@ whenever(
class="size-6 my-auto bg-white text-sm ms-1 flex items-center justify-center shadow-xl rounded-full transition-transform translate-x-0.5 will-change-transform data-[state=checked]:-translate-x-[68%]" class="size-6 my-auto bg-white text-sm ms-1 flex items-center justify-center shadow-xl rounded-full transition-transform translate-x-0.5 will-change-transform data-[state=checked]:-translate-x-[68%]"
/> />
</SwitchRoot> </SwitchRoot>
<span <span class="w-full text-slate-800 text-sm lg:text-[1rem]">پست پیشتاز</span>
class="w-full text-slate-800 text-sm lg:text-[1rem]"
>پست پیشتاز</span
>
</div> </div>
<span class="text-slate-800 text-sm lg:text-[1rem]"> <span class="text-slate-800 text-sm lg:text-[1rem]"> ۱۵۰٬۰۰۰ تومان </span>
۱۵۰٬۰۰۰ تومان
</span>
</div> </div>
</label> </label>
@@ -185,14 +140,10 @@ whenever(
class="size-6 my-auto bg-white text-sm ms-1 flex items-center justify-center shadow-xl rounded-full transition-transform translate-x-0.5 will-change-transform data-[state=checked]:-translate-x-[68%]" class="size-6 my-auto bg-white text-sm ms-1 flex items-center justify-center shadow-xl rounded-full transition-transform translate-x-0.5 will-change-transform data-[state=checked]:-translate-x-[68%]"
/> />
</SwitchRoot> </SwitchRoot>
<span class="w-full text-slate-800 text-sm lg:text-[1rem]" <span class="w-full text-slate-800 text-sm lg:text-[1rem]">تیپاکس</span>
>تیپاکس</span
>
</div> </div>
<span class="text-slate-800 text-sm lg:text-[1rem]"> <span class="text-slate-800 text-sm lg:text-[1rem]"> ۱۵۰٬۰۰۰ تومان </span>
۱۵۰٬۰۰۰ تومان
</span>
</label> </label>
</div> </div>
+21 -72
View File
@@ -4,9 +4,7 @@
import useVuelidate from "@vuelidate/core"; import useVuelidate from "@vuelidate/core";
import { helpers, required, minLength, email } from "@vuelidate/validators"; import { helpers, required, minLength, email } from "@vuelidate/validators";
import useGetAccount from "~/composables/api/account/useGetAccount"; import useGetAccount from "~/composables/api/account/useGetAccount";
import useUpdateAccount, { import useUpdateAccount, { type UpdateAccountRequest } from "~/composables/api/account/useUpdateAccount";
type UpdateAccountRequest,
} from "~/composables/api/account/useUpdateAccount";
import { useObjectTrack } from "~/composables/global/useObjectTrack"; import { useObjectTrack } from "~/composables/global/useObjectTrack";
import { useToast } from "~/composables/global/useToast"; import { useToast } from "~/composables/global/useToast";
import { QUERY_KEYS } from "~/constants"; import { QUERY_KEYS } from "~/constants";
@@ -40,18 +38,11 @@ const profilePictureModalIsShow = ref(false);
const { isNotEqual, clear: clearObjectTracker } = useObjectTrack(personalData); const { isNotEqual, clear: clearObjectTracker } = useObjectTrack(personalData);
const alises = ref([ const alises = ref(["شکارچی", "آیفون باز", "خوش سلیقه", "دست و دلباز", "چرم باز"]);
"شکارچی",
"آیفون باز",
"خوش سلیقه",
"دست و دلباز",
"چرم باز",
]);
// queries // queries
const { mutateAsync: updateAccount, isPending: updateAccountIsPending } = const { mutateAsync: updateAccount, isPending: updateAccountIsPending } = useUpdateAccount();
useUpdateAccount();
// computed // computed
@@ -59,52 +50,28 @@ const formRules = computed(() => {
return { return {
first_name: { first_name: {
required: helpers.withMessage("فیلد نام الزامی می باشد", required), required: helpers.withMessage("فیلد نام الزامی می باشد", required),
minLength: helpers.withMessage( minLength: helpers.withMessage("فیلد نام حداقل ۳ کرکتر می باشد", minLength(3)),
"فیلد نام حداقل ۳ کرکتر می باشد",
minLength(3)
),
}, },
last_name: { last_name: {
required: helpers.withMessage( required: helpers.withMessage("فیلد نام خانوادگی الزامی می باشد", required),
"فیلد نام خانوادگی الزامی می باشد", minLength: helpers.withMessage("فیلد نام خانوادگی حداقل ۳ کرکتر می باشد", minLength(3)),
required
),
minLength: helpers.withMessage(
"فیلد نام خانوادگی حداقل ۳ کرکتر می باشد",
minLength(3)
),
}, },
phone: { phone: {
required: helpers.withMessage( required: helpers.withMessage("فیلد شماره تلفن الزامی می باشد", required),
"فیلد شماره تلفن الزامی می باشد",
required
),
phoneValidator: helpers.withMessage( phoneValidator: helpers.withMessage(
"شماره تلفن وارد شده معتبر نمی باشد", "شماره تلفن وارد شده معتبر نمی باشد",
helpers.regex(/^0?[1-9][0-9]{9}$/) helpers.regex(/^0?[1-9][0-9]{9}$/)
), ),
}, },
gender: { gender: {
required: helpers.withMessage( required: helpers.withMessage("فیلد جنسیت الزامی می باشد", required),
"فیلد جنسیت الزامی می باشد",
required
),
}, },
email: { email: {
required: helpers.withMessage( required: helpers.withMessage("فیلد حساب الکترونیکی الزامی می باشد", required),
"فیلد حساب الکترونیکی الزامی می باشد", email: helpers.withMessage("حساب الکترونیکی وارد شده معتبر نمی باشد", email),
required
),
email: helpers.withMessage(
"حساب الکترونیکی وارد شده معتبر نمی باشد",
email
),
}, },
birth_date: { birth_date: {
required: helpers.withMessage( required: helpers.withMessage("فیلد تاریخ تولد الزامی می باشد", required),
"فیلد تاریخ تولد الزامی می باشد",
required
),
}, },
}; };
}); });
@@ -167,18 +134,14 @@ const handleSubmit = (withValidation: boolean) => {
<div <div
class="w-full flex flex-col lg:flex-row items-center max-lg:gap-5 lg:justify-between border p-6 rounded-xl border-slate-200" class="w-full flex flex-col lg:flex-row items-center max-lg:gap-5 lg:justify-between border p-6 rounded-xl border-slate-200"
> >
<div <div class="flex items-center justify-start gap-5 w-full lg:w-8/12">
class="flex items-center justify-start gap-5 w-full lg:w-8/12"
>
<div class="relative shrink-0 rounded-full flex-center"> <div class="relative shrink-0 rounded-full flex-center">
<Avatar <Avatar
class="!size-20 lg:!size-32" class="!size-20 lg:!size-32"
:src="account!.profile_photo" :src="account!.profile_photo"
:alt=" :alt="
account?.first_name && account?.last_name account?.first_name && account?.last_name
? `${account?.first_name.charAt( ? `${account?.first_name.charAt(0)} ${account?.last_name.charAt(0)}`
0
)} ${account?.last_name.charAt(0)}`
: 'بدون نام کاربری' : 'بدون نام کاربری'
" "
/> />
@@ -192,18 +155,12 @@ const handleSubmit = (withValidation: boolean) => {
<div class="flex flex-col gap-2 lg:gap-3"> <div class="flex flex-col gap-2 lg:gap-3">
<span class="typo-sub-h-md lg:typo-sub-h-lg" <span class="typo-sub-h-md lg:typo-sub-h-lg"
>{{ account?.first_name }} >{{ account?.first_name }} {{ account?.last_name }}</span
{{ account?.last_name }}</span
> >
<span <span class="typo-sub-h-xs lg:typo-sub-h-sm !font-light text-slate-600 leading-[200%]">
class="typo-sub-h-xs lg:typo-sub-h-sm !font-light text-slate-600 leading-[200%]" با اولین خریدتون هوش مصنوعی وبسایتمون واستون یک بایوگرافی درست میکنه :)
>
با اولین خریدتون هوش مصنوعی وبسایتمون واستون یک
بایوگرافی درست میکنه :)
</span> </span>
<div <div class="flex-center border border-yellow-500 pe-3.5 ps-1 w-max rounded-full">
class="flex-center border border-yellow-500 pe-3.5 ps-1 w-max rounded-full"
>
<div class="rounded-full p-1.5 lg:p-2"> <div class="rounded-full p-1.5 lg:p-2">
<Icon <Icon
name="bi:patch-check" name="bi:patch-check"
@@ -211,26 +168,20 @@ const handleSubmit = (withValidation: boolean) => {
size="20" size="20"
/> />
</div> </div>
<span class="text-[10px] lg:text-xs text-yellow-500" <span class="text-[10px] lg:text-xs text-yellow-500">جزو ۳ مشتری برتر</span>
>جزو ۳ مشتری برتر</span
>
</div> </div>
</div> </div>
</div> </div>
<div class="flex flex-col items-start gap-3 w-full lg:w-4/12"> <div class="flex flex-col items-start gap-3 w-full lg:w-4/12">
<span class="typo-sub-h-md lg:typo-sub-h-lg" <span class="typo-sub-h-md lg:typo-sub-h-lg">لقب های شما</span>
>لقب های شما</span
>
<span class="flex w-full flex-wrap gap-2"> <span class="flex w-full flex-wrap gap-2">
<span <span
v-for="(alise, index) in alises" v-for="(alise, index) in alises"
:key="index" :key="index"
class="flex-center bg-slate-50 border border-slate-200 py-1.5 lg:py-2 px-3 w-max rounded-full" class="flex-center bg-slate-50 border border-slate-200 py-1.5 lg:py-2 px-3 w-max rounded-full"
> >
<span class="text-[10px] lg:text-xs text-black">{{ <span class="text-[10px] lg:text-xs text-black">{{ alise }}</span>
alise
}}</span>
</span> </span>
</span> </span>
</div> </div>
@@ -251,9 +202,7 @@ const handleSubmit = (withValidation: boolean) => {
<span v-else> ثبت تغییرات </span> <span v-else> ثبت تغییرات </span>
</Button> </Button>
</template> </template>
<div <div class="w-full grid grid-cols-1 lg:grid-cols-2 gap-x-3 gap-y-5">
class="w-full grid grid-cols-1 lg:grid-cols-2 gap-x-3 gap-y-5"
>
<DataField <DataField
id="personal-data-name" id="personal-data-name"
label="نام" label="نام"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

+4 -19
View File
@@ -109,10 +109,7 @@ declare global {
colors: string[]; colors: string[];
}; };
type ProductListItem = Pick< type ProductListItem = Pick<Product, "id" | "variants" | "name" | "rating" | "slug" | "category" | "colors">;
Product,
"id" | "variants" | "name" | "rating" | "slug" | "category" | "colors"
>;
type Article = { type Article = {
id: number; id: number;
@@ -193,13 +190,7 @@ declare global {
id: number; id: number;
count: number; count: number;
images: string[]; images: string[];
status: status: "ADMIN_PENDING" | "PENDING" | "POSTED" | "RECEIVED" | "CANCELED" | "REFUND";
| "ADMIN_PENDING"
| "PENDING"
| "POSTED"
| "RECEIVED"
| "CANCELED"
| "REFUND";
verbose_status: string; verbose_status: string;
is_paid: boolean; is_paid: boolean;
created_at: string; created_at: string;
@@ -248,6 +239,7 @@ declare global {
cart_total: string; cart_total: string;
tax: string; tax: string;
final_price: string; final_price: string;
address: Address;
}; };
type ServerFile = { type ServerFile = {
@@ -281,14 +273,7 @@ declare global {
id: number; id: number;
picture: string; picture: string;
title: string; title: string;
type: type: "ZARINPAL" | "SEP" | "MELLAT" | "IDPAY" | "ZIBAL" | "BAHAMTA" | "BMI";
| "ZARINPAL"
| "SEP"
| "MELLAT"
| "IDPAY"
| "ZIBAL"
| "BAHAMTA"
| "BMI";
}; };
type Transaction = { type Transaction = {