merge
This commit is contained in:
+12
-2
@@ -6,14 +6,21 @@ from .serializers import AllBlogSerilizer, BlogSerilizer
|
||||
from django.shortcuts import get_object_or_404
|
||||
from utils.pagination import StructurePagination
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
class AllBlogView(APIView):
|
||||
serializer_class = AllBlogSerilizer
|
||||
pagination_class = StructurePagination
|
||||
authentication_classes = []
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="search",
|
||||
description="بگرددددد تو بلاااااگگگووووو",
|
||||
required=False,
|
||||
type=OpenApiTypes.STR,
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="limit",
|
||||
description="لیمیتش",
|
||||
@@ -34,6 +41,9 @@ class AllBlogView(APIView):
|
||||
)
|
||||
def get(self, request):
|
||||
blogs = BlogModel.objects.filter(is_published=True)
|
||||
search_query = request.query_params.get('search', None)
|
||||
if search_query:
|
||||
blogs = blogs.filter(Q(title__icontains=search_query) | Q(content__icontains=search_query))
|
||||
paginator = self.pagination_class()
|
||||
paginated_blogs = paginator.paginate_queryset(blogs, request)
|
||||
blog_ser = self.serializer_class(instance=paginated_blogs, many=True, context={'request': request})
|
||||
@@ -43,7 +53,7 @@ class AllBlogView(APIView):
|
||||
|
||||
class BlogView(APIView):
|
||||
serializer_class = BlogSerilizer
|
||||
|
||||
authentication_classes = []
|
||||
def get_client_ip(self, request):
|
||||
"""Helper function to get the client IP from request headers."""
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
|
||||
@@ -8,6 +8,7 @@ from rest_framework import status
|
||||
|
||||
|
||||
class HomeView(APIView):
|
||||
authentication_classes = []
|
||||
def get(self, request):
|
||||
|
||||
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
|
||||
|
||||
@@ -24,6 +24,7 @@ from rest_framework.permissions import AllowAny
|
||||
|
||||
class AllCategories(APIView):
|
||||
serializer_class = MainCategorySerializer
|
||||
authentication_classes = []
|
||||
@extend_schema(
|
||||
# parameters=[
|
||||
# OpenApiParameter(
|
||||
@@ -50,6 +51,7 @@ class AllCategories(APIView):
|
||||
class ProductView(APIView):
|
||||
serializer_class = ProductSerializer
|
||||
permission_classes = [AllowAny]
|
||||
authentication_classes = []
|
||||
def get(self, request, pk):
|
||||
product = get_object_or_404(ProductModel, id=pk)
|
||||
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique')
|
||||
@@ -61,7 +63,7 @@ class ProductView(APIView):
|
||||
class AllProductsView(APIView):
|
||||
serializer_class = ProductSerializer
|
||||
pagination_class = StructurePagination
|
||||
|
||||
authentication_classes = []
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
@@ -171,9 +173,7 @@ class AllProductsView(APIView):
|
||||
# Price filters
|
||||
price_gte = request.query_params.get('price_gte', None)
|
||||
price_lte = request.query_params.get('price_lte', None)
|
||||
|
||||
if type(price_gte) != int or type(price_lte) != int:
|
||||
return Response({'detail': 'value error price_gte and price_lte should be a number'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if price_gte:
|
||||
products = products.filter(price__gte=price_gte)
|
||||
if price_lte:
|
||||
|
||||
@@ -24,10 +24,10 @@ const { colorObject } = useImageColor(`#category-image-${id.value}`);
|
||||
|
||||
<template>
|
||||
<NuxtLink :to="`/products?category=${id}`">
|
||||
<div class="relative rounded-150 overflow-hidden w-full h-[500px] bg-white brightness-[97%]">
|
||||
<div class="group relative rounded-150 overflow-hidden w-full h-[500px] bg-white brightness-[97%]">
|
||||
<img
|
||||
:id="`category-image-${id}`"
|
||||
class="absolute object-contain size-full"
|
||||
class="group-hover:scale-105 transition-transform duration-200 absolute object-contain size-full"
|
||||
:src="picture"
|
||||
alt=""
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import type { GetProductsFilters } from "~/composables/api/products/useGetProducts";
|
||||
|
||||
// types
|
||||
|
||||
@@ -17,9 +16,9 @@ defineProps<Props>();
|
||||
|
||||
// state
|
||||
|
||||
const params: GetProductsFilters = inject("params");
|
||||
const params : any = inject("params");
|
||||
|
||||
const page = ref(Number(params.page) ?? 1);
|
||||
const page = ref(params?.page ? Number(params.page) : 1);
|
||||
|
||||
// watch
|
||||
|
||||
@@ -82,6 +81,4 @@ watch(
|
||||
</PaginationFirst>
|
||||
</PaginationList>
|
||||
</PaginationRoot>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
</template>
|
||||
@@ -37,21 +37,28 @@ const changeSlide = (id: number) => {
|
||||
<template>
|
||||
<div class="flex flex-col relative gap-6">
|
||||
<div class="bg-white brightness-[97%] w-full relative aspect-square overflow-hidden rounded-200">
|
||||
<img
|
||||
class="size-full absolute object-contain"
|
||||
:src="selectedSlideDetail.picture"
|
||||
:alt="String(selectedSlideDetail.id)"
|
||||
/>
|
||||
<Transition name="zoom" mode="out-in">
|
||||
<img
|
||||
:key="selectedSlideDetail.id"
|
||||
class="size-full absolute object-contain"
|
||||
:src="selectedSlideDetail.picture"
|
||||
:alt="String(selectedSlideDetail.id)"
|
||||
/>
|
||||
</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="cursor-pointer brightness-[97%] bg-white aspect-square ring-2 ring-offset-4 rounded-200 w-full overflow-hidden relative"
|
||||
: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"
|
||||
>
|
||||
<img class="absolute object-cover size-full" :src="slide.picture" :alt="String(slide.id)" />
|
||||
<img
|
||||
class="absolute object-cover size-full"
|
||||
:src="slide.picture"
|
||||
:alt="String(slide.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
// type
|
||||
|
||||
type Props = {
|
||||
title: string,
|
||||
date: string,
|
||||
username: string,
|
||||
content: string,
|
||||
}
|
||||
|
||||
// props
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const { date } = toRefs(props);
|
||||
|
||||
// state
|
||||
|
||||
const formattedDate = useDateFormat(date.value, "YYYY/MM/DD");
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -10,14 +28,15 @@
|
||||
خیلی محصول خوبی بودددد
|
||||
</span>
|
||||
<span class="typo-p-sm text-slate-500">
|
||||
منصور مرزبان در ۱۴۰۳/۱۲/۲ ساعت ۱۲:۳۴
|
||||
{{ username }}
|
||||
در
|
||||
{{ formattedDate }}
|
||||
</span>
|
||||
</div>
|
||||
<Rating />
|
||||
</div>
|
||||
<div class="typo-p-md">
|
||||
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و متون
|
||||
بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است و برای شرد گذشته.
|
||||
{{ content }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -34,12 +34,12 @@ const { colorObject } = useImageColor(`#product-image-${id.value}`);
|
||||
<template>
|
||||
<NuxtLink :to="'/product/' + id">
|
||||
<div
|
||||
class="relative size-full min-h-[31.25rem] rounded-2xl bg-white brightness-[98%] overflow-hidden p-6"
|
||||
class="group relative size-full min-h-[31.25rem] rounded-2xl bg-white brightness-[98%] overflow-hidden p-6"
|
||||
>
|
||||
<img
|
||||
:id="`product-image-${id}`"
|
||||
:src="picture"
|
||||
class="size-full object-contain absolute inset-0"
|
||||
class="group-hover:scale-105 transition-transform duration-200 size-full object-contain absolute inset-0"
|
||||
alt="product-background"
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,29 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
// import
|
||||
|
||||
import useGetComments from "~/composables/api/product/useGetComments";
|
||||
import useCreateComment from "~/composables/api/product/useCreateComment";
|
||||
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||
|
||||
// state
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const id = route.params.id as string | undefined;
|
||||
const page = ref(1);
|
||||
|
||||
const { token } = useAuth();
|
||||
const userComment = ref("");
|
||||
|
||||
const { data: comments, refetch : refetchComments } = useGetComments(id, page);
|
||||
const { mutateAsync: createComment, isPending: isCreateCommentPending } = useCreateComment(id);
|
||||
|
||||
// method
|
||||
|
||||
const submitComment = async () => {
|
||||
if (userComment.value.length > 3) {
|
||||
await createComment({
|
||||
content: userComment.value
|
||||
});
|
||||
|
||||
userComment.value = "";
|
||||
|
||||
await refetchComments();
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="bg-slate-50">
|
||||
<div class="flex gap-12 my-42 container">
|
||||
<div class="flex flex-col gap-6 min-w-fit">
|
||||
<div class="flex relative gap-8 my-42 container">
|
||||
<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">
|
||||
<h3 class="typo-h-3">
|
||||
نظرات کاربران
|
||||
</h3>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Rating />
|
||||
<span class="typo-p-sm">
|
||||
بر اساس ۴ نظر
|
||||
</span>
|
||||
بر اساس {{ comments?.count }} نظر
|
||||
</span>
|
||||
</div>
|
||||
<Button class="rounded-full">
|
||||
نظر بنویسید
|
||||
</Button>
|
||||
<form @submit.prevent="submitComment" class="flex flex-col gap-6">
|
||||
<textarea
|
||||
:disabled="!token"
|
||||
class="w-full min-h-[200px] field-sizing-content rounded-xl bg-white p-4 border border-slate-200"
|
||||
v-model="userComment"
|
||||
placeholder="نظر خود را بنویسید..."
|
||||
/>
|
||||
<Button
|
||||
v-if="token"
|
||||
type="submit"
|
||||
class="rounded-full w-full"
|
||||
:loading="isCreateCommentPending"
|
||||
:disabled="isCreateCommentPending"
|
||||
>
|
||||
نظر بنویسید
|
||||
</Button>
|
||||
<NuxtLink v-else to="/signin">
|
||||
<Button
|
||||
type="button"
|
||||
class="rounded-full w-full"
|
||||
>
|
||||
وارد شوید
|
||||
</Button>
|
||||
</NuxtLink>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex flex-col gap-12">
|
||||
<Comment />
|
||||
<Comment />
|
||||
<Comment />
|
||||
<div class="flex flex-col gap-8 w-full">
|
||||
<Comment
|
||||
v-for="comment in comments!.results"
|
||||
:key="comment.id"
|
||||
title=""
|
||||
:content="comment.content"
|
||||
:date="comment.timestamp"
|
||||
:username="'منصور مرزبان'"
|
||||
/>
|
||||
<div class="flex items-center justify-center w-full">
|
||||
<Pagination
|
||||
:total="comments!.count"
|
||||
:items="comments!.results.map((item, i) => ({ type: 'page', value: i }))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
// imports
|
||||
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type CreateCommentRequest = {
|
||||
content: string
|
||||
};
|
||||
|
||||
const useCreateComment = (id: number | string | undefined) => {
|
||||
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
// method
|
||||
|
||||
const handleCreateComment = async (variables: CreateCommentRequest) => {
|
||||
const { data } = await axios.post(`${API_ENDPOINTS.product.create_comment}/${id}`, variables);
|
||||
return data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (variables: CreateCommentRequest) => handleCreateComment(variables)
|
||||
});
|
||||
};
|
||||
|
||||
export default useCreateComment;
|
||||
@@ -0,0 +1,29 @@
|
||||
// imports
|
||||
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type GetCommentsResponse = ApiPaginated<UserComment>;
|
||||
|
||||
const useGetComments = (id: string | number | undefined, page: Ref<number>) => {
|
||||
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
// methods
|
||||
|
||||
const handleGetComments = async () => {
|
||||
const { data } = await axios.get<GetCommentsResponse>(`${API_ENDPOINTS.product.comments}/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
return useQuery({
|
||||
queryKey: [QUERY_KEYS.comments, id, page],
|
||||
queryFn: () => handleGetComments()
|
||||
});
|
||||
};
|
||||
|
||||
export default useGetComments;
|
||||
@@ -5,6 +5,8 @@ export const API_ENDPOINTS = {
|
||||
send_otp: "/accounts/send_otp"
|
||||
},
|
||||
product: {
|
||||
comments: "/products/comments",
|
||||
create_comment: "/products/comments",
|
||||
get: "/products"
|
||||
},
|
||||
auth: {
|
||||
@@ -24,6 +26,7 @@ export const API_ENDPOINTS = {
|
||||
};
|
||||
|
||||
export const QUERY_KEYS = {
|
||||
comments: "comments",
|
||||
home: "home",
|
||||
chat: "chat",
|
||||
product: "product",
|
||||
|
||||
@@ -2,23 +2,27 @@
|
||||
|
||||
import ChatButton from "~/components/product/ChatBox/ChatButton.vue";
|
||||
import useGetProduct from "~/composables/api/product/useGetProduct";
|
||||
import useGetComments from "~/composables/api/product/useGetComments";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const id = route.params.id as string | undefined;
|
||||
const page = ref(1);
|
||||
|
||||
const { suspense } = useGetProduct(id);
|
||||
const { suspense : suspenseProduct } = useGetProduct(id);
|
||||
const { suspense : suspenseComments} = useGetComments(id, page);
|
||||
|
||||
// ssr
|
||||
|
||||
await useAsyncData(async () => {
|
||||
const response = await suspense();
|
||||
const productResponse = await suspenseProduct();
|
||||
const commentsResponse = await suspenseComments();
|
||||
|
||||
if (response.isError) {
|
||||
if (productResponse.isError || commentsResponse.isError) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: `error : ${response.error.message}`,
|
||||
})
|
||||
statusMessage: `error : product ${id} prefetch error`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -30,7 +34,7 @@ await useAsyncData(async () => {
|
||||
<ProductVideo />
|
||||
<ProductComments />
|
||||
<ProductDetails />
|
||||
<!-- <ProductsSlider title="محصولات مشابه" />-->
|
||||
<!-- <ProductsSlider title="محصولات مشابه" />-->
|
||||
<ChatButton />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -16,7 +16,7 @@ export default defineNuxtPlugin(() => {
|
||||
!config.url?.includes(API_ENDPOINTS.auth.signin) &&
|
||||
!config.url?.includes(API_ENDPOINTS.account.send_otp)
|
||||
) {
|
||||
config.headers.Authorization = `Bearer ${token.value}`;
|
||||
config.headers.Authorization = token.value ? `Bearer ${token.value}` : undefined;
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
Vendored
+10
-1
@@ -43,6 +43,15 @@ declare global {
|
||||
meta_rating: number | null;
|
||||
};
|
||||
|
||||
type UserComment = {
|
||||
"id": number,
|
||||
"content": string,
|
||||
"timestamp": string,
|
||||
"show": boolean,
|
||||
"product": number,
|
||||
"user": number
|
||||
}
|
||||
|
||||
type Category = {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -57,7 +66,7 @@ declare global {
|
||||
"name": string,
|
||||
"slug": string,
|
||||
"icon": string,
|
||||
"image" : string,
|
||||
"image": string,
|
||||
"product_count": string,
|
||||
"parent": string,
|
||||
"show": boolean
|
||||
|
||||
Reference in New Issue
Block a user