merge
This commit is contained in:
@@ -31,5 +31,5 @@ jobs:
|
||||
script: |
|
||||
cd /root/hshop/
|
||||
docker compose down
|
||||
docker compose build --no-cache
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
@@ -240,15 +240,20 @@ AWS_S3_OBJECT_PARAMETERS = {
|
||||
# ==============================================================================
|
||||
|
||||
AZ_IRANIAN_BANK_GATEWAYS = {
|
||||
'GATEWAYS': {
|
||||
'ZARINPAL': {
|
||||
'MERCHANT_CODE': 'Merchant-Code',
|
||||
'SANDBOX': True,
|
||||
}
|
||||
"GATEWAYS": {
|
||||
"ZARINPAL": {
|
||||
"MERCHANT_CODE": "",
|
||||
"SANDBOX": 0,
|
||||
},
|
||||
},
|
||||
'IS_SAMPLE_FORM_ENABLE': True,
|
||||
'DEFAULT_BANK': 'ZARINPAL',
|
||||
'CURRENCY': 'IRR',
|
||||
'TRACKING_CODE_QUERY_PARAM': 'tc',
|
||||
'BANK_PRIORITIES': ['ZARINPAL'],
|
||||
"IS_SAMPLE_FORM_ENABLE": True,
|
||||
"DEFAULT": "ZARINPAL",
|
||||
"CURRENCY": "IRT",
|
||||
"TRACKING_CODE_QUERY_PARAM": "tc",
|
||||
"TRACKING_CODE_LENGTH": 16,
|
||||
"SETTING_VALUE_READER_CLASS": "azbankgateways.readers.DefaultReader",
|
||||
"BANK_PRIORITIES": [
|
||||
"ZARINPAL",
|
||||
],
|
||||
"IS_SAFE_GET_GATEWAY_PAYMENT": False # better to be True
|
||||
}
|
||||
@@ -8,6 +8,9 @@ from product import views
|
||||
from account.views import CustomTokenObtainPairView
|
||||
from home.views import HomeView
|
||||
from .views import FakeAdminLoginView
|
||||
from azbankgateways.urls import az_bank_gateways_urls
|
||||
|
||||
admin.autodiscover()
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
@@ -29,6 +32,7 @@ urlpatterns = [
|
||||
path('blogs/', include('blog.urls')),
|
||||
path('order/', include('order.urls')),
|
||||
path('home/', include('home.urls')),
|
||||
path("bankgateways/", az_bank_gateways_urls()),
|
||||
path('', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.1.2 on 2025-03-13 16:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0014_alter_orderitemmodel_price'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Payment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('amount', models.PositiveIntegerField()),
|
||||
('status', models.CharField(default='Pending', max_length=50)),
|
||||
('tracking_code', models.CharField(blank=True, max_length=100)),
|
||||
('bank_type', models.CharField(max_length=100)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.1.2 on 2025-03-13 16:28
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0015_payment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='Payment',
|
||||
new_name='PaymentModel',
|
||||
),
|
||||
]
|
||||
@@ -120,3 +120,13 @@ class OrderItemModel(models.Model):
|
||||
def __str__(self):
|
||||
return f'({self.product}) - ({self.order.user})'
|
||||
|
||||
|
||||
#TODO complate this shit
|
||||
class PaymentModel(models.Model):
|
||||
amount = models.PositiveIntegerField()
|
||||
status = models.CharField(max_length=50, default='Pending')
|
||||
tracking_code = models.CharField(max_length=100, blank=True)
|
||||
bank_type = models.CharField(max_length=100)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
def __str__(self):
|
||||
return 'payment'
|
||||
@@ -14,7 +14,7 @@ class ProductVariantSerialzier(serializers.ModelSerializer):
|
||||
category = serializers.SerializerMethodField()
|
||||
class Meta:
|
||||
model = ProductVariant
|
||||
fields = ['title', 'product_attributes', 'in_stock', 'price', 'discount', 'color', 'image', 'discount_amount', 'category', 'final_price']
|
||||
fields = ['id', 'title', 'product_attributes', 'in_stock', 'price', 'discount', 'color', 'image', 'discount_amount', 'category', 'final_price']
|
||||
|
||||
def get_discount_amount(self, obj):
|
||||
discount_amount = int(obj.price * (obj.discount / 100))
|
||||
@@ -95,11 +95,10 @@ class CartSerializer(serializers.ModelSerializer):
|
||||
class OrderSerializer(serializers.ModelSerializer):
|
||||
count = serializers.SerializerMethodField()
|
||||
images = serializers.SerializerMethodField()
|
||||
address = UserAddressSerializer()
|
||||
items = OrderItemSerailzier(many=True)
|
||||
order_id = serializers.SerializerMethodField()
|
||||
class Meta:
|
||||
model = OrderModel
|
||||
fields = ['address', 'created_at', 'items', 'status', 'discount_code', "images", "count", "id"]
|
||||
fields = ['created_at', 'status', "images", "count", "id", 'final_price', 'order_id']
|
||||
|
||||
def get_count(self, obj):
|
||||
return obj.items.all().count()
|
||||
@@ -110,4 +109,6 @@ class OrderSerializer(serializers.ModelSerializer):
|
||||
if (image := item.product.images.all().first()) else None
|
||||
for item in obj.items.all()[:3]
|
||||
]
|
||||
return filter(lambda x: x is not None, image_list)
|
||||
return filter(lambda x: x is not None, image_list)
|
||||
def get_order_id(self, obj):
|
||||
return obj.id + 1000
|
||||
@@ -2,6 +2,7 @@ from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from .views import CartItemViews, CartView, OrderlistView, CartItemClear, ApplyDiscountView
|
||||
from .views import PaymentView, callback_view
|
||||
|
||||
urlpatterns = [
|
||||
path('all', OrderlistView.as_view(), name='order-list'),
|
||||
@@ -9,6 +10,6 @@ urlpatterns = [
|
||||
path('cart/discount', ApplyDiscountView.as_view()),
|
||||
path('cart/all', CartItemClear.as_view()),
|
||||
path('cart/item/<int:pk>', CartItemViews.as_view(), name='change-item-cart'),
|
||||
# path('payment', CartView.as_view()),
|
||||
# path('', CartView.as_view()),
|
||||
path('payment', PaymentView.as_view(), name='payment'),
|
||||
path('callback', callback_view, name='callback-gateway'),
|
||||
]
|
||||
|
||||
+97
-52
@@ -7,13 +7,17 @@ from rest_framework.permissions import IsAuthenticated
|
||||
from .serializers import *
|
||||
# from cart.models import
|
||||
from rest_framework import status
|
||||
from .models import OrderItemModel, OrderModel, DiscountCode
|
||||
from .models import OrderItemModel, OrderModel, DiscountCode, PaymentModel
|
||||
from .permissons import CanDeleteCartItemPermissions
|
||||
from azbankgateways import bankfactories, models as bank_models
|
||||
from azbankgateways.exceptions import AZBankGatewaysException
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
|
||||
from utils.pagination import StructurePagination
|
||||
try:
|
||||
pass
|
||||
except DiscountNotAvailableError:
|
||||
pass
|
||||
|
||||
from django.urls import reverse
|
||||
"""
|
||||
|
||||
add post
|
||||
@@ -105,69 +109,110 @@ class CartView(APIView):
|
||||
class OrderlistView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = OrderSerializer
|
||||
pagination_class = StructurePagination
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="limit",
|
||||
description="لیمیتش",
|
||||
required=False,
|
||||
type=OpenApiTypes.INT,
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="offset",
|
||||
description="افستش",
|
||||
required=False,
|
||||
type=OpenApiTypes.INT,
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="status",
|
||||
description=(
|
||||
"['CART', 'ADMIN_PENDING', 'PENDING', 'POSTED', 'RECEIVED', 'CANCELED', 'BACK']"
|
||||
),
|
||||
required=False,
|
||||
type=OpenApiTypes.STR,
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="sort",
|
||||
description=(
|
||||
"Sort results by one of the following fields:\n"
|
||||
"['created_at', '-created_at', 'final_price', '-final_price']"
|
||||
"\nPrefix with `-` for descending order."
|
||||
),
|
||||
required=False,
|
||||
type=OpenApiTypes.STR,
|
||||
),
|
||||
]
|
||||
)
|
||||
def get(self, request):
|
||||
user = request.user
|
||||
orders = OrderModel.objects.filter(user=user).exclude(status="CART")
|
||||
orders_ser = self.serializer_class(instance=orders, many=True, context={'request': request})
|
||||
return Response(orders_ser.data, status=status.HTTP_200_OK)
|
||||
status_filter = request.query_params.get("status", None)
|
||||
sort = request.query_params.get('sort', None)
|
||||
if status_filter in ['CART', 'ADMIN_PENDING', 'PENDING', 'POSTED', 'RECEIVED', 'CANCELED', 'BACK']:
|
||||
orders.filter(status=status_filter)
|
||||
if sort:
|
||||
if sort not in ['created_at', '-created_at', 'final_price', '-final_price']:
|
||||
return Response({'detail': 'پارامتر sort اشتباه است'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
orders = orders.order_by(sort)
|
||||
paginator = self.pagination_class()
|
||||
paginated_orders = paginator.paginate_queryset(orders, request)
|
||||
orders_ser = self.serializer_class(instance=paginated_orders, many=True, context={'request': request})
|
||||
return paginator.get_paginated_response(orders_ser.data)
|
||||
|
||||
|
||||
|
||||
# from rest_framework.views import APIView
|
||||
# from rest_framework.response import Response
|
||||
# from rest_framework import status
|
||||
# from azbankgateways import bankfactories, models as bank_models
|
||||
|
||||
# class PaymentView(APIView):
|
||||
# def post(self, request):
|
||||
# amount = request.data.get('amount')
|
||||
# user = request.user
|
||||
class PaymentView(APIView):
|
||||
def post(self, request):
|
||||
amount = 10000000
|
||||
user_mobile_number = request.user.phone
|
||||
|
||||
factory = bankfactories.BankFactory()
|
||||
try:
|
||||
bank = (
|
||||
factory.create(bank_models.BankType.ZARINPAL)
|
||||
)
|
||||
bank.set_request(request)
|
||||
bank.set_amount(amount)
|
||||
|
||||
# payment = Payment.objects.create(amount=amount, bank_type='ZARINPAL')
|
||||
bank.set_client_callback_url(request.build_absolute_uri(reverse("callback-gateway")))
|
||||
bank.set_mobile_number(user_mobile_number)
|
||||
|
||||
|
||||
# factory = bankfactories.ZarinpalBankFactory()
|
||||
# try:
|
||||
# bank = factory.create(
|
||||
# amount=amount,
|
||||
# user=user,
|
||||
# callback_url='http://.com/callback/',
|
||||
# reference_model=payment,
|
||||
# )
|
||||
# bank.ready()
|
||||
# return Response({'gateway_url': bank.redirect_url}, status=status.HTTP_200_OK)
|
||||
# except Exception as e:
|
||||
# return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
bank_record = bank.ready()
|
||||
return Response(bank.redirect_gateway().url)
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# from django.views.decorators.csrf import csrf_exempt
|
||||
# from rest_framework.decorators import api_view
|
||||
# from rest_framework.response import Response
|
||||
# from azbankgateways import bankfactories, models as bank_models
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from azbankgateways import bankfactories, models as bank_models
|
||||
|
||||
# @csrf_exempt
|
||||
# @api_view(['POST'])
|
||||
# def callback_view(request):
|
||||
# tracking_code = request.POST.get('tracking_code')
|
||||
# payment_id = request.POST.get('payment_id')
|
||||
@csrf_exempt
|
||||
@api_view(['POST'])
|
||||
def callback_view(request):
|
||||
tracking_code = request.GET.get(settings.TRACKING_CODE_QUERY_PARAM, None)
|
||||
if not tracking_code:
|
||||
logging.debug("این لینک معتبر نیست.")
|
||||
raise Http404
|
||||
|
||||
# payment = Payment.objects.get(id=payment_id)
|
||||
# bank_type = payment.bank_type
|
||||
try:
|
||||
bank_record = bank_models.Bank.objects.get(tracking_code=tracking_code)
|
||||
except bank_models.Bank.DoesNotExist:
|
||||
logging.debug("این لینک معتبر نیست.")
|
||||
raise Http404
|
||||
|
||||
|
||||
# factory = bankfactories.BankFactory.get_bank(bank_type)
|
||||
# try:
|
||||
# result = factory.verify_transaction(tracking_code)
|
||||
# if result.is_success:
|
||||
# payment.status = 'Paid'
|
||||
# payment.tracking_code = tracking_code
|
||||
# payment.save()
|
||||
# return Response({'status': 'Payment successful'})
|
||||
# else:
|
||||
# payment.status = 'Failed'
|
||||
# payment.save()
|
||||
# return Response({'status': 'Payment failed'})
|
||||
# except Exception as e:
|
||||
# return Response({'error': str(e)})
|
||||
if bank_record.is_success:
|
||||
return HttpResponse("پرداخت با موفقیت انجام شد.")
|
||||
|
||||
|
||||
return HttpResponse(
|
||||
"پرداخت با شکست مواجه شده است. اگر پول کم شده است ظرف مدت ۴۸ ساعت پول به حساب شما بازخواهد گشت."
|
||||
)
|
||||
@@ -2,7 +2,7 @@ from .models import *
|
||||
from rest_framework import serializers
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ class ProductVariantSerialzier(serializers.ModelSerializer):
|
||||
in_pack_items = InPackItemsSerialzier(many=True)
|
||||
images = ProductImageSerailizer(many=True)
|
||||
details = ProductDetailSerializer(many=True, read_only=True)
|
||||
cart_quantity = serializers.SerializerMethodField()
|
||||
class Meta:
|
||||
model = ProductVariant
|
||||
exclude = ('min_price', 'sell', 'currency', 'product', 'input_price')
|
||||
@@ -60,6 +61,11 @@ class ProductVariantSerialzier(serializers.ModelSerializer):
|
||||
if view_type == 'list':
|
||||
self.fields.pop('in_pack_items', None)
|
||||
|
||||
def get_cart_quantity(self, obj):
|
||||
request = self.context.get('request')
|
||||
if not request or not request.user.is_authenticated:
|
||||
return 0
|
||||
return 1
|
||||
|
||||
|
||||
|
||||
@@ -135,7 +141,7 @@ class DynamicProductSerializer(serializers.ModelSerializer):
|
||||
many=True,
|
||||
context={
|
||||
'view_type': 'list',
|
||||
'dollor_price': self.context.get('dollor_price')
|
||||
'request': self.context.get('request')
|
||||
}
|
||||
)
|
||||
return serializer.data
|
||||
|
||||
@@ -51,7 +51,7 @@ class AllCategories(APIView):
|
||||
class ProductView(APIView):
|
||||
serializer_class = DynamicProductSerializer
|
||||
permission_classes = [AllowAny]
|
||||
authentication_classes = []
|
||||
# authentication_classes = []
|
||||
def get(self, request, pk):
|
||||
product = get_object_or_404(ProductModel, id=pk)
|
||||
product_ser = self.serializer_class(instance=product, many=False, context={'request': request, 'view_type': 'instance'})
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
@import "./fonts/morabba.css";
|
||||
@import "./fonts/yekan-bakh.css";
|
||||
|
||||
@theme {
|
||||
:root {
|
||||
/* CONTAINER */
|
||||
|
||||
--app-container-padding: 1rem;
|
||||
}
|
||||
|
||||
@theme {
|
||||
|
||||
/* COLORS */
|
||||
--color-slate-50: hsl(210, 40%, 98%);
|
||||
@@ -120,7 +122,6 @@
|
||||
--font-morabba: "Morabba", "sans-serif";
|
||||
|
||||
/* BREAKPOINTS */
|
||||
--breakpoint-3xl: 1700px;
|
||||
--breakpoint-2xs: 400px;
|
||||
--breakpoint-xs: 480px;
|
||||
--breakpoint-sm: 640px;
|
||||
@@ -128,6 +129,7 @@
|
||||
--breakpoint-lg: 1024px;
|
||||
--breakpoint-xl: 1280px;
|
||||
--breakpoint-2xl: 1536px;
|
||||
--breakpoint-3xl: 1700px;
|
||||
|
||||
/* ANIMATIONS */
|
||||
--animate-marquee: marquee 20s linear infinite;
|
||||
|
||||
@@ -29,6 +29,7 @@ const isMobile = useMediaQuery('(max-width: 1024px)');
|
||||
:id="article.id"
|
||||
:date="article.created_at"
|
||||
:variant="isMobile ? 'sm' : 'lg'"
|
||||
:category="article.category"
|
||||
tag="تگ ندارد"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
// types
|
||||
|
||||
import { usePersianTimeAgo } from "~/composables/global/usePersianTimeAgo";
|
||||
|
||||
type Props = {
|
||||
id: number;
|
||||
tag: string;
|
||||
@@ -9,15 +11,22 @@ type Props = {
|
||||
title: string;
|
||||
description: string;
|
||||
variant?: "sm" | "lg";
|
||||
category: SubCategory;
|
||||
image: string,
|
||||
}
|
||||
|
||||
// props
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
variant: "lg"
|
||||
});
|
||||
|
||||
const { date } = toRefs(props);
|
||||
|
||||
// state
|
||||
|
||||
const createdAt = usePersianTimeAgo(new Date(date.value));
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -32,7 +41,7 @@ withDefaults(defineProps<Props>(), {
|
||||
v-if="variant === 'lg'"
|
||||
class="bg-success-500 absolute left-6 top-6 z-20"
|
||||
>
|
||||
اسپیکر
|
||||
{{ category.name }}
|
||||
</Tag>
|
||||
|
||||
<div
|
||||
@@ -42,7 +51,7 @@ withDefaults(defineProps<Props>(), {
|
||||
<Tag
|
||||
class="bg-success-500 absolute z-20 left-4 sm:left-6 top-4 sm:top-6 max-sm:text-xs"
|
||||
>
|
||||
اسپیکر
|
||||
{{ category.name }}
|
||||
</Tag>
|
||||
|
||||
<img
|
||||
@@ -73,7 +82,7 @@ withDefaults(defineProps<Props>(), {
|
||||
class="**:stroke-white size-3 md:size-3.5"
|
||||
/>
|
||||
<span class="typo-p-xs md:typo-p-sm text-white">
|
||||
۳۱ مهر ۱۴۰۳
|
||||
{{ createdAt }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,7 +98,8 @@ withDefaults(defineProps<Props>(), {
|
||||
v-if="variant === 'sm'"
|
||||
class="text-white typo-p-xs max-sm:!leading-[175%] sm:typo-p-sm md:typo-p-md line-clamp-3"
|
||||
>
|
||||
تا با نرم افزارها شناخت بیشتری را برای طراحان رایانه ای علی الخصوص طراحان خلاقی، و فرهنگ پیشرو در زبان فارسی ایجاد کرد، در این صورت می توان امید داشت که تمام و دشواری موجود در ارائه راهکارها
|
||||
تا با نرم افزارها شناخت بیشتری را برای طراحان رایانه ای علی الخصوص طراحان خلاقی، و فرهنگ پیشرو
|
||||
در زبان فارسی ایجاد کرد، در این صورت می توان امید داشت که تمام و دشواری موجود در ارائه راهکارها
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
class="size-[150px] lg:size-[220px] rounded-full"
|
||||
/>
|
||||
<span class="font-bold text-2xl lg:text-5xl text-gradient bg-gradient-to-l from-blue-500 to-blue-700">
|
||||
@@ -19,13 +21,13 @@
|
||||
</div>
|
||||
|
||||
<footer class="w-full flex max-lg:flex-col justify-between py-[100px] max-lg:gap-16 container items-center lg:items-start relative z-20">
|
||||
<div class="flex flex-col gap-4 text-white max-w-[300px]">
|
||||
<div class="flex flex-col gap-4 max-w-[300px]">
|
||||
|
||||
<h3 class="font-bold text-xl xl:text-3xl max-lg:text-center">
|
||||
<h3 class="font-bold text-xl xl:text-3xl max-lg:text-center text-white">
|
||||
با ما در ارتباط باشید...
|
||||
</h3>
|
||||
|
||||
<p class="text-md font-thin leading-[175%] mt-4 max-lg:text-center">
|
||||
<p class="text-md font-thin leading-[175%] mt-4 max-lg:text-center text-slate-300">
|
||||
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها
|
||||
</p>
|
||||
<div class="flex items-center gap-4 mt-6 max-lg:justify-center">
|
||||
@@ -60,11 +62,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center lg:justify-end flex-1">
|
||||
<div class="flex flex-col gap-6 text-white max-lg:text-center">
|
||||
<h3 class="font-bold">
|
||||
<div class="flex flex-col gap-6 max-lg:text-center">
|
||||
<h3 class="font-bold text-white">
|
||||
لینک های مفید
|
||||
</h3>
|
||||
<ul class="flex flex-col gap-4 font-thin">
|
||||
<ul class="flex flex-col gap-4 font-thin text-slate-300">
|
||||
<li>از طراحان گرافیک است</li>
|
||||
<li>تولید نامفهوم</li>
|
||||
<li>ستون و سطرآنچنان که لازم</li>
|
||||
@@ -73,11 +75,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end flex-1">
|
||||
<div class="flex flex-col gap-6 text-white max-lg:text-center">
|
||||
<h3 class="font-bold">
|
||||
<div class="flex flex-col gap-6 max-lg:text-center">
|
||||
<h3 class="font-bold text-white">
|
||||
لینک های مفید
|
||||
</h3>
|
||||
<ul class="flex flex-col gap-4 font-thin">
|
||||
<ul class="flex flex-col gap-4 font-thin text-slate-300">
|
||||
<li>از طراحان گرافیک است</li>
|
||||
<li>تولید نامفهوم</li>
|
||||
<li>ستون و سطرآنچنان که لازم</li>
|
||||
@@ -86,11 +88,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end flex-1">
|
||||
<div class="flex flex-col gap-6 text-white max-lg:text-center">
|
||||
<h3 class="font-bold w-full">
|
||||
<div class="flex flex-col gap-6 max-lg:text-center">
|
||||
<h3 class="font-bold w-full text-white">
|
||||
لینک های مفید
|
||||
</h3>
|
||||
<ul class="flex flex-col gap-4 font-thin">
|
||||
<ul class="flex flex-col gap-4 font-thin text-slate-300">
|
||||
<li>از طراحان گرافیک است</li>
|
||||
<li>تولید نامفهوم</li>
|
||||
<li>ستون و سطرآنچنان که لازم</li>
|
||||
|
||||
@@ -58,7 +58,7 @@ const onInput = (e: any) => {
|
||||
</NumberFieldIncrement>
|
||||
<NumberFieldInput
|
||||
@input="onInput"
|
||||
class="field-sizing-content bg-transparent outline-none typo-label-md text-black"
|
||||
class="field-sizing-content bg-transparent w-[30px] text-center outline-none typo-label-md text-black"
|
||||
/>
|
||||
<NumberFieldDecrement class="cursor-pointer">
|
||||
<Icon name="ci:minus" class="**:stroke-slate-500 size-5" />
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
// types
|
||||
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import type { SwiperClass } from "swiper/react";
|
||||
|
||||
type Props = {
|
||||
selectedSlide: number;
|
||||
slides: ProductImage[];
|
||||
@@ -12,8 +15,19 @@ const props = defineProps<Props>();
|
||||
const { slides, selectedSlide } = toRefs(props);
|
||||
|
||||
// emit
|
||||
|
||||
const emit = defineEmits(["update:selectedSlide"]);
|
||||
|
||||
// state
|
||||
|
||||
const swiper_instance = ref<SwiperClass | null>(null);
|
||||
|
||||
// methods
|
||||
|
||||
const onSwiper = (swiper: SwiperClass) => {
|
||||
swiper_instance.value = swiper;
|
||||
};
|
||||
|
||||
// computed
|
||||
|
||||
const selectedSlideDetail = computed(() => {
|
||||
@@ -22,17 +36,22 @@ const selectedSlideDetail = computed(() => {
|
||||
})!;
|
||||
});
|
||||
|
||||
const emptySlidesCount = computed(() => {
|
||||
return 3 - slides.value.length > 0 ? 3 - slides.value.length : 0;
|
||||
});
|
||||
|
||||
// methods
|
||||
|
||||
const changeSlide = (id: number) => {
|
||||
emit("update:selectedSlide", id);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col relative gap-6">
|
||||
<div
|
||||
class="bg-white brightness-[97%] w-full relative aspect-square overflow-hidden rounded-200"
|
||||
class="bg-white brightness-[97%] w-full relative aspect-square overflow-hidden rounded-[12px] md:rounded-200"
|
||||
>
|
||||
<Transition name="zoom" mode="out-in">
|
||||
<img
|
||||
@@ -43,24 +62,87 @@ const changeSlide = (id: number) => {
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-6">
|
||||
<div
|
||||
@click="changeSlide(slide.id)"
|
||||
v-for="slide in slides"
|
||||
:class="
|
||||
selectedSlide === slide.id
|
||||
? '!ring-black'
|
||||
: 'ring-transparent'
|
||||
"
|
||||
class="active:scale-95 hover:ring-slate-200 transition-all cursor-pointer brightness-[97%] bg-white aspect-square ring-2 ring-offset-4 rounded-200 w-full overflow-hidden relative"
|
||||
:key="slide.id"
|
||||
|
||||
<div class="relative w-full">
|
||||
<Swiper
|
||||
:slides-per-view="3"
|
||||
:space-between="20"
|
||||
@swiper="onSwiper"
|
||||
class="w-full"
|
||||
>
|
||||
<img
|
||||
class="absolute object-cover size-full"
|
||||
:src="slide.image"
|
||||
:alt="String(slide.id)"
|
||||
<SwiperSlide
|
||||
class="py-4"
|
||||
v-for="slide in slides"
|
||||
:key="slide.id"
|
||||
>
|
||||
<div
|
||||
@click="changeSlide(slide.id)"
|
||||
:class="
|
||||
selectedSlide === slide.id
|
||||
? '!border-black'
|
||||
: 'border-transparent'
|
||||
"
|
||||
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
|
||||
class="absolute object-cover size-full"
|
||||
:src="slide.image"
|
||||
:alt="String(slide.id)"
|
||||
/>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide
|
||||
v-if="emptySlidesCount > 0"
|
||||
v-for="slide in emptySlidesCount"
|
||||
:key="slide"
|
||||
class="py-4"
|
||||
>
|
||||
<div class="brightness-[97%] bg-white aspect-square rounded-[12px] md:rounded-200 w-full" />
|
||||
</SwiperSlide>
|
||||
</Swiper>
|
||||
|
||||
<div
|
||||
v-if="slides.length > 3"
|
||||
@click="swiper_instance?.slidePrev()"
|
||||
class="absolute z-20 -right-4 shadow-lg cursor-pointer shadow-black/10 bottom-[50%] translate-y-1/2 bg-white rounded-full size-10 xs:size-11.5 flex justify-center items-center"
|
||||
>
|
||||
<Icon
|
||||
name="ci:arrow-right"
|
||||
class="**:stroke-black size-5 xs:size-6"
|
||||
:class="swiper_instance?.isBeginning ? '**:stroke-black/30' : ''"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="slides.length > 3"
|
||||
@click="swiper_instance?.slideNext()"
|
||||
class="absolute z-20 -left-4 shadow-lg cursor-pointer shadow-black/10 bottom-[50%] translate-y-1/2 bg-white rounded-full size-10 xs:size-11.5 flex justify-center items-center"
|
||||
>
|
||||
<Icon
|
||||
name="ci:arrow-left"
|
||||
class="**:stroke-black size-5 xs:size-6"
|
||||
:class="swiper_instance?.isEnd ? '**:stroke-black/30' : ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="grid grid-cols-3 gap-6">-->
|
||||
<!-- <div-->
|
||||
<!-- @click="changeSlide(slide.id)"-->
|
||||
<!-- v-for="slide in slides"-->
|
||||
<!-- :class="-->
|
||||
<!-- selectedSlide === slide.id-->
|
||||
<!-- ? '!ring-black'-->
|
||||
<!-- : 'ring-transparent'-->
|
||||
<!-- "-->
|
||||
<!-- class="active:scale-95 hover:ring-slate-200 transition-all cursor-pointer brightness-[97%] bg-white aspect-square ring-2 ring-offset-4 rounded-[12px] md:rounded-200 w-full overflow-hidden relative"-->
|
||||
<!-- :key="slide.id"-->
|
||||
<!-- >-->
|
||||
<!-- <img-->
|
||||
<!-- class="absolute object-cover size-full"-->
|
||||
<!-- :src="slide.image"-->
|
||||
<!-- :alt="String(slide.id)"-->
|
||||
<!-- />-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -21,13 +21,13 @@ const formattedDate = useDateFormat(date.value, "YYYY/MM/DD");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full p-12 rounded-150 bg-white border-[0.5px] border-slate-200 flex flex-col gap-6">
|
||||
<div class="flex justify-between items-start w-full">
|
||||
<div class="w-full p-6 xs:p-12 rounded-150 bg-white border-[0.5px] border-slate-200 flex flex-col gap-6">
|
||||
<div class="flex justify-between items-start w-full max-sm:flex-col gap-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<span class="typo-h-6 text-black">
|
||||
<span class="text-lg font-semibold sm:typo-h-6 text-black">
|
||||
خیلی محصول خوبی بودددد
|
||||
</span>
|
||||
<span class="typo-p-sm text-slate-500">
|
||||
<span class="typo-p-xs sm:typo-p-sm text-slate-500">
|
||||
{{ username }}
|
||||
در
|
||||
{{ formattedDate }}
|
||||
|
||||
@@ -32,7 +32,8 @@ const { picture, price, title, color } = toRefs(props);
|
||||
</div>
|
||||
</div>
|
||||
<Button class="rounded-full">
|
||||
افزودن به سبد
|
||||
افزودن
|
||||
<span class="max-sm:hidden">به سبد</span>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -187,6 +187,8 @@ onUnmounted(() => {
|
||||
:id="`slide-video-${index}`"
|
||||
:muted="swiper_instance?.realIndex !== index ? true : isMuted"
|
||||
loop
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
class="slide-video absolute inset-0 size-full object-cover brightness-90"
|
||||
:src="slide.video"
|
||||
/>
|
||||
@@ -227,11 +229,12 @@ onUnmounted(() => {
|
||||
</h3>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="truncate typo-p-xs md:typo-p-sm lg:typo-p-lg text-white">
|
||||
{{ slide.description }}
|
||||
</span>
|
||||
<span class="truncate typo-p-xs md:typo-p-sm lg:typo-p-lg text-white">
|
||||
{{ slide.description }}
|
||||
</span>
|
||||
<Button
|
||||
class="max-sm:hidden max-lg:typo-label-xs invert rounded-full hover:bg-transparent">
|
||||
class="max-sm:hidden max-lg:typo-label-xs invert rounded-full hover:bg-transparent"
|
||||
>
|
||||
مشاهده
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -69,6 +69,8 @@ watch(
|
||||
v-else
|
||||
autoplay
|
||||
muted
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
src="/video/vid-3.mp4"
|
||||
class="select-none absolute size-full object-cover brightness-[95%]"
|
||||
/>
|
||||
@@ -86,6 +88,8 @@ watch(
|
||||
v-else
|
||||
autoplay
|
||||
muted
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
src="/video/vid-3.mp4"
|
||||
class="overlay-image select-none absolute object-cover size-full brightness-[95%]"
|
||||
/>
|
||||
|
||||
@@ -24,7 +24,7 @@ const { mutateAsync: createComment, isPending: isCreateCommentPending } =
|
||||
const submitComment = async () => {
|
||||
if (userComment.value.length > 3) {
|
||||
await createComment({
|
||||
content: userComment.value,
|
||||
content: userComment.value
|
||||
});
|
||||
|
||||
userComment.value = "";
|
||||
@@ -36,11 +36,11 @@ const submitComment = async () => {
|
||||
|
||||
<template>
|
||||
<section class="bg-slate-50">
|
||||
<div class="flex relative gap-8 my-42 container">
|
||||
<div class="flex relative gap-8 my-42 container max-lg:flex-col">
|
||||
<div
|
||||
class="sticky top-0 flex flex-col gap-6 min-w-[400px] max-h-min bg-white p-8 rounded-xl border-[0.5px] border-slate-200"
|
||||
class="sticky top-0 flex flex-col gap-6 lg:min-w-[400px] h-fit bg-white p-8 rounded-xl border-[0.5px] border-slate-200"
|
||||
>
|
||||
<h3 class="typo-h-3">نظرات کاربران</h3>
|
||||
<h3 class="typo-h-6 max-sm:text-xl md:typo-h-5 lg:typo-h-4">نظرات کاربران</h3>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Rating :rate="2" />
|
||||
<span class="typo-p-sm">
|
||||
|
||||
@@ -9,21 +9,21 @@ const { selectedVariant } = inject("productVariant") as ProductVariantProvideTyp
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="w-full p-[5rem] flex flex-col gap-y-[1.5rem]">
|
||||
<section class="w-full container py-20 flex flex-col gap-y-[1.5rem]">
|
||||
<div class="w-full flex">
|
||||
<span class="text-black typo-h-3"> جزيات محصول </span>
|
||||
<span class="text-black max-lg:hidden typo-h-4 mb-4"> جزيات محصول </span>
|
||||
</div>
|
||||
<div class="w-full flex items-start justify-between gap-[3rem]">
|
||||
<div class="w-8/12">
|
||||
<div class="w-full flex items-start justify-between gap-[3rem] max-lg:flex-col-reverse">
|
||||
<div class="flex-1 w-full">
|
||||
<Accordion />
|
||||
</div>
|
||||
<div class="w-4/12">
|
||||
<div class="w-full lg:w-[450px] xl:w-[600px]">
|
||||
<div
|
||||
class="w-full bg-slate-50 rounded-xl flex-col-center p-[5rem] gap-[1.5rem]"
|
||||
class="w-full bg-slate-50 rounded-xl flex-col-center px-5 py-16 sm:p-[5rem] gap-[1.5rem]"
|
||||
>
|
||||
<span>داخل جعبه چیه؟</span>
|
||||
<span class="typo-h-6 mb-8">داخل جعبه چیه؟</span>
|
||||
<div
|
||||
class="w-full grid grid-cols-2 gap-y-[1.5rem] gap-x-[3rem]"
|
||||
class="w-full grid grid-cols-2 gap-y-[1.5rem] sm:gap-x-[3rem]"
|
||||
>
|
||||
<div
|
||||
v-for="inPackItem in selectedVariant.in_pack_items"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
// import
|
||||
|
||||
import useGetProduct from "~/composables/api/product/useGetProduct";
|
||||
@@ -20,9 +21,7 @@ const selectedColor = ref(product.value!.colors[0]);
|
||||
|
||||
// provide / inject
|
||||
|
||||
const { selectedVariant, changeSelectedVariant } = inject(
|
||||
"productVariant"
|
||||
) as ProductVariantProvideType;
|
||||
const { selectedVariant, changeSelectedVariant } = inject("productVariant") as ProductVariantProvideType;
|
||||
|
||||
// computed
|
||||
|
||||
@@ -32,79 +31,105 @@ const sanitizedProductDescription = computed(() => {
|
||||
|
||||
// watch
|
||||
|
||||
watch(
|
||||
() => selectedVariantId.value,
|
||||
(newId) => {
|
||||
const newVariant = product.value!.variants.find(
|
||||
(variant) => variant.id === newId
|
||||
)!;
|
||||
changeSelectedVariant(newVariant);
|
||||
}
|
||||
);
|
||||
watch(() => selectedVariantId.value, (newId) => {
|
||||
const newVariant = product.value!.variants.find(variant => variant.id === newId)!;
|
||||
changeSelectedVariant(newVariant);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => selectedColor.value,
|
||||
(newValue) => {
|
||||
const filteredVariants = product.value!.variants.filter(
|
||||
(v) => v.color === newValue
|
||||
);
|
||||
selectedVariantId.value = filteredVariants[0].id;
|
||||
selectedVariant.value = filteredVariants[0];
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
watch(() => selectedColor.value, (newValue) => {
|
||||
const filteredVariants = product.value!.variants.filter(v => v.color === newValue);
|
||||
selectedVariantId.value = filteredVariants[0].id;
|
||||
selectedVariant.value = filteredVariants[0];
|
||||
}, {
|
||||
immediate: true
|
||||
});
|
||||
|
||||
watch(() => selectedVariant.value, (newValue) => {
|
||||
selectedQuantity.value = 1;
|
||||
selectedSlide.value = newValue.images[0].id;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => selectedVariant.value,
|
||||
(newValue) => {
|
||||
selectedQuantity.value = 1;
|
||||
selectedSlide.value = newValue.images[0].id;
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-16 container pt-[5rem] pb-28">
|
||||
<Slider
|
||||
class="flex-1"
|
||||
v-model:selectedSlide="selectedSlide"
|
||||
:slides="selectedVariant.images"
|
||||
/>
|
||||
<div class="flex-1 flex flex-col gap-3 mt-12">
|
||||
<span class="typo-label-sm"> سامسونگ </span>
|
||||
<h1 class="typo-h-2">{{ product!.name }}</h1>
|
||||
<div class="flex max-lg:flex-col gap-12 xl:gap-16 container pt-[5rem] pb-28">
|
||||
<div class="flex flex-col gap-3 lg:hidden">
|
||||
<NuxtLink to="#" class="typo-label-sm"> {{ product!.category.name }}</NuxtLink>
|
||||
<h1 class="typo-h-6 xs:typo-h-5 sm:typo-h-4 lg:typo-h-2"> {{ product!.name }} </h1>
|
||||
<div class="flex w-full items-center justify-between h-[85px]">
|
||||
<div class="flex items-end gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span
|
||||
v-if="selectedVariant.discount > 0"
|
||||
class="typo-p-lg relative flex-center w-fit line-through"
|
||||
class="typo-p-lg relative flex-center w-fit"
|
||||
:class="'after:w-full after:h-[2px] after:bg-black after:absolute'"
|
||||
>
|
||||
{{ selectedVariant.price }}
|
||||
</span>
|
||||
<span
|
||||
class="typo-p-2xl relative flex-center w-fit font-medium"
|
||||
>
|
||||
{{
|
||||
selectedVariant.discount > 0
|
||||
? selectedVariant.price
|
||||
: selectedVariant.price
|
||||
}}
|
||||
{{ selectedVariant.discount > 0 ? selectedVariant.price : selectedVariant.price }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedVariant.discount > 0"
|
||||
class="text-white bg-blue-500 mb-1 px-4 py-2 text-xs rounded-full flex items-center gap-1"
|
||||
>
|
||||
<Icon name="bi:percent" class="size-4" />
|
||||
<Icon name="material-symbols:percent" class="size-4" />
|
||||
{{ selectedVariant.discount }}
|
||||
درصد تخفیف
|
||||
</div>
|
||||
</div>
|
||||
<Rating :rate="3" />
|
||||
</div>
|
||||
</div>
|
||||
<Slider
|
||||
class="w-full lg:w-1/2 lg:max-w-[620px]"
|
||||
v-model:selectedSlide="selectedSlide"
|
||||
:slides="selectedVariant.images"
|
||||
/>
|
||||
<div class="lg:w-1/2 flex flex-col gap-3 lg:mt-12">
|
||||
<NuxtLink
|
||||
to="#"
|
||||
class="typo-label-sm max-lg:hidden"
|
||||
>
|
||||
{{ product!.category.name }}
|
||||
</NuxtLink>
|
||||
<h1 class="typo-h-4 xl:typo-h-3 max-lg:hidden"> {{ product!.name }} </h1>
|
||||
<div class="flex w-full items-center justify-between h-[85px] max-lg:hidden">
|
||||
<div class="flex items-end gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span
|
||||
v-if="selectedVariant.discount > 0"
|
||||
class="typo-p-lg relative flex-center w-fit"
|
||||
:class="'after:w-full after:h-[2px] after:bg-black after:absolute'"
|
||||
>
|
||||
{{ selectedVariant.price }}
|
||||
</span>
|
||||
<span
|
||||
class="typo-p-2xl relative flex-center w-fit font-medium"
|
||||
>
|
||||
{{ selectedVariant.discount > 0 ? selectedVariant.price : selectedVariant.price }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedVariant.discount > 0"
|
||||
class="text-white bg-blue-500 mb-1 px-4 py-2 text-xs rounded-full flex items-center gap-1"
|
||||
>
|
||||
<Icon name="material-symbols:percent" class="size-4" />
|
||||
{{ selectedVariant.discount }}
|
||||
<span class="max-sm:hidden">درصد</span>
|
||||
تخفیف
|
||||
</div>
|
||||
|
||||
<Rating :rate="3" class="sm:hidden" />
|
||||
|
||||
</div>
|
||||
|
||||
<Rating :rate="3" class="max-sm:hidden" />
|
||||
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="py-8 typo-p-md text-slate-500 text-justify [&_a]:text-blue-400 [&_strong]:font-bold [&_u]:text-red-400"
|
||||
@@ -112,15 +137,17 @@ watch(
|
||||
/>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="typo-p-lg"> تنوع رنگی : </span>
|
||||
<span class="typo-p-lg">
|
||||
تنوع رنگی :
|
||||
</span>
|
||||
<div class="flex items-center gap-4 py-4">
|
||||
<ColorCircle
|
||||
v-for="color in product!.colors"
|
||||
:key="color"
|
||||
@click="selectedColor = color"
|
||||
selectable
|
||||
:selected="selectedColor === color"
|
||||
:style="{ backgroundColor: color }"
|
||||
:selected="selectedColor === color "
|
||||
:style="{backgroundColor: color}"
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
@@ -128,11 +155,7 @@ watch(
|
||||
|
||||
<div class="flex items-center gap-6 flex-wrap">
|
||||
<ProductVariant
|
||||
@click="
|
||||
variant.in_stock > 0
|
||||
? (selectedVariantId = variant.id)
|
||||
: undefined
|
||||
"
|
||||
@click="variant.in_stock > 0 ? selectedVariantId = variant.id : undefined"
|
||||
v-for="variant in product!.variants.filter(p => p.color === selectedColor)"
|
||||
:key="variant.id"
|
||||
:variantDetail="variant"
|
||||
@@ -141,28 +164,26 @@ watch(
|
||||
</div>
|
||||
|
||||
<div class="w-full flex flex-col gap-6 mt-10">
|
||||
|
||||
<RemainQuantity
|
||||
:maxQuantity="selectedVariant.in_stock"
|
||||
:quantity="selectedQuantity"
|
||||
/>
|
||||
|
||||
<div class="w-full flex gap-3 flex-col">
|
||||
<div class="w-full flex gap-3">
|
||||
<Button class="w-full rounded-full" end-icon="ci:plus">
|
||||
افزودن به سبد خرید
|
||||
</Button>
|
||||
<QuantityCounter
|
||||
v-model="selectedQuantity"
|
||||
:max="selectedVariant.in_stock"
|
||||
/>
|
||||
</div>
|
||||
<Button class="w-full rounded-full" variant="outlined">
|
||||
همین الان بخر
|
||||
<div class="w-full flex gap-3">
|
||||
<Button class="w-full rounded-full" end-icon="ci:plus">
|
||||
افزودن به سبد خرید
|
||||
</Button>
|
||||
<QuantityCounter
|
||||
v-model="selectedQuantity"
|
||||
:max="selectedVariant.in_stock"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<InfoCard />
|
||||
|
||||
<Share />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,17 +21,21 @@ const { selectedVariant } = inject("productVariant") as ProductVariantProvideTyp
|
||||
<video
|
||||
:src="selectedVariant.video"
|
||||
class="object-cover absolute size-full"
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
muted
|
||||
autoplay
|
||||
loop
|
||||
/>
|
||||
<div class="size-full absolute inset-0 bg-black/20" />
|
||||
<StickyCard
|
||||
:color="selectedVariant.color!"
|
||||
:price="selectedVariant.price"
|
||||
:picture="selectedVariant.images[0].image"
|
||||
:title="product!.name"
|
||||
class="absolute right-10 bottom-10"
|
||||
/>
|
||||
<div class="absolute max-sm:flex items-center justify-center max-sm:px-5 sm:right-10 bottom-10 w-full">
|
||||
<StickyCard
|
||||
:color="selectedVariant.color!"
|
||||
:price="selectedVariant.price"
|
||||
:picture="selectedVariant.images[0].image"
|
||||
:title="product!.name"
|
||||
class="max-sm:!w-full"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -5,12 +5,10 @@ import Logger from "~/tools/logger";
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
const config = useRuntimeConfig();
|
||||
const { token, logout } = useAuth();
|
||||
const { token } = useAuth();
|
||||
|
||||
const axios = axiosOriginal.create({
|
||||
timeout: 30000,
|
||||
timeoutErrorMessage: "فرآیند بیش از حد انتظار طول کشید",
|
||||
baseURL: config.public.API_BASE_URL,
|
||||
baseURL: config.public.API_BASE_URL
|
||||
});
|
||||
|
||||
axios.interceptors.request.use((config) => {
|
||||
@@ -30,7 +28,7 @@ export default defineNuxtPlugin(() => {
|
||||
(response) => {
|
||||
return response;
|
||||
},
|
||||
async function (error) {
|
||||
async function(error) {
|
||||
await Logger.axiosErrorLog(error);
|
||||
|
||||
// if (error.status === 401) {
|
||||
@@ -43,7 +41,7 @@ export default defineNuxtPlugin(() => {
|
||||
|
||||
return {
|
||||
provide: {
|
||||
axios,
|
||||
},
|
||||
axios
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ precacheAndRoute(self.__WB_MANIFEST);
|
||||
|
||||
// Version
|
||||
|
||||
const VERSION = "1.0.2";
|
||||
const VERSION = "1.0.4";
|
||||
|
||||
// Service Worker Installation
|
||||
self.addEventListener("install", (event) => {
|
||||
|
||||
Vendored
+2
-1
@@ -84,7 +84,7 @@ declare global {
|
||||
meta_description: string | null;
|
||||
meta_keywords: string | null;
|
||||
meta_rating: number;
|
||||
category: number;
|
||||
category: SubCategory;
|
||||
colors: string[];
|
||||
};
|
||||
|
||||
@@ -195,6 +195,7 @@ declare global {
|
||||
type CartItem = {
|
||||
id: number;
|
||||
product: {
|
||||
id: number;
|
||||
title: string;
|
||||
product_attributes: {
|
||||
id: number;
|
||||
|
||||
Reference in New Issue
Block a user