This commit is contained in:
Mamalizz
2025-01-30 01:56:14 +03:30
15 changed files with 222 additions and 46 deletions
+12 -2
View File
@@ -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')
+1
View File
@@ -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')
+4 -4
View File
@@ -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:
+2 -2
View File
@@ -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=""
/>
+3 -6
View File
@@ -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>
+22 -3
View File
@@ -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"
/>
+78 -11
View File
@@ -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;
+3
View File
@@ -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",
+10 -6
View File
@@ -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>
+1 -1
View File
@@ -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;
+10 -1
View File
@@ -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