tried fixing infinite loop
@@ -41,12 +41,30 @@ jobs:
|
||||
username: ${{ secrets.SSH_USER }}
|
||||
password: ${{ secrets.SSH_PASSWORD }}
|
||||
script: |
|
||||
cd /root/hshop/
|
||||
cd /root/hshop/ || { echo "ERROR: دایرکتوری پیدا نشد"; exit 1; }
|
||||
|
||||
docker compose down --remove-orphans --timeout 60
|
||||
docker compose down --remove-orphans --timeout 10
|
||||
|
||||
docker compose up --build --detach --remove-orphans
|
||||
|
||||
docker image prune -af
|
||||
|
||||
docker compose ps
|
||||
docker compose up --build --detach || { echo "ERROR: ارور در بیلد"; exit 1; }
|
||||
|
||||
- name: display active containers
|
||||
uses: appleboy/ssh-action@v0.1.6
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SSH_USER }}
|
||||
password: ${{ secrets.SSH_PASSWORD }}
|
||||
script: |
|
||||
cd /root/hshop/ || { echo "ERROR: دایرکتوری پیدا نشد"; exit 1; }
|
||||
docker compose ps
|
||||
|
||||
- name: clean up server
|
||||
uses: appleboy/ssh-action@v0.1.6
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SSH_USER }}
|
||||
password: ${{ secrets.SSH_PASSWORD }}
|
||||
script: |
|
||||
|
||||
docker image prune -af --filter "until=48h" || { echo "ERROR: ارور در پاک کردن images";}
|
||||
|
||||
docker builder prune -af --filter "until=48h" || { echo "ERROR: ارور در پاک کردن builder cache";}
|
||||
|
||||
@@ -43,10 +43,6 @@ class SendOTPView(APIView):
|
||||
Code: {otp}"""
|
||||
sms_api = ghasedak_sms.Ghasedak(api_key="1227eaaddcba72bcb0169b37032cf16ae9ac6ed8b3b7c2768b74e2ee351d1b52gyRe3AGomZRPTNEd")
|
||||
|
||||
# response = sms_api.send_single_sms(ghasedak_sms.SendSingleSmsInput(message=message, receptor=phone, line_number='30005006006908', send_date='', client_reference_id=''))
|
||||
# print(response)
|
||||
|
||||
|
||||
|
||||
response = sms_api.send_single_sms(
|
||||
ghasedak_sms.SendSingleSmsInput(
|
||||
@@ -57,8 +53,6 @@ Code: {otp}"""
|
||||
)
|
||||
)
|
||||
|
||||
# response = sms_api.send_otp_sms(otp_input)
|
||||
|
||||
if response['statusCode'] == 200:
|
||||
return Response({'detail': f'OTP sent successfully {otp}'}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
|
||||
@@ -241,19 +241,19 @@ AWS_S3_OBJECT_PARAMETERS = {
|
||||
|
||||
AZ_IRANIAN_BANK_GATEWAYS = {
|
||||
"GATEWAYS": {
|
||||
"ZARINPAL": {
|
||||
"MERCHANT_CODE": "9cf93a18-dc99-4e6c-8873-d37a8190a027",
|
||||
"SANDBOX": 0,
|
||||
"ZIBAL": {
|
||||
"MERCHANT_CODE": "zibal",
|
||||
"SANDBOX": True
|
||||
},
|
||||
},
|
||||
"IS_SAMPLE_FORM_ENABLE": True,
|
||||
"DEFAULT": "ZARINPAL",
|
||||
"DEFAULT": "ZIBAL",
|
||||
"CURRENCY": "IRT",
|
||||
"TRACKING_CODE_QUERY_PARAM": "tc",
|
||||
"TRACKING_CODE_LENGTH": 16,
|
||||
"SETTING_VALUE_READER_CLASS": "azbankgateways.readers.DefaultReader",
|
||||
"BANK_PRIORITIES": [
|
||||
"ZARINPAL",
|
||||
"ZIBAL",
|
||||
],
|
||||
"IS_SAFE_GET_GATEWAY_PAYMENT": False # better to be True
|
||||
}
|
||||
@@ -32,7 +32,7 @@ UNFOLD = {
|
||||
lambda request: static("rtl.css"),
|
||||
],
|
||||
|
||||
"BORDER_RADIUS": "20px",
|
||||
"BORDER_RADIUS": "8px",
|
||||
"SHOW_HISTORY": True,
|
||||
"SHOW_VIEW_ON_SITE": True,
|
||||
"ENVIRONMENT": "core.settings.environment_callback",
|
||||
@@ -47,7 +47,7 @@ UNFOLD = {
|
||||
"500": "115 115 115",
|
||||
"600": "82 82 82",
|
||||
"700": "64 64 64",
|
||||
"800": "38 38 38",
|
||||
"800": "42 42 42",
|
||||
"900": "23 23 23",
|
||||
"950": "10 10 10"
|
||||
},
|
||||
|
||||
@@ -1,88 +1,51 @@
|
||||
@layer base {
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-Thin.woff2");
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-UltraLight.woff2");
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-Light.woff2");
|
||||
font-family: "Peyda";
|
||||
src: url("./fonts/peyda/300-PeydaWeb-Light-fanum.woff2");
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-Regular.woff2");
|
||||
font-family: "Peyda";
|
||||
src: url("./fonts/peyda/400-PeydaWeb-Regular-fanum.woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-Medium.woff2");
|
||||
font-family: "Peyda";
|
||||
src: url("./fonts/peyda/500-PeydaWeb-Medium-fanum.woff2");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-DemiBold.woff2");
|
||||
font-family: "Peyda";
|
||||
src: url("./fonts/peyda/600-PeydaWeb-SemiBold-fanum.woff2");
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-Bold.woff2");
|
||||
font-family: "Peyda";
|
||||
src: url("./fonts/peyda/700-PeydaWeb-Bold-fanum.woff2");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-ExtraBold.woff2");
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-Black.woff2");
|
||||
font-family: "Peyda";
|
||||
src: url("./fonts/peyda/900-PeydaWeb-Black-fanum.woff2");
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-ExtraBlack.woff2");
|
||||
font-weight: 950;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-Heavy.woff2");
|
||||
font-weight: 1000;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
@layer base {
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-Thin.woff2");
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-UltraLight.woff2");
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-Light.woff2");
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-Regular.woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-Medium.woff2");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-DemiBold.woff2");
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-Bold.woff2");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-ExtraBold.woff2");
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-Black.woff2");
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-ExtraBlack.woff2");
|
||||
font-weight: 950;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IRANYekanXVF";
|
||||
src: url("./fonts/IranYekanX/IRANYekanX-Heavy.woff2");
|
||||
font-weight: 1000;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
*:not(span){
|
||||
font-family: 'IRANYekanXVF' !important;
|
||||
font-family: 'Peyda' !important;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch import receiver
|
||||
from .models import OrderModel
|
||||
from account.models import PushSubscription
|
||||
import ghasedak_sms
|
||||
|
||||
@receiver(pre_save, sender=OrderModel)
|
||||
def order_status_changed(sender, instance, **kwargs):
|
||||
@@ -9,10 +11,36 @@ def order_status_changed(sender, instance, **kwargs):
|
||||
|
||||
if previous.status != instance.status:
|
||||
send_change_status_notif(instance)
|
||||
send_change_status_sms(instance)
|
||||
|
||||
|
||||
def send_change_status_notif(order):
|
||||
pass
|
||||
|
||||
def send_change_status_notif(instance):
|
||||
user_subs = PushSubscription.objects.filter(user=instance.user)
|
||||
for user_sub in user_subs:
|
||||
try:
|
||||
user_sub.send_notif(f'سفارش شما به {instance.get_status_display()} تغییر کرد', f'سفارش شما به {instance.get_status_display()} تغییر کرد', ProductImageModel.objects.all().first().image.url)
|
||||
except:
|
||||
print('log later send notif error')
|
||||
|
||||
|
||||
def send_change_status_sms(instance):
|
||||
sms_api = ghasedak_sms.Ghasedak(api_key="1227eaaddcba72bcb0169b37032cf16ae9ac6ed8b3b7c2768b74e2ee351d1b52gyRe3AGomZRPTNEd")
|
||||
|
||||
|
||||
response = sms_api.send_single_sms(
|
||||
ghasedak_sms.SendSingleSmsInput(
|
||||
message=f'سفارش شما به {instance.get_status_display()} تغییر کرد',
|
||||
receptor=instance.user.phone,
|
||||
line_number='30005006004095',
|
||||
client_reference_id=str(instance.user.pk)
|
||||
)
|
||||
)
|
||||
if response['statusCode'] == 200:
|
||||
print('done log later')
|
||||
else:
|
||||
print(f'error: {response}')
|
||||
|
||||
|
||||
def update_cart_price_fields(order):
|
||||
pass
|
||||
|
||||
@@ -202,7 +202,7 @@ class PaymentView(APIView):
|
||||
factory = bankfactories.BankFactory()
|
||||
try:
|
||||
bank = (
|
||||
factory.create(bank_models.BankType.ZARINPAL)
|
||||
factory.auto_create()
|
||||
)
|
||||
bank.set_request(request)
|
||||
bank.set_amount(amount)
|
||||
@@ -220,7 +220,6 @@ class PaymentView(APIView):
|
||||
except AZBankGatewaysException as e:
|
||||
print(e)
|
||||
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({'gateway_url': bank.redirect_url}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class ProductVariantSerialzier(serializers.ModelSerializer):
|
||||
return item['quantity']
|
||||
return 0
|
||||
|
||||
def get_pirce(self, obj):
|
||||
def get_price(self, obj):
|
||||
return f'{obj.price:,.0f} تومان'
|
||||
|
||||
|
||||
|
||||
@@ -35,6 +35,6 @@ const closeModal = () => {
|
||||
/>
|
||||
</ToastProvider>
|
||||
|
||||
<VueQueryDevtools dir="ltr" buttonPosition="bottom-left" />
|
||||
<VueQueryDevtools dir="ltr" buttonPosition="bottom-right" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@layer base {
|
||||
@font-face {
|
||||
font-family: "Morabba";
|
||||
src: url("/fonts/Morabba/IRANYekanX-UltraLight.woff2");
|
||||
src: url("/fonts/Morabba/Morabba-UltraLight.woff2");
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
|
||||
@@ -109,7 +109,7 @@ watch(
|
||||
(newValue) => {
|
||||
if (!isEditing.value) {
|
||||
addressData.value.phone =
|
||||
newValue == "بله" ? account.value?.phone : "";
|
||||
newValue == "بله" ? account.value!.phone : "";
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -21,18 +21,17 @@ const discountCode = ref(cart.value?.discount_code?.code || "");
|
||||
|
||||
const {
|
||||
mutateAsync: submitDiscountCode,
|
||||
isPending: submitDiscountCodeIsPending,
|
||||
isPending: submitDiscountCodeIsPending
|
||||
} = useSubmitDiscountCode();
|
||||
|
||||
const {
|
||||
mutateAsync: deleteDiscountCode,
|
||||
isPending: deleteDiscountCodeIsPending,
|
||||
isPending: deleteDiscountCodeIsPending
|
||||
} = useDeleteDiscountCode();
|
||||
|
||||
// computed
|
||||
|
||||
const nextPage: ComputedRef<{ name: string; label: string } | undefined> =
|
||||
computed(() => route.meta.nextPage);
|
||||
const nextPage = computed(() => route.meta.nextPage as { name: string; label: string } | undefined);
|
||||
|
||||
const hasSubmittedDiscountCode = computed(() => !!cart.value?.discount_code);
|
||||
|
||||
@@ -49,11 +48,11 @@ const handleSubmitDiscountCode = () => {
|
||||
addToast({
|
||||
message: "خطایی در ثبت کد تخفیف رخ داد",
|
||||
options: {
|
||||
status: "error",
|
||||
},
|
||||
status: "error"
|
||||
}
|
||||
});
|
||||
discountCode.value = "";
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -68,11 +67,11 @@ const handleDeleteDiscountCode = () => {
|
||||
addToast({
|
||||
message: "خطایی در حذف کد تخفیف رخ داد",
|
||||
options: {
|
||||
status: "error",
|
||||
},
|
||||
status: "error"
|
||||
}
|
||||
});
|
||||
discountCode.value = "";
|
||||
},
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -27,7 +27,7 @@ const { isLoading: cartImageIsLoading } = useImage({
|
||||
v-if="!cartImageIsLoading"
|
||||
class="size-[3.5rem] shrink-0 rounded-100 border border-gray-300 overflow-hidden"
|
||||
>
|
||||
<img :src="image" alt="product" class="object-conver" />
|
||||
<NuxtImg :src="image" alt="product" class="object-conver" />
|
||||
</div>
|
||||
<Skeleton
|
||||
v-else
|
||||
|
||||
@@ -44,7 +44,7 @@ const { data: cart, isLoading: cartIsLoading } = useGetCartOrders();
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="cart?.items.length > 5"
|
||||
v-if="cart && cart.items.length > 5"
|
||||
class="h-7 flex-center col-span-full lg:hidden"
|
||||
>
|
||||
<button
|
||||
|
||||
@@ -121,7 +121,7 @@ watch(
|
||||
v-if="!cartImageIsLoading"
|
||||
class="size-[4rem] lg:size-[12rem] aspect-square shrink-0 rounded-xl border border-slate-200 overflow-hidden"
|
||||
>
|
||||
<img
|
||||
<NuxtImg
|
||||
:src="data.product.image"
|
||||
class="object-cover size-full"
|
||||
alt="product"
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useImage } from "@vueuse/core";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
src: string;
|
||||
src: string | null | undefined;
|
||||
alt: string;
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ const { src } = toRefs(props);
|
||||
|
||||
// state
|
||||
|
||||
const { isLoading } = useImage({ src: src.value });
|
||||
const { isLoading } = useImage({ src: src.value ?? '' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -26,11 +26,12 @@ const { isLoading } = useImage({ src: src.value });
|
||||
class="flex-center size-full select-none rounded-full align-middle overflow-hidden inset-shadow-black/20 inset-shadow-sm"
|
||||
>
|
||||
<Skeleton
|
||||
v-if="isLoading"
|
||||
v-if="isLoading && !!src"
|
||||
class="w-full !h-[110%] !rounded-full aspect-square"
|
||||
/>
|
||||
<template v-else>
|
||||
<AvatarImage
|
||||
v-if="!!src"
|
||||
class="!size-full rounded-full object-cover"
|
||||
:src="src"
|
||||
:alt="alt"
|
||||
|
||||
@@ -36,7 +36,7 @@ const remaining = computed(() => items.value.length - max.value);
|
||||
:class="index < 0 ? '' : ''"
|
||||
:style="{ width: size, height: size, zIndex: index + 2 }"
|
||||
>
|
||||
<img
|
||||
<NuxtImg
|
||||
:src="item"
|
||||
alt="avatar"
|
||||
class="rounded-full object-cover w-full h-full"
|
||||
|
||||
@@ -54,7 +54,7 @@ const createdAt = usePersianTimeAgo(new Date(date.value));
|
||||
{{ category.name }}
|
||||
</Tag>
|
||||
|
||||
<img
|
||||
<NuxtImg
|
||||
:src="image"
|
||||
class="group-hover:scale-105 transition-transform duration-200 absolute size-full object-cover z-10"
|
||||
alt=""
|
||||
@@ -104,7 +104,7 @@ const createdAt = usePersianTimeAgo(new Date(date.value));
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
<NuxtImg
|
||||
v-if="variant === 'lg'"
|
||||
:src="image"
|
||||
class="group-hover:scale-105 transition-transform duration-200 absolute size-full object-cover z-10"
|
||||
|
||||
@@ -25,7 +25,7 @@ const { colorObject } = useImageColor(`#category-image-${id.value}`);
|
||||
<template>
|
||||
<NuxtLink :to="`/products?category=${id}`">
|
||||
<div class="group relative rounded-150 overflow-hidden w-full aspect-square bg-white brightness-[97%]">
|
||||
<img
|
||||
<NuxtImg
|
||||
:id="`category-image-${id}`"
|
||||
class="group-hover:scale-105 transition-transform duration-200 absolute object-contain size-full"
|
||||
:src="picture"
|
||||
|
||||
@@ -37,7 +37,7 @@ const value = ref<OptionChildren>();
|
||||
|
||||
watch(
|
||||
() => value.value,
|
||||
(newValue: OptionChildren) => {
|
||||
(newValue) => {
|
||||
if (!!newValue) {
|
||||
emit("update:modelValue", newValue.id);
|
||||
}
|
||||
@@ -51,7 +51,7 @@ watch(
|
||||
.flatMap((option) => option.children)
|
||||
.find((child) => child.id == newValue);
|
||||
|
||||
value.value = target || null;
|
||||
value.value = target || undefined;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
@@ -35,8 +35,8 @@ const fileLimit = 1024 * 1024 * 2;
|
||||
// methods
|
||||
|
||||
const onDrop = (files: File[] | null) => {
|
||||
if (modelValue.value.length < 3) {
|
||||
files?.forEach((file, index) => {
|
||||
if (modelValue.value.length < 3 && files) {
|
||||
files.forEach((file, index) => {
|
||||
if (file.size > fileLimit) {
|
||||
files.splice(index, 1);
|
||||
addToast({
|
||||
@@ -51,7 +51,7 @@ const onDrop = (files: File[] | null) => {
|
||||
emit("update:modelValue", [...files.slice(0, 3)]);
|
||||
} else {
|
||||
if (modelValue.value.length + files.length <= 3) {
|
||||
files?.forEach((item) => {
|
||||
files.forEach((item) => {
|
||||
emit("change", item);
|
||||
resetFileDialog();
|
||||
});
|
||||
@@ -110,7 +110,7 @@ const removeAttachment = (id: number) => {
|
||||
<div class="flex flex-col w-full h-max gap-5 pt-8">
|
||||
<div
|
||||
ref="dropZoneRef"
|
||||
@click="openDialog"
|
||||
@click="openDialog()"
|
||||
class="bg-slate-50 relative flex-col-center w-full transition-all text-black/50 gap-3 h-[20rem] border border-dashed rounded-xl cursor-pointer select-none"
|
||||
:class="{
|
||||
'border-black': isOverDropZone,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="bg-black relative overflow-hidden">
|
||||
|
||||
<img src="/img/footer-bg.jpg" alt="" class="absolute z-10 object-cover opacity-45" />
|
||||
<NuxtImg src="/img/footer-bg.jpg" alt="" class="absolute z-10 object-cover opacity-45" />
|
||||
|
||||
<div class="flex flex-col gap-4 items-center justify-center relative z-20">
|
||||
|
||||
|
||||
@@ -55,9 +55,9 @@ onMounted(() => {
|
||||
id="loading-overlay"
|
||||
class="fixed inset-0 size-full z-9999 flex-center bg-black"
|
||||
>
|
||||
<img
|
||||
<NuxtImg
|
||||
id="loading-overlay-image"
|
||||
src="/img/heymlz-loading-1.gif"
|
||||
src="/img/heymlz/heymlz-loading-1.gif"
|
||||
class="opacity-0 scale-70 absolute z-20"
|
||||
alt=""
|
||||
/>
|
||||
|
||||
@@ -11,22 +11,22 @@ type Highlight = {
|
||||
|
||||
const highlights = ref<Highlight[]>([
|
||||
{
|
||||
icon: "/img/footer-support.svg",
|
||||
icon: "/img/heymlz/footer-support.svg",
|
||||
title: "خدمات مشتری",
|
||||
description: "پشتیبانی استثنایی، راهحلهای پایدار برای شما",
|
||||
},
|
||||
{
|
||||
icon: "/img/footer-send.svg",
|
||||
icon: "/img/heymlz/footer-send.svg",
|
||||
title: "ارسال سریع و رایگان",
|
||||
description: "ارسال رایگان برای سفارشهای بالای ۱۵۰ دلار",
|
||||
},
|
||||
{
|
||||
icon: "/img/footer-share.svg",
|
||||
icon: "/img/heymlz/footer-share.svg",
|
||||
title: "معرفی به دوستان",
|
||||
description: "ما را به دوستان خود معرفی کنید",
|
||||
},
|
||||
{
|
||||
icon: "/img/footer-security.svg",
|
||||
icon: "/img/heymlz/footer-security.svg",
|
||||
title: "پرداخت امن",
|
||||
description: "پرداخت شما بهصورت امن پردازش میشود",
|
||||
},
|
||||
@@ -40,7 +40,7 @@ const highlights = ref<Highlight[]>([
|
||||
>
|
||||
<template v-for="(highlight, index) in highlights" :key="index">
|
||||
<div class="flex flex-col-center gap-[.75rem] px-5">
|
||||
<img
|
||||
<NuxtImg
|
||||
:src="highlight.icon"
|
||||
class="size-[70px] md:size-[90px]"
|
||||
alt=""
|
||||
|
||||
@@ -54,7 +54,7 @@ const changeSlide = (id: number) => {
|
||||
class="bg-white brightness-[97%] w-full relative aspect-square overflow-hidden rounded-[12px] md:rounded-200"
|
||||
>
|
||||
<Transition name="zoom" mode="out-in">
|
||||
<img
|
||||
<NuxtImg
|
||||
:key="selectedSlideDetail.id"
|
||||
class="size-full absolute object-contain"
|
||||
:src="selectedSlideDetail.image"
|
||||
@@ -84,7 +84,7 @@ const changeSlide = (id: number) => {
|
||||
"
|
||||
class="active:scale-95 hover:border-slate-200 transition-all cursor-pointer brightness-[97%] bg-white aspect-square border-2 rounded-[12px] md:rounded-200 w-full overflow-hidden relative"
|
||||
>
|
||||
<img
|
||||
<NuxtImg
|
||||
class="absolute object-cover size-full"
|
||||
:src="slide.image"
|
||||
:alt="String(slide.id)"
|
||||
|
||||
@@ -12,12 +12,12 @@ const { selectedVariant } = inject("productVariant") as ProductVariantProvideTyp
|
||||
<div class="w-full flex flex-col">
|
||||
<AccordionRoot
|
||||
class="w-full last:border-b last:border-slate-200"
|
||||
:default-value="'item' + selectedVariant.details[0].detail_category"
|
||||
:default-value="'item' + selectedVariant!.details[0].detail_category"
|
||||
type="single"
|
||||
:collapsible="true"
|
||||
>
|
||||
<AccordionItem
|
||||
v-for="detailItem in selectedVariant.details"
|
||||
v-for="detailItem in selectedVariant!.details"
|
||||
:value="'item' + detailItem.detail_category"
|
||||
class="overflow-hidden"
|
||||
>
|
||||
|
||||
@@ -36,7 +36,7 @@ const { colorObject } = useImageColor(`#product-image-${id.value}`);
|
||||
<div
|
||||
class="group relative size-full aspect-square rounded-xl @[280px]:rounded-2xl bg-white brightness-[98%] overflow-hidden p-6"
|
||||
>
|
||||
<img
|
||||
<NuxtImg
|
||||
:id="`product-image-${id}`"
|
||||
:src="picture"
|
||||
class="group-hover:scale-105 transition-transform duration-200 size-full object-contain absolute inset-0"
|
||||
|
||||
@@ -20,7 +20,7 @@ const { picture, price, title, color } = toRefs(props);
|
||||
<div class="max-w-[500px] w-full h-[116px] flex items-center justify-between py-2 pe-6 ps-2 bg-white rounded-150">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative size-[100px] rounded-100 overflow-hidden border-[0.5px] border-slate-200">
|
||||
<img :src="picture" :alt="title" class="object-cover absolute" />
|
||||
<NuxtImg :src="picture" :alt="title" class="object-cover absolute" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="typo-sub-h-md text-black">{{ title }}</span>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { PRODUCT_RANGE } from "~/constants";
|
||||
|
||||
// state
|
||||
|
||||
const params: GetProductsFilters = inject("params");
|
||||
const params = inject("params") as GetProductsFilters;
|
||||
|
||||
const sort_filter = ref([
|
||||
{ title: "جدیدترین ها", value: "newest" },
|
||||
@@ -22,12 +22,12 @@ const sliderValue = ref([
|
||||
params.price_lte ?? PRODUCT_RANGE.max,
|
||||
]);
|
||||
|
||||
const has_discount = ref(JSON.parse(params.has_discount ?? false));
|
||||
const in_stock = ref(JSON.parse(params.in_stock ?? false));
|
||||
const has_discount = ref(Boolean(params.has_discount) ?? false);
|
||||
const in_stock = ref(Boolean(params.in_stock) ?? false);
|
||||
|
||||
const sliderValueDebounced = refDebounced(sliderValue, 1000);
|
||||
|
||||
const filtersSuccessMessage = ref<{ title: string; status: string } | null>("");
|
||||
const filtersSuccessMessage = ref<{ title: string; status: string } | null>(null);
|
||||
|
||||
// queries
|
||||
|
||||
|
||||
@@ -25,14 +25,14 @@ const onSwiper = (swiper: SwiperClass) => {
|
||||
class="flex flex-col justify-center gap-4 bg-black h-[150svh] relative overflow-hidden"
|
||||
>
|
||||
|
||||
<div class="w-full flex justify-center items-center relative z-10">
|
||||
<span class="text-white typo-h-6 md:typo-h-5 lg:typo-h-4">
|
||||
دسته بندی ها
|
||||
</span>
|
||||
</div>
|
||||
<!-- <div class="w-full flex justify-center items-center relative z-10">-->
|
||||
<!-- <span class="text-white typo-h-6 md:typo-h-5 lg:typo-h-4">-->
|
||||
<!-- دسته بندی ها-->
|
||||
<!-- </span>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<img
|
||||
src="/img/gradient-2.png"
|
||||
<NuxtImg
|
||||
src="/img/categories-gradient.png"
|
||||
class="animate-spin [animation-duration:16s] object-cover absolute size-full brightness-45 scale-115 aspect-square"
|
||||
:style="{
|
||||
maskImage: 'radial-gradient(black, transparent 50%)'
|
||||
@@ -41,6 +41,17 @@ const onSwiper = (swiper: SwiperClass) => {
|
||||
/>
|
||||
|
||||
<div class="w-full my-20 relative">
|
||||
<video
|
||||
class="aspect-square w-[450px] translate-[-253px] absolute left-1/2 -translate-x-1/2 z-10"
|
||||
:style="{
|
||||
filter: 'drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.4))',
|
||||
}"
|
||||
src="/video/heymlz/heymlz-seat-2.webm"
|
||||
autoplay
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
muted
|
||||
/>
|
||||
<Swiper
|
||||
:loop="true"
|
||||
:centered-slides="true"
|
||||
@@ -98,7 +109,8 @@ const onSwiper = (swiper: SwiperClass) => {
|
||||
|
||||
<div class="w-full flex justify-center items-center">
|
||||
<NuxtLink to="/category">
|
||||
<Button variant="solid" class="invert rounded-full max-xs:typo-label-sm !px-4 xs:!px-8" end-icon="ci:arrow-left">
|
||||
<Button variant="solid" class="invert rounded-full max-xs:typo-label-sm !px-4 xs:!px-8"
|
||||
end-icon="ci:arrow-left">
|
||||
مشاهده همه دسته ها
|
||||
</Button>
|
||||
</NuxtLink>
|
||||
|
||||
@@ -193,7 +193,7 @@ onUnmounted(() => {
|
||||
/>
|
||||
</template>
|
||||
|
||||
<img
|
||||
<NuxtImg
|
||||
v-else
|
||||
class="absolute inset-0 size-full object-cover"
|
||||
:src="slide.image!"
|
||||
|
||||
@@ -28,7 +28,7 @@ const onSwiper = (swiper: SwiperClass) => {
|
||||
|
||||
<template>
|
||||
<div class="relative max-h-[700px] flex justify-center items-center h-svh w-full">
|
||||
<img
|
||||
<NuxtImg
|
||||
class="absolute size-full object-cover"
|
||||
src="/img/hero-bg.jpg"
|
||||
alt=""
|
||||
|
||||
@@ -59,7 +59,7 @@ watch(
|
||||
class="rounded-200 overflow-hidden h-[70svh] md:h-[80svh] relative"
|
||||
>
|
||||
<Transition name="fade">
|
||||
<img
|
||||
<NuxtImg
|
||||
v-if="activeSlideVideo !== 'right'"
|
||||
:src="homeData!.difreance_section.image1"
|
||||
class="select-none absolute size-full object-cover brightness-[95%]"
|
||||
@@ -77,8 +77,9 @@ watch(
|
||||
</Transition>
|
||||
|
||||
<div class="absolute size-full right-0 w-full">
|
||||
|
||||
<Transition name="fade">
|
||||
<img
|
||||
<NuxtImg
|
||||
v-if="activeSlideVideo !== 'left'"
|
||||
:src="homeData!.difreance_section.image2"
|
||||
class="overlay-image select-none absolute object-cover size-full brightness-[95%]"
|
||||
@@ -94,12 +95,27 @@ watch(
|
||||
class="overlay-image select-none absolute object-cover size-full brightness-[95%]"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<video
|
||||
src="/video/heymlz/heymlz-pulling.webm"
|
||||
autoplay
|
||||
muted
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
class="size-[300px] absolute translate-x-[-100px] z-10 top-[32%] -translate-y-1/2"
|
||||
:style="{
|
||||
left: `${clipPathPercent}%`,
|
||||
filter: 'drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.3))'
|
||||
}"
|
||||
/>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
left: `${clipPathPercent}%`,
|
||||
}"
|
||||
left: `${clipPathPercent}%`,
|
||||
}"
|
||||
class="select-none w-2 h-full bg-black absolute left-0 flex items-center justify-center"
|
||||
>
|
||||
|
||||
<div
|
||||
ref="draggableEl"
|
||||
class="cursor-grab hover:scale-115 transition-transform rounded-full absolute bg-black size-11 flex items-center justify-center"
|
||||
|
||||
@@ -93,7 +93,7 @@ onUnmounted(() => {
|
||||
class="showcase-slide origin-bottom absolute size-full bg-transparent flex items-center justify-center"
|
||||
>
|
||||
|
||||
<img
|
||||
<NuxtImg
|
||||
class="w-[280px] xs:w-[350px] lg:w-[500px] xl:w-[650px] z-20 mb-30 sm:mb-20 lg:mb-30"
|
||||
:src="slide.image"
|
||||
:style="{
|
||||
|
||||
@@ -29,17 +29,17 @@ const {
|
||||
isPending: isChatPending,
|
||||
isFetchingNextPage: isNextChatPagePending,
|
||||
hasNextPage: hasMoreChat,
|
||||
fetchNextPage: loadMoreChat,
|
||||
fetchNextPage: loadMoreChat
|
||||
} = useGetChat(id, isOpen);
|
||||
const isCreateMessagePending = useIsMutating({
|
||||
mutationKey: [MUTATION_KEYS.create_chat],
|
||||
mutationKey: [MUTATION_KEYS.create_chat]
|
||||
});
|
||||
|
||||
const canLoadMoreChat = ref(false);
|
||||
|
||||
const isChatScrollLocked = useScrollLock(chatContainerEl);
|
||||
const { y: chatContainerScrollY } = useScroll(chatContainerEl, {
|
||||
behavior: "smooth",
|
||||
behavior: "smooth"
|
||||
});
|
||||
|
||||
useInfiniteScroll(
|
||||
@@ -56,7 +56,7 @@ useInfiniteScroll(
|
||||
distance: 10,
|
||||
direction: "top",
|
||||
throttle: 1000,
|
||||
canLoadMore: () => canLoadMoreChat.value,
|
||||
canLoadMore: () => canLoadMoreChat.value
|
||||
}
|
||||
);
|
||||
|
||||
@@ -116,7 +116,7 @@ whenever(
|
||||
}, 2000);
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
once: true
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -125,7 +125,7 @@ whenever(
|
||||
<Transition name="fade-right-to-left">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="fixed right-8 bottom-8 w-[450px] transition-all duration-500 overflow-hidden h-[700px] rounded-250 shadow-2xl shadow-black/30 pt-[40px] bg-white"
|
||||
class="fixed z-50 right-8 bottom-8 w-[450px] transition-all duration-500 overflow-hidden h-[700px] rounded-250 shadow-2xl shadow-black/30 pt-[40px] bg-white"
|
||||
>
|
||||
<CloseButton :disabled="!!isCreateMessagePending" />
|
||||
|
||||
@@ -187,6 +187,7 @@ whenever(
|
||||
class="text-black p-4.5 size-full flex justify-center items-center"
|
||||
v-else
|
||||
>
|
||||
<img class="size-[50px]" src="/img/heymlz/heymlz-idle.gif" alt="" />
|
||||
Please sign in first
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,18 +13,18 @@ provide("isOpen", {
|
||||
isOpen,
|
||||
closeChat,
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
v-if="!isOpen"
|
||||
@click="isOpen = !isOpen"
|
||||
class="cursor-pointer fixed shadow-xl shadow-black/30 right-8 bottom-8 bg-black size-[70px] flex justify-center items-center rounded-full"
|
||||
class="cursor-pointer z-50 fixed shadow-xl shadow-black/30 right-8 bottom-8 bg-black size-[70px] flex justify-center items-center rounded-full"
|
||||
>
|
||||
<Icon
|
||||
name="streamline:artificial-intelligence-spark"
|
||||
class="**:stroke-white"
|
||||
size="26"
|
||||
class="**:stroke-white size-[26px]"
|
||||
/>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ onMounted(() => {
|
||||
>
|
||||
<img
|
||||
v-if="!reverse"
|
||||
src="/public/img/hero-bg.jpg"
|
||||
src="/img/footer-bg.jpg"
|
||||
class="size-full object-cover absolute"
|
||||
alt="profile"
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
// provide / inject
|
||||
// import
|
||||
|
||||
import type { ProductVariantProvideType } from "~/pages/product";
|
||||
import type { ProductVariantProvideType } from "~/pages/product/[id].vue";
|
||||
|
||||
// provide / inject
|
||||
|
||||
const { selectedVariant } = inject("productVariant") as ProductVariantProvideType;
|
||||
|
||||
@@ -26,14 +28,14 @@ const { selectedVariant } = inject("productVariant") as ProductVariantProvideTyp
|
||||
class="w-full grid grid-cols-2 gap-y-[1.5rem] sm:gap-x-[3rem]"
|
||||
>
|
||||
<div
|
||||
v-for="inPackItem in selectedVariant.in_pack_items"
|
||||
v-for="inPackItem in selectedVariant!.in_pack_items"
|
||||
class="w-full flex-col-center gap-[.75rem]"
|
||||
>
|
||||
<div
|
||||
class="size-[6.25rem] rounded-full border-slate-200 bg-white flex-center"
|
||||
>
|
||||
<div class="size-11 relative">
|
||||
<img
|
||||
<NuxtImg
|
||||
class="size-full absolute object-cover"
|
||||
:src="inPackItem.cover"
|
||||
:alt="inPackItem.item_title"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// import
|
||||
|
||||
import useGetProduct from "~/composables/api/product/useGetProduct";
|
||||
import type { ProductVariantProvideType } from "~/pages/product/types";
|
||||
import type { ProductVariantProvideType } from "~/pages/product/[id].vue";
|
||||
|
||||
// state
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ const username = computed(() => {
|
||||
:class="is_user ? 'rounded-br-none' : 'rounded-bl-none'"
|
||||
>
|
||||
<div class="w-2/12 flex items-start justify-start">
|
||||
<img :src="profile" class="size-16 rounded-full" />
|
||||
<NuxtImg :src="profile" class="size-16 rounded-full" />
|
||||
</div>
|
||||
|
||||
<div class="w-10/12 flex flex-col items-start pt-2">
|
||||
|
||||
@@ -6,7 +6,7 @@ import { usePersianTimeAgo } from "~/composables/global/usePersianTimeAgo";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
data: Ticket;
|
||||
data: Omit<Ticket, "messages">;
|
||||
};
|
||||
|
||||
// props
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
import { sanitize } from "isomorphic-dompurify";
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
// types
|
||||
|
||||
@@ -27,8 +27,8 @@ const useGetArticle = (id: number | string | undefined) => {
|
||||
select: (article) => {
|
||||
const copyOfArticle = { ...article };
|
||||
|
||||
copyOfArticle.summery = sanitize(copyOfArticle.summery);
|
||||
copyOfArticle.content = sanitize(copyOfArticle.content);
|
||||
copyOfArticle.summery = sanitizeHtml(copyOfArticle.summery);
|
||||
copyOfArticle.content = sanitizeHtml(copyOfArticle.content);
|
||||
|
||||
return copyOfArticle;
|
||||
}
|
||||
|
||||
@@ -76,10 +76,9 @@ const useCreateChatMessage = (queryClient: QueryClient) => {
|
||||
onSuccess: (response) => {
|
||||
queryClient.setQueryData<InfiniteData<ApiPaginated<Chat>>>(
|
||||
[QUERY_KEYS.chat],
|
||||
(oldData) => {
|
||||
(oldData : any) => {
|
||||
if (oldData) {
|
||||
const lastPage =
|
||||
oldData!.pages[oldData!.pages.length - 1];
|
||||
const lastPage = oldData!.pages[oldData!.pages.length - 1];
|
||||
|
||||
return {
|
||||
pages: [
|
||||
|
||||
@@ -14,6 +14,7 @@ export type GetAllOrdersRequest = {
|
||||
};
|
||||
|
||||
const useGetAllOrders = (params: ComputedRef<GetAllOrdersRequest>) => {
|
||||
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
import { sanitize } from "isomorphic-dompurify";
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
// types
|
||||
|
||||
@@ -27,7 +27,7 @@ const useGetProduct = (id: string | number | undefined) => {
|
||||
select: (product) => {
|
||||
const copyOfProduct = { ...product };
|
||||
|
||||
copyOfProduct.description = sanitize(copyOfProduct.description);
|
||||
copyOfProduct.description = sanitizeHtml(copyOfProduct.description);
|
||||
|
||||
copyOfProduct.variants = copyOfProduct.variants.sort((a, b) => b.in_stock - a.in_stock);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export const useImageColor = (img: string) => {
|
||||
try {
|
||||
const color = await fac.getColorAsync(imageEl);
|
||||
isPending.value = false;
|
||||
colorObject.value = color;
|
||||
colorObject.value = color
|
||||
} catch (e) {
|
||||
isPending.value = false;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const route = useRoute();
|
||||
// computed
|
||||
|
||||
const pageTitle = computed(() => route.meta.pageTitle);
|
||||
const prevPage = computed(() => route.meta.prevPage);
|
||||
const prevPage = computed(() => route.meta.prevPage as { name: string, label: string } | undefined);
|
||||
|
||||
// queries
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ export default defineNuxtConfig({
|
||||
"@vueuse/nuxt",
|
||||
"@formkit/auto-animate/nuxt",
|
||||
"@vite-pwa/nuxt",
|
||||
"@nuxt/image",
|
||||
],
|
||||
|
||||
pwa: {
|
||||
@@ -110,6 +111,14 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
|
||||
typescript: {
|
||||
typeCheck: false,
|
||||
},
|
||||
|
||||
image: {
|
||||
quality: 65,
|
||||
},
|
||||
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
API_BASE_URL: process.env.API_BASE_URL,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"start": "node .output/server/index.mjs",
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"lint": "nuxi typecheck",
|
||||
"dev-network": "nuxi dev --host",
|
||||
"dev-o": "nuxt dev -- -o",
|
||||
"test": "vitest",
|
||||
@@ -16,10 +17,12 @@
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@nuxt/icon": "^1.10.3",
|
||||
"@nuxt/image": "^1.10.0",
|
||||
"@nuxtjs/google-fonts": "^3.2.0",
|
||||
"@tanstack/vue-query": "^5.62.2",
|
||||
"@tanstack/vue-query-devtools": "^5.62.3",
|
||||
"@vite-pwa/nuxt": "^0.10.6",
|
||||
"@vue/language-server": "^2.2.8",
|
||||
"@vuelidate/core": "^2.0.3",
|
||||
"@vuelidate/validators": "^2.0.4",
|
||||
"@vueuse/integrations": "^12.7.0",
|
||||
@@ -30,10 +33,10 @@
|
||||
"fast-average-color": "^9.4.0",
|
||||
"gsap": "^3.12.7",
|
||||
"highlight.js": "^11.11.1",
|
||||
"isomorphic-dompurify": "^2.22.0",
|
||||
"jalali-ts": "^8.0.0",
|
||||
"nuxt": "^3.15.4",
|
||||
"reka-ui": "^1.0.0-alpha.6",
|
||||
"sanitize-html": "^2.15.0",
|
||||
"swiper": "^11.2.4",
|
||||
"universal-cookie": "^7.2.2",
|
||||
"vue": "latest",
|
||||
@@ -47,9 +50,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.0.9",
|
||||
"@types/masonry-layout": "^4.2.8",
|
||||
"@types/node": "^22.13.11",
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.0.9"
|
||||
"tailwindcss": "^4.0.9",
|
||||
"typescript": "^5.8.2",
|
||||
"vue-tsc": "^2.2.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ if (response.isError) {
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="w-full h-[80svh] rounded-3xl relative overflow-hidden">
|
||||
<img class="absolute object-cover size-full" :alt="article!.title" :src="article!.cover_image" />
|
||||
<NuxtImg class="absolute object-cover size-full" :alt="article!.title" :src="article!.cover_image" />
|
||||
<div class="absolute bg-linear-to-t from-black/75 to-transparent size-full" />
|
||||
<div class="absolute pl-10 right-10 bottom-10 flex flex-col gap-6">
|
||||
<h1 class="typo-h-4 text-white pl-8">
|
||||
@@ -48,7 +48,7 @@ if (response.isError) {
|
||||
class="w-fit pr-2 pl-5 h-[50px] rounded-full flex items-center justify-center gap-3 bg-white">
|
||||
<div
|
||||
class="relative flex items-center justify-center rounded-full overflow-hidden size-[35px]">
|
||||
<img
|
||||
<NuxtImg
|
||||
class="size-full object-cover absolute"
|
||||
:src="article!.author.profile_photo"
|
||||
alt="article-author"
|
||||
|
||||
@@ -76,7 +76,7 @@ const selectedGateway = ref<PaymentGateway>(paymentGateways.value[0]);
|
||||
class="w-full p-5 border rounded-xl flex flex-col gap-4 transition-all cursor-pointer"
|
||||
>
|
||||
<div class="aspect-square flex-center">
|
||||
<img :src="gateway.picture" class="object-cover" />
|
||||
<NuxtImg :src="gateway.picture" class="object-cover" />
|
||||
</div>
|
||||
<span class="typo-label-sm text-black">
|
||||
{{ gateway.title }}
|
||||
|
||||
@@ -133,7 +133,7 @@ const contactWays = ref([
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-4/12 h-full bg-red-300">
|
||||
<img src="/mlz.jpeg" class="-mt-16" />
|
||||
<NuxtImg src="/mlz.jpeg" class="-mt-16" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ const disableLoadingOverlay = useState("disableLoadingOverlay", () => false);
|
||||
const response = await suspense();
|
||||
|
||||
if (response.isError) {
|
||||
console.log(response);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: `Landing error : ${response.error.message}`
|
||||
@@ -41,7 +42,7 @@ onMounted(() => {
|
||||
/>
|
||||
<Categories class="mt-40" />
|
||||
<Brands />
|
||||
<MostRecentComments />
|
||||
<!-- <MostRecentComments />-->
|
||||
<ClientOnly>
|
||||
<LatestStories class="mb-20" />
|
||||
</ClientOnly>
|
||||
|
||||
@@ -123,16 +123,16 @@ watch(
|
||||
</div>
|
||||
</ul>
|
||||
<div v-else class="w-full h-max">
|
||||
<div v-if="!products?.length" class="flex flex-grow w-full">
|
||||
<div v-if="!products!.length" class="flex flex-grow w-full">
|
||||
<Placeholder title="محصولی یافت نشد :(" icon="bi:search" />
|
||||
</div>
|
||||
<ProductsGrid
|
||||
:with-header="false"
|
||||
:products="products"
|
||||
:products="products!"
|
||||
class="!p-0"
|
||||
/>
|
||||
<div v-if="data?.count > 10" class="w-full flex-center py-10">
|
||||
<Pagination :items="paginationData" :total="data?.count" />
|
||||
<div v-if="data && paginationData && data.count > 10" class="w-full flex-center py-10">
|
||||
<Pagination :items="paginationData" :total="data.count" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,7 +109,7 @@ const formRules = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
const formValidator$ = useVuelidate(formRules, personalData);
|
||||
const formValidator$ = useVuelidate(formRules, personalData as any);
|
||||
|
||||
// methods
|
||||
|
||||
@@ -195,7 +195,7 @@ const handleSubmit = (withValidation: boolean) => {
|
||||
با اولین خریدتون هوش مصنوعی وبسایتمون واستون یک
|
||||
بایوگرافی درست میکنه :)
|
||||
</span>
|
||||
<span
|
||||
<div
|
||||
class="flex-center border border-yellow-500 pe-3.5 ps-1 w-max rounded-full"
|
||||
>
|
||||
<div class="rounded-full p-2">
|
||||
@@ -208,7 +208,7 @@ const handleSubmit = (withValidation: boolean) => {
|
||||
<span class="text-xs text-yellow-500"
|
||||
>جزو ۳ مشتری برتر</span
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ definePageMeta({
|
||||
|
||||
// state
|
||||
|
||||
const params: GetAllOrdersRequest = useUrlSearchParams("history");
|
||||
const params = useUrlSearchParams("history") as GetAllOrdersRequest;
|
||||
|
||||
const filters = computed(() => {
|
||||
return {
|
||||
@@ -91,12 +91,12 @@ const purchases = computed(() => {
|
||||
return data.value?.results.flat();
|
||||
});
|
||||
|
||||
const hasPurchases = computed(() => data.value?.count > 0);
|
||||
const hasPurchases = computed(() => data.value && data.value.count > 0);
|
||||
|
||||
const hasFilters = computed(() =>
|
||||
Object.keys(params)
|
||||
.filter((key) => key != "page")
|
||||
.some((key) => params[key] != undefined)
|
||||
.some((key) => (params as any)[key] != undefined)
|
||||
);
|
||||
|
||||
const paginationData = computed(() => {
|
||||
@@ -246,8 +246,8 @@ const clearFilters = () => {
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<div v-if="data?.count > 10" class="w-full flex-center py-10">
|
||||
<Pagination :items="paginationData" :total="data?.count" />
|
||||
<div v-if="data && paginationData && data.count > 10" class="w-full flex-center py-10">
|
||||
<Pagination :items="paginationData" :total="data.count" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ definePageMeta({
|
||||
|
||||
// state
|
||||
|
||||
const params: GetAllTicketsRequest = useUrlSearchParams("history");
|
||||
const params = useUrlSearchParams("history") as GetAllTicketsRequest;
|
||||
|
||||
const filters = computed(() => {
|
||||
return {
|
||||
@@ -72,12 +72,12 @@ const tickets = computed(() => {
|
||||
return data.value?.results.flat();
|
||||
});
|
||||
|
||||
const hasTickets = computed(() => data.value?.count > 0);
|
||||
const hasTickets = computed(() => data.value && data.value.count > 0);
|
||||
|
||||
const hasFilters = computed(() =>
|
||||
Object.keys(params)
|
||||
.filter((key) => key != "page")
|
||||
.some((key) => params[key] != undefined)
|
||||
.some((key) => (params as any)[key] != undefined)
|
||||
);
|
||||
|
||||
const paginationData = computed(() => {
|
||||
@@ -227,7 +227,7 @@ const clearFilters = () => {
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<div v-if="data?.count > 7" class="w-full flex-center py-10">
|
||||
<div v-if="data && paginationData && data.count > 7" class="w-full flex-center py-10">
|
||||
<Pagination :items="paginationData" :total="data?.count" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -159,10 +159,16 @@ const resetForm = () => {
|
||||
}"
|
||||
/>
|
||||
<div class="flex items-center justify-center flex-col size-full translate-y-[-80px]">
|
||||
<img
|
||||
class="aspect-square w-[300px] translate-y-[100px] animate-fade-in"
|
||||
src="/img/heymlz-seat.gif"
|
||||
alt=""
|
||||
<video
|
||||
class="aspect-square w-[450px] translate-y-[197px] animate-fade-in"
|
||||
src="/video/heymlz/heymlz-seat-2.webm"
|
||||
:style="{
|
||||
filter: 'drop-shadow(0px 4px 20px rgba(0, 0, 0, 0.15))'
|
||||
}"
|
||||
autoplay
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
muted
|
||||
/>
|
||||
|
||||
<div
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
// import
|
||||
|
||||
import hljs from "highlight.js";
|
||||
import javascript from "highlight.js/lib/languages/javascript";
|
||||
import json from "highlight.js/lib/languages/json";
|
||||
import xml from "highlight.js/lib/languages/xml";
|
||||
import "highlight.js/styles/atom-one-dark.css";
|
||||
import LogDate from "~/components/server-logs/LogDate.vue";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
@@ -11,7 +12,7 @@ import { useQuery } from "@tanstack/vue-query";
|
||||
// meta
|
||||
|
||||
definePageMeta({
|
||||
middleware : "check-is-debug",
|
||||
middleware: "check-is-debug",
|
||||
layout: "none"
|
||||
});
|
||||
|
||||
@@ -22,7 +23,7 @@ const { $axios: axios } = useNuxtApp();
|
||||
const { data: serverLogs, isFetching, suspense } = useQuery({
|
||||
queryKey: ["server-logs"],
|
||||
queryFn: async () => {
|
||||
const response = await axios.get("http://localhost:3000/api/server-logger");
|
||||
const response = await axios.get<AxiosLogType[]>("http://localhost:3000/api/server-logger");
|
||||
return response.data.reverse();
|
||||
},
|
||||
refetchInterval: 5000,
|
||||
@@ -41,7 +42,9 @@ const logIcon = (status: number) => {
|
||||
// lifecycle
|
||||
|
||||
onMounted(() => {
|
||||
hljs.registerLanguage("json", javascript);
|
||||
hljs.registerLanguage("json", json);
|
||||
hljs.registerLanguage("xml", xml);
|
||||
|
||||
hljs.highlightAll();
|
||||
});
|
||||
|
||||
@@ -102,34 +105,41 @@ onMounted(() => {
|
||||
<details class="text-white">
|
||||
<summary class="cursor-pointer select-none">Details :</summary>
|
||||
<div class="flex flex-col gap-2 mt-2 ml-4">
|
||||
<details class="text-white">
|
||||
<details
|
||||
v-if="log.response && typeof log.response === 'string' && (log.response.startsWith('<!DOCTYPE html>') || log.response.startsWith('<html>'))"
|
||||
class="text-white"
|
||||
>
|
||||
<summary class="cursor-pointer select-none">Preview :</summary>
|
||||
<iframe class="w-full h-[500px] bg-white" :srcdoc="log.response"></iframe>
|
||||
</details>
|
||||
<details v-if="log.response" class="text-white">
|
||||
<summary class="cursor-pointer select-none">Response :</summary>
|
||||
<pre>
|
||||
<code class="language-json">
|
||||
{{ log.response }}
|
||||
<pre style="tab-size: 2">
|
||||
<code class="whitespace-pre-wrap">
|
||||
{{ String(log.response) }}
|
||||
</code>
|
||||
</pre>
|
||||
</details>
|
||||
<details class="text-white">
|
||||
<summary class="cursor-pointer select-none">Req Headers :</summary>
|
||||
<pre class="whitespace-pre-line">
|
||||
<code class="language-json">
|
||||
<pre style="tab-size: 2">
|
||||
<code class="whitespace-pre-line">
|
||||
{{ log.requestHeaders }}
|
||||
</code>
|
||||
</pre>
|
||||
</details>
|
||||
<details class="text-white">
|
||||
<summary class="cursor-pointer select-none">Res Headers :</summary>
|
||||
<pre>
|
||||
<code class="language-json">
|
||||
<pre style="tab-size: 2">
|
||||
<code class="whitespace-pre-line">
|
||||
{{ log.responseHeaders }}
|
||||
</code>
|
||||
</pre>
|
||||
</details>
|
||||
<details v-if="log.payload" class="text-white">
|
||||
<summary class="cursor-pointer select-none">Payload :</summary>
|
||||
<pre>
|
||||
<code class="language-json">
|
||||
<pre style="tab-size: 2">
|
||||
<code class="whitespace-pre-line">
|
||||
{{ log.payload }}
|
||||
</code>
|
||||
</pre>
|
||||
@@ -151,11 +161,11 @@ onMounted(() => {
|
||||
|
||||
@keyframes log-fade-in {
|
||||
from {
|
||||
opacity : 0;
|
||||
opacity: 0;
|
||||
scale: 0.8;
|
||||
}
|
||||
to {
|
||||
opacity : 1;
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 7.9 MiB After Width: | Height: | Size: 7.9 MiB |
|
Before Width: | Height: | Size: 7.1 MiB |
|
Before Width: | Height: | Size: 7.7 MiB |
|
Before Width: | Height: | Size: 5.2 MiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 10 MiB After Width: | Height: | Size: 10 MiB |
|
Before Width: | Height: | Size: 943 KiB After Width: | Height: | Size: 943 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 237 KiB |
|
After Width: | Height: | Size: 830 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 366 KiB |
@@ -2,5 +2,5 @@ import fs from "fs/promises";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const oldLogs = await fs.readFile(".logs/log.json", "utf-8");
|
||||
return JSON.parse(oldLogs) as Record<any, any>[];
|
||||
return JSON.parse(oldLogs) as Record<any, any>[];
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from "fs/promises";
|
||||
import { ensureFileExists } from "~/utils";
|
||||
|
||||
class Logger {
|
||||
public static async axiosErrorLog(error: any) {
|
||||
@@ -13,19 +14,23 @@ class Logger {
|
||||
method: errorJson.config.method,
|
||||
response: error?.response?.data,
|
||||
requestHeaders: errorJson.config.headers,
|
||||
responseHeaders: error.response.headers,
|
||||
// responseHeaders: error.response.headers,
|
||||
payload: errorJson.config.data ? JSON.parse(errorJson.config.data) : undefined,
|
||||
params: errorJson.config.params ?? undefined,
|
||||
date: nowDate.toString()
|
||||
};
|
||||
|
||||
const logFilePath = ".logs/log.json";
|
||||
|
||||
try {
|
||||
const oldLogs = await fs.readFile(".logs/log.json", "utf-8");
|
||||
await ensureFileExists(logFilePath, "[]");
|
||||
|
||||
const oldLogs = await fs.readFile(logFilePath, "utf-8");
|
||||
const oldLogsJson = JSON.parse(oldLogs) as Record<any, any>[];
|
||||
|
||||
oldLogsJson.push(logData);
|
||||
|
||||
await fs.writeFile(".logs/log.json", JSON.stringify(oldLogsJson));
|
||||
await fs.writeFile(logFilePath, JSON.stringify(oldLogsJson, null, 2));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ declare global {
|
||||
status: number,
|
||||
code: string,
|
||||
requestHeaders: Record<any, any>,
|
||||
responseHeaders: Record<any, any>,
|
||||
response?: Record<any, any>,
|
||||
responseHeaders?: Record<any, any>,
|
||||
response?: any,
|
||||
payload?: Record<any, any>,
|
||||
params?: Record<any, any>,
|
||||
date: string
|
||||
@@ -35,7 +35,7 @@ declare global {
|
||||
};
|
||||
|
||||
type Account = {
|
||||
profile_photo: File | null;
|
||||
profile_photo: string | null;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
phone: string;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
export const dateFormatter = (date: string | undefined) => {
|
||||
const formattedDate = useTimeAgo(date!);
|
||||
return formattedDate.value;
|
||||
@@ -77,3 +80,19 @@ export const isImage = (name: string | undefined) => {
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Ensure Exist
|
||||
|
||||
export const ensureFileExists = async (filePath: string, initialContent = "") => {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
} catch (error) {
|
||||
const err = error as any;
|
||||
if (err.code === "ENOENT") {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.writeFile(filePath, initialContent, "utf-8");
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineVitestConfig } from "@nuxt/test-utils/config";
|
||||
|
||||
export default defineVitestConfig({
|
||||
test: {
|
||||
environment: "nuxt",
|
||||
},
|
||||
});
|
||||