new changes

This commit is contained in:
Mamalizz
2025-02-23 23:22:42 +03:30
parent 1dcd3647ac
commit 0e171e167b
7 changed files with 135 additions and 114 deletions
@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
// import // import
import type { ToastOptions } from "~/composables/global/useToast"; import type { ToastOptions } from "~/composables/global/useToast";
@@ -9,9 +8,9 @@ import { useToast } from "~/composables/global/useToast";
type Props = { type Props = {
id: number; id: number;
message: string, message: string;
options: ToastOptions options: ToastOptions;
} };
// props // props
@@ -24,7 +23,7 @@ const { destroyToast } = useToast();
const open = ref(true); const open = ref(true);
// method // methods
const onSwipeEnd = () => { const onSwipeEnd = () => {
setTimeout(() => { setTimeout(() => {
@@ -41,37 +40,39 @@ const statusIcon = computed(() => {
case "success": case "success":
return { return {
name: "duo-icons:check-circle", name: "duo-icons:check-circle",
class: "**:fill-success-500 [filter:drop-shadow(0_0_10px_var(--color-success-500))]" class: "**:fill-success-500 [filter:drop-shadow(0_0_10px_var(--color-success-500))]",
}; };
case "error": case "error":
return { return {
name: "duo-icons:alert-triangle", name: "duo-icons:alert-triangle",
class: "**:fill-danger-500 [filter:drop-shadow(0_0_10px_var(--color-danger-500))]" class: "**:fill-danger-500 [filter:drop-shadow(0_0_10px_var(--color-danger-500))]",
}; };
case "info": case "info":
return { return {
name: "duo-icons:info", name: "duo-icons:info",
class: "**:fill-cyan-500 [filter:drop-shadow(0_0_10px_var(--color-cyan-500))]" class: "**:fill-cyan-500 [filter:drop-shadow(0_0_10px_var(--color-cyan-500))]",
}; };
case "warning": case "warning":
return { return {
name: "duo-icons:alert-octagon", name: "duo-icons:alert-octagon",
class: "**:fill-warning-500 [filter:drop-shadow(0_0_10px_var(--color-warning-500))]" class: "**:fill-warning-500 [filter:drop-shadow(0_0_10px_var(--color-warning-500))]",
}; };
default: default:
return { return {
name: "duo-icons:info", name: "duo-icons:info",
class: "**:fill-slate-500 [filter:drop-shadow(0_0_10px_var(--color-slate-500))]" class: "**:fill-slate-500 [filter:drop-shadow(0_0_10px_var(--color-slate-500))]",
}; };
} }
}); });
// watch // watch
watch(() => open.value, (value) => { watch(
if (!value) onSwipeEnd(); () => open.value,
}); (value) => {
if (!value) onSwipeEnd();
}
);
// lifecycle // lifecycle
@@ -80,7 +81,6 @@ onMounted(() => {
open.value = false; open.value = false;
}, options.value.duration ?? 4000); }, options.value.duration ?? 4000);
}); });
</script> </script>
<template> <template>
@@ -92,7 +92,7 @@ onMounted(() => {
:class="options.description ? 'rounded-150' : 'rounded-full'" :class="options.description ? 'rounded-150' : 'rounded-full'"
> >
<ToastTitle <ToastTitle
:class="[ { 'mb-1.5' : options.description } ]" :class="[{ 'mb-1.5': options.description }]"
class="[grid-area:_title] font-medium text-slate-600 text-sm flex items-center gap-2" class="[grid-area:_title] font-medium text-slate-600 text-sm flex items-center gap-2"
> >
<Icon :name="statusIcon.name" :class="statusIcon.class" size="24" /> <Icon :name="statusIcon.name" :class="statusIcon.class" size="24" />
@@ -106,4 +106,4 @@ onMounted(() => {
</div> </div>
</ToastDescription> </ToastDescription>
</ToastRoot> </ToastRoot>
</template> </template>
@@ -1,11 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
// types // types
type Props = { type Props = {
selectedSlide: number; selectedSlide: number;
slides: ProductImage[] slides: ProductImage[];
} };
// props // props
@@ -23,17 +22,18 @@ const selectedSlideDetail = computed(() => {
})!; })!;
}); });
// method // methods
const changeSlide = (id: number) => { const changeSlide = (id: number) => {
emit("update:selectedSlide", id); emit("update:selectedSlide", id);
}; };
</script> </script>
<template> <template>
<div class="flex flex-col relative gap-6"> <div class="flex flex-col relative gap-6">
<div class="bg-white brightness-[97%] w-full relative aspect-square overflow-hidden rounded-200"> <div
class="bg-white brightness-[97%] w-full relative aspect-square overflow-hidden rounded-200"
>
<Transition name="zoom" mode="out-in"> <Transition name="zoom" mode="out-in">
<img <img
:key="selectedSlideDetail.id" :key="selectedSlideDetail.id"
@@ -47,7 +47,11 @@ const changeSlide = (id: number) => {
<div <div
@click="changeSlide(slide.id)" @click="changeSlide(slide.id)"
v-for="slide in slides" v-for="slide in slides"
:class="selectedSlide === slide.id ? '!ring-black' : 'ring-transparent'" :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" 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" :key="slide.id"
> >
@@ -59,4 +63,4 @@ const changeSlide = (id: number) => {
</div> </div>
</div> </div>
</div> </div>
</template> </template>
@@ -29,17 +29,17 @@ const {
isPending: isChatPending, isPending: isChatPending,
isFetchingNextPage: isNextChatPagePending, isFetchingNextPage: isNextChatPagePending,
hasNextPage: hasMoreChat, hasNextPage: hasMoreChat,
fetchNextPage: loadMoreChat fetchNextPage: loadMoreChat,
} = useGetChat(id, isOpen); } = useGetChat(id, isOpen);
const isCreateMessagePending = useIsMutating({ const isCreateMessagePending = useIsMutating({
mutationKey: [MUTATION_KEYS.create_chat] mutationKey: [MUTATION_KEYS.create_chat],
}); });
const canLoadMoreChat = ref(false); const canLoadMoreChat = ref(false);
const isChatScrollLocked = useScrollLock(chatContainerEl); const isChatScrollLocked = useScrollLock(chatContainerEl);
const { y: chatContainerScrollY } = useScroll(chatContainerEl, { const { y: chatContainerScrollY } = useScroll(chatContainerEl, {
behavior: "smooth" behavior: "smooth",
}); });
useInfiniteScroll( useInfiniteScroll(
@@ -56,11 +56,11 @@ useInfiniteScroll(
distance: 10, distance: 10,
direction: "top", direction: "top",
throttle: 1000, throttle: 1000,
canLoadMore: () => canLoadMoreChat.value canLoadMore: () => canLoadMoreChat.value,
} }
); );
// method // methods
const scrollToBottom = () => { const scrollToBottom = () => {
chatContainerScrollY.value = chatContainerEl.value?.scrollHeight ?? 0; chatContainerScrollY.value = chatContainerEl.value?.scrollHeight ?? 0;
@@ -116,7 +116,7 @@ whenever(
}, 2000); }, 2000);
}, },
{ {
once: true once: true,
} }
); );
</script> </script>
@@ -131,16 +131,15 @@ whenever(
<template v-if="isLoggedIn"> <template v-if="isLoggedIn">
<Transition name="zoom" mode="out-in"> <Transition name="zoom" mode="out-in">
<div <div
v-if="!isChatPending" v-if="!isChatPending"
class="p-4.5 h-full flex flex-col justify-between gap-4" class="p-4.5 h-full flex flex-col justify-between gap-4"
> >
<div <div
:style="{ :style="{
maskImage: maskImage:
'linear-gradient(to top, transparent, black 5%, black, black)', 'linear-gradient(to top, transparent, black 5%, black, black)',
}" }"
class="hide-scrollbar flex flex-col py-7 gap-6 h-full overflow-y-auto" class="hide-scrollbar flex flex-col py-7 gap-6 h-full overflow-y-auto"
ref="chatContainerEl" ref="chatContainerEl"
> >
@@ -148,7 +147,10 @@ whenever(
v-if="hasMoreChat" v-if="hasMoreChat"
class="py-2 flex items-center justify-center" class="py-2 flex items-center justify-center"
> >
<Icon name="svg-spinners:3-dots-fade" size="24" /> <Icon
name="svg-spinners:3-dots-fade"
size="24"
/>
</div> </div>
<ChatMessage <ChatMessage
v-for="(message, index) in chatMessages" v-for="(message, index) in chatMessages"
@@ -173,7 +175,6 @@ whenever(
<ChatInput /> <ChatInput />
</div> </div>
<div <div
v-else v-else
class="w-full h-full flex items-center justify-center absolute inset-0" class="w-full h-full flex items-center justify-center absolute inset-0"
@@ -182,7 +183,10 @@ whenever(
</div> </div>
</Transition> </Transition>
</template> </template>
<div class="text-black p-4.5 size-full flex justify-center items-center" v-else> <div
class="text-black p-4.5 size-full flex justify-center items-center"
v-else
>
Please sign in first Please sign in first
</div> </div>
</div> </div>
@@ -1,24 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
// state // state
const isOpen = ref(false); const isOpen = ref(false);
// method // methods
const closeChat = () => isOpen.value = false; const closeChat = () => (isOpen.value = false);
// provide-inject // provide-inject
provide("isOpen", { provide("isOpen", {
isOpen, isOpen,
closeChat closeChat,
}); });
</script> </script>
<template> <template>
<button <button
v-if="!isOpen" v-if="!isOpen"
@click="isOpen = !isOpen" @click="isOpen = !isOpen"
@@ -32,5 +29,4 @@ provide("isOpen", {
</button> </button>
<ChatBoxContainer :isOpen="isOpen" /> <ChatBoxContainer :isOpen="isOpen" />
</template> </template>
@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
// types // types
import AiLoading from "~/components/product/ChatBox/AiLoading.vue"; import AiLoading from "~/components/product/ChatBox/AiLoading.vue";
@@ -12,12 +11,12 @@ const { $queryClient: queryClient } = useNuxtApp();
const { addToast } = useToast(); const { addToast } = useToast();
const { mutateAsync: createMessage, isPending: isCreatingMessage } = useCreateChatMessage(queryClient); const { mutateAsync: createMessage, isPending: isCreatingMessage } =
useCreateChatMessage(queryClient);
const chatInputEl = ref<HTMLInputElement | null>(null); const chatInputEl = ref<HTMLInputElement | null>(null);
// methods
// method
const sendMessage = async () => { const sendMessage = async () => {
const value = chatInputEl.value!.value; const value = chatInputEl.value!.value;
@@ -28,20 +27,18 @@ const sendMessage = async () => {
await createMessage({ await createMessage({
new_message: value, new_message: value,
productId: 1 productId: 1,
}); });
} catch (e) { } catch (e) {
addToast({ addToast({
message: "مشکلی پیش آمده", message: "مشکلی پیش آمده",
options: { options: {
status: "error" status: "error",
} },
}); });
} }
} }
}; };
</script> </script>
<template> <template>
@@ -71,12 +68,20 @@ const sendMessage = async () => {
<form <form
class="transition-all duration-200 relative flex items-center gap-4 w-full shadow-sm rounded-full h-[56px] border pe-3 ps-4" class="transition-all duration-200 relative flex items-center gap-4 w-full shadow-sm rounded-full h-[56px] border pe-3 ps-4"
:class="isCreatingMessage ? 'border-transparent shadow-black/10 bg-white/85 backdrop-blur-xl' : 'border-slate-200 shadow-transparent bg-white'" :class="
isCreatingMessage
? 'border-transparent shadow-black/10 bg-white/85 backdrop-blur-xl'
: 'border-slate-200 shadow-transparent bg-white'
"
> >
<input <input
ref="chatInputEl" ref="chatInputEl"
:disabled="isCreatingMessage" :disabled="isCreatingMessage"
:placeholder="isCreatingMessage ? 'دارم فکر میکنم...' : 'سوال خود را بپرسید'" :placeholder="
isCreatingMessage
? 'دارم فکر میکنم...'
: 'سوال خود را بپرسید'
"
type="text" type="text"
name="text" name="text"
class="focus:outline-none h-full typo-p-sm w-full border-none" class="focus:outline-none h-full typo-p-sm w-full border-none"
@@ -89,13 +94,21 @@ const sendMessage = async () => {
:class="isCreatingMessage ? 'bg-transparent' : 'bg-black'" :class="isCreatingMessage ? 'bg-transparent' : 'bg-black'"
> >
<TransitionGroup name="fade-down"> <TransitionGroup name="fade-down">
<AiLoading v-if="isCreatingMessage" circle :size="75" class="mb-1" /> <AiLoading
<Icon v-else name="iconamoon:send-light" class="absolute rotate-180 **:stroke-white" /> v-if="isCreatingMessage"
circle
:size="75"
class="mb-1"
/>
<Icon
v-else
name="iconamoon:send-light"
class="absolute rotate-180 **:stroke-white"
/>
</TransitionGroup> </TransitionGroup>
</button> </button>
</form> </form>
</div> </div>
</div> </div>
</template> </template>
@@ -303,4 +316,4 @@ const sendMessage = async () => {
transform: translate(-50%, -50%) rotate(443deg); transform: translate(-50%, -50%) rotate(443deg);
} }
} }
</style> </style>
@@ -1,14 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
// types // types
type Props = { type Props = {
id: number, id: number;
reverse?: boolean, reverse?: boolean;
content: string, content: string;
isLast?: boolean, isLast?: boolean;
loadingContent?: boolean loadingContent?: boolean;
} };
// props // props
@@ -23,20 +22,24 @@ const emit = defineEmits(["textUpdate"]);
const { $gsap: gsap } = useNuxtApp(); const { $gsap: gsap } = useNuxtApp();
// method // methods
const showMessage = () => { const showMessage = () => {
gsap.fromTo(`#message-container-${id.value}`, { gsap.fromTo(
rotateX: -50, `#message-container-${id.value}`,
translateY: -40, {
opacity: 0 rotateX: -50,
}, { translateY: -40,
rotateX: 0, opacity: 0,
translateY: 0, },
opacity: 1, {
duration: 0.5, rotateX: 0,
ease: "expo.out" translateY: 0,
}); opacity: 1,
duration: 0.5,
ease: "expo.out",
}
);
}; };
// lifecycle // lifecycle
@@ -47,19 +50,22 @@ onMounted(() => {
} }
if (reverse.value && isLast.value) { if (reverse.value && isLast.value) {
gsap.fromTo(`#chat-message-content-${id.value}`, { gsap.fromTo(
text: "", `#chat-message-content-${id.value}`,
duration: 2.5, {
ease: "none" text: "",
}, { duration: 2.5,
text: { value: content.value, rtl: false }, ease: "none",
duration: 2.5, },
ease: "none", {
onUpdate: () => emit("textUpdate") text: { value: content.value, rtl: false },
}); duration: 2.5,
ease: "none",
onUpdate: () => emit("textUpdate"),
}
);
} }
}); });
</script> </script>
<template> <template>
@@ -81,7 +87,11 @@ onMounted(() => {
</div> </div>
<div <div
class="rounded-150 px-4 py-3" class="rounded-150 px-4 py-3"
:class="reverse ? 'bg-slate-100 text-slate-600' : 'bg-black text-white'" :class="
reverse
? 'bg-slate-100 text-slate-600'
: 'bg-black text-white'
"
> >
<p <p
v-if="!loadingContent" v-if="!loadingContent"
@@ -91,12 +101,8 @@ onMounted(() => {
{{ content }} {{ content }}
</p> </p>
<Icon <Icon v-else name="svg-spinners:3-dots-bounce" size="20" />
v-else
name="svg-spinners:3-dots-bounce"
size="20"
/>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
+15 -17
View File
@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
// import // import
import useGetComments from "~/composables/api/product/useGetComments"; import useGetComments from "~/composables/api/product/useGetComments";
@@ -16,15 +15,16 @@ const page = ref(1);
const { token } = useAuth(); const { token } = useAuth();
const userComment = ref(""); const userComment = ref("");
const { data: comments, refetch : refetchComments } = useGetComments(id, page); const { data: comments, refetch: refetchComments } = useGetComments(id, page);
const { mutateAsync: createComment, isPending: isCreateCommentPending } = useCreateComment(id); const { mutateAsync: createComment, isPending: isCreateCommentPending } =
useCreateComment(id);
// method // methods
const submitComment = async () => { const submitComment = async () => {
if (userComment.value.length > 3) { if (userComment.value.length > 3) {
await createComment({ await createComment({
content: userComment.value content: userComment.value,
}); });
userComment.value = ""; userComment.value = "";
@@ -32,23 +32,25 @@ const submitComment = async () => {
await refetchComments(); await refetchComments();
} }
}; };
</script> </script>
<template> <template>
<section class="bg-slate-50"> <section class="bg-slate-50">
<div class="flex relative gap-8 my-42 container"> <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"> <div
<h3 class="typo-h-3"> 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> <h3 class="typo-h-3">نظرات کاربران</h3>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<Rating /> <Rating />
<span class="typo-p-sm"> <span class="typo-p-sm">
بر اساس {{ comments?.count }} نظر بر اساس {{ comments?.count }} نظر
</span> </span>
</div> </div>
<form @submit.prevent="submitComment" class="flex flex-col gap-6"> <form
@submit.prevent="submitComment"
class="flex flex-col gap-6"
>
<textarea <textarea
:disabled="!token" :disabled="!token"
class="w-full min-h-[200px] field-sizing-content rounded-xl bg-white p-4 border border-slate-200" class="w-full min-h-[200px] field-sizing-content rounded-xl bg-white p-4 border border-slate-200"
@@ -65,10 +67,7 @@ const submitComment = async () => {
نظر بنویسید نظر بنویسید
</Button> </Button>
<NuxtLink v-else to="/signin"> <NuxtLink v-else to="/signin">
<Button <Button type="button" class="rounded-full w-full">
type="button"
class="rounded-full w-full"
>
وارد شوید وارد شوید
</Button> </Button>
</NuxtLink> </NuxtLink>
@@ -90,7 +89,6 @@ const submitComment = async () => {
/> />
</div> </div>
</div> </div>
</div> </div>
</section> </section>
</template> </template>