Merge branch 'main' of https://github.com/Byeto-Company/hossein_por_shop
This commit is contained in:
+1
-1
@@ -14,7 +14,7 @@ import { VueQueryDevtools } from "@tanstack/vue-query-devtools";
|
||||
</div>
|
||||
<ToastContainer />
|
||||
<ToastViewport
|
||||
class="[--viewport-padding:_25px] fixed bottom-0 right-0 flex flex-col p-[var(--viewport-padding)] gap-[10px] w-[390px] max-w-[100vw] m-0 list-none z-[2147483647] outline-none"
|
||||
class="[--viewport-padding:_25px] fixed bottom-0 left-0 flex flex-col p-[var(--viewport-padding)] gap-[10px] w-[390px] max-w-[100vw] m-0 list-none z-[2147483647] outline-none"
|
||||
/>
|
||||
</ToastProvider>
|
||||
</NuxtLayout>
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
Zoom animation
|
||||
*/
|
||||
|
||||
.zoom-leave-active,
|
||||
.zoom-enter-active {
|
||||
transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.zoom-enter-to,
|
||||
.zoom-leave-from {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0px);
|
||||
}
|
||||
|
||||
.zoom-enter-from,
|
||||
.zoom-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(15px);
|
||||
}
|
||||
|
||||
/*
|
||||
Fade animation
|
||||
*/
|
||||
|
||||
.fade-leave-active,
|
||||
.fade-enter-active {
|
||||
transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-enter-to,
|
||||
.fade-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
FadeDown animation
|
||||
*/
|
||||
|
||||
.fade-down-leave-active,
|
||||
.fade-down-enter-active {
|
||||
transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-down-enter-to,
|
||||
.fade-down-leave-from {
|
||||
opacity: 1;
|
||||
transform: translateY(0px) scale(1);
|
||||
}
|
||||
|
||||
.fade-down-enter-from,
|
||||
.fade-down-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10px) scale(0.95);
|
||||
}
|
||||
|
||||
/*
|
||||
FadeRightToLeft animation
|
||||
*/
|
||||
|
||||
.fade-right-to-left-leave-active,
|
||||
.fade-right-to-left-enter-active {
|
||||
transform-origin: bottom right;
|
||||
transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-right-to-left-enter-to,
|
||||
.fade-right-to-left-leave-from {
|
||||
opacity: 1;
|
||||
transform: translateX(0px) scale(1);
|
||||
}
|
||||
|
||||
.fade-right-to-left-enter-from,
|
||||
.fade-right-to-left-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px) scale(0.95);
|
||||
}
|
||||
|
||||
/*
|
||||
FadeRight animation
|
||||
*/
|
||||
|
||||
.fade-right-leave-active,
|
||||
.fade-right-enter-active {
|
||||
transform-origin: right;
|
||||
transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-right-enter-to,
|
||||
.fade-right-leave-from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.fade-right-enter-from,
|
||||
.fade-right-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@import "./animations.css";
|
||||
@import "./other.utils.css";
|
||||
@import "./typo.utils.css";
|
||||
@import "./button.comp.css";
|
||||
@@ -166,38 +167,42 @@
|
||||
@keyframes contentShow {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -48%) scale(0.96);
|
||||
transform: scale(0.96);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toastHide {
|
||||
from { opacity: 1 }
|
||||
to { opacity: 0 }
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toastSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(calc(100% + var(--viewport-padding)))
|
||||
transform: translateX(calc(100% + var(--viewport-padding)));
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0)
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toastSlideOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateX(var(--reka-toast-swipe-end-x))
|
||||
transform: translateX(var(--reka-toast-swipe-end-x));
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(calc(100% + var(--viewport-padding)))
|
||||
transform: translateX(calc(100% + var(--viewport-padding)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,31 +2,47 @@
|
||||
|
||||
// types
|
||||
|
||||
import { useImageColor } from "~/composables/useImageColor";
|
||||
|
||||
type Props = {
|
||||
id: number,
|
||||
category: string;
|
||||
count: number;
|
||||
description: string;
|
||||
picture: string;
|
||||
darkLayer?: boolean;
|
||||
}
|
||||
|
||||
// props
|
||||
|
||||
defineProps<Props>();
|
||||
const props = defineProps<Props>();
|
||||
const { id } = toRefs(props);
|
||||
|
||||
// state
|
||||
|
||||
const { colorObject } = useImageColor(`#category-image-${id.value}`);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative rounded-150 overflow-hidden w-full h-[500px]">
|
||||
<img
|
||||
:id="`category-image-${id}`"
|
||||
class="absolute object-cover size-full"
|
||||
:src="picture"
|
||||
alt=""
|
||||
/>
|
||||
<div class="bg-linear-to-t from-black/80 to-transparent absolute z-10 size-full" />
|
||||
<div
|
||||
v-if="darkLayer"
|
||||
class="bg-linear-to-t from-black/50 to-transparent to-40% absolute z-10 size-full"
|
||||
/>
|
||||
|
||||
<div class="absolute z-20 bottom-0 p-6 flex items-end justify-between w-full">
|
||||
|
||||
<div class="flex flex-col gap-2 text-white">
|
||||
<div
|
||||
:class="(colorObject?.isLight && !darkLayer) ? 'text-black' : 'text-white'"
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<div class="typo-s-h-md">
|
||||
تمام دسته ها
|
||||
<span class="typo-p-xs -translate-y-1 inline-block mr-1">
|
||||
@@ -39,7 +55,8 @@ defineProps<Props>();
|
||||
<Icon
|
||||
size="24"
|
||||
name="ci:arrow-left"
|
||||
class="**:stroke-white mb-1"
|
||||
class="mb-1"
|
||||
:class="(colorObject?.isLight && !darkLayer) ? '**:stroke-black' : '**:stroke-white'"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
// types
|
||||
|
||||
type Option = {
|
||||
name: string;
|
||||
children: { name: string }[];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
options: Option[];
|
||||
modelValue: string[];
|
||||
};
|
||||
|
||||
// props
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const { modelValue } = toRefs(props);
|
||||
|
||||
// emit
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
// state
|
||||
|
||||
const value = ref<string[]>([]);
|
||||
|
||||
// watch
|
||||
|
||||
watch(
|
||||
() => value.value,
|
||||
(newValue) => {
|
||||
emit("update:modelValue", newValue);
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => modelValue.value,
|
||||
(newValue: string[]) => {
|
||||
value.value = newValue;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ComboboxRoot class="relative" dir="rtl" :multiple="true" v-model="value">
|
||||
<ComboboxAnchor
|
||||
class="w-full inline-flex items-center justify-between rounded-full border border-slate-200 hover:border-slate-300 focus:border-slate-800 px-[1rem] text-sm leading-none py-3.5 gap-[5px] bg-slate-50 text-black hover:bg-white/90 transition-all data-[placeholder]:text-black/80 typo-label-sm outline-none"
|
||||
>
|
||||
<ComboboxInput
|
||||
class="!bg-transparent outline-none text-black h-full selection:bg-slate-100 placeholder-stone-400"
|
||||
placeholder="جست و جوی دسته بندی"
|
||||
/>
|
||||
<ComboboxTrigger>
|
||||
<Icon name="ci:chevron-down" class="size-5" />
|
||||
</ComboboxTrigger>
|
||||
</ComboboxAnchor>
|
||||
|
||||
<ComboboxContent
|
||||
class="absolute z-10 w-full mt-1.5 bg-slate-50 overflow-hidden rounded-3xl shadow-sm border border-slate-200 will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=bottom]:animate-slideUpAndFade"
|
||||
>
|
||||
<ComboboxViewport class="p-[1rem]">
|
||||
<ComboboxEmpty
|
||||
class="text-mauve8 text-xs font-medium text-center py-5"
|
||||
/>
|
||||
|
||||
<template v-for="(group, index) in options" :key="group.name">
|
||||
<ComboboxGroup>
|
||||
<ComboboxSeparator
|
||||
v-if="index !== 0"
|
||||
class="h-[1px] bg-slate-200 m-[1rem]"
|
||||
/>
|
||||
|
||||
<ComboboxLabel
|
||||
class="px-[1.2rem] w-full text-sm bg-black text-white rounded-full leading-[25px] py-1.5"
|
||||
>
|
||||
{{ group.name }}
|
||||
</ComboboxLabel>
|
||||
|
||||
<ComboboxItem
|
||||
v-for="option in group.children"
|
||||
:key="option.name"
|
||||
:value="option.name"
|
||||
class="text-sm leading-none text-black/90 my-1.5 rounded-full hover:!bg-slate-200 flex items-center py-2.5 px-[1.2rem] relative select-none data-[disabled]:text-slate-50 data-[disabled]:pointer-events-none"
|
||||
>
|
||||
<ComboboxItemIndicator
|
||||
class="absolute left-3 w-[25px] inline-flex items-center justify-center"
|
||||
>
|
||||
<Icon name="ci:checkmark" size="18" />
|
||||
</ComboboxItemIndicator>
|
||||
<span>
|
||||
{{ option.name }}
|
||||
</span>
|
||||
</ComboboxItem>
|
||||
</ComboboxGroup>
|
||||
</template>
|
||||
</ComboboxViewport>
|
||||
</ComboboxContent>
|
||||
</ComboboxRoot>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -34,27 +34,29 @@ const highlights = ref<Highlight[]>([
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="w-full flex-center py-[5rem] gap-[1.25rem] container">
|
||||
<template v-for="(highlight, index) in highlights" :key="index">
|
||||
<div class="flex flex-col-center gap-[.75rem] w-1/4 px-5">
|
||||
<div class="size-[1.5rem] flex-center">
|
||||
<Icon :name="highlight.icon" />
|
||||
</div>
|
||||
<div class="w-full flex-col-center gap-[.25rem]">
|
||||
<section class="w-full border-t-[0.5px] border-slate-200 mt-20">
|
||||
<div class="w-full flex-center py-[5rem] gap-[1.25rem] container">
|
||||
<template v-for="(highlight, index) in highlights" :key="index">
|
||||
<div class="flex flex-col-center gap-[.75rem] w-1/4 px-5">
|
||||
<div class="size-[1.5rem] flex-center">
|
||||
<Icon :name="highlight.icon" />
|
||||
</div>
|
||||
<div class="w-full flex-col-center gap-[.25rem]">
|
||||
<span class="typo-sub-h-md text-black text-center">
|
||||
{{ highlight.title }}
|
||||
</span>
|
||||
<p class="text-slate-500 typo-p-md text-center">
|
||||
{{ highlight.description }}
|
||||
</p>
|
||||
<p class="text-slate-500 typo-p-sm mt-1 text-center">
|
||||
{{ highlight.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w-[1px] h-[5rem] bg-slate-200"
|
||||
v-if="index + 1 != highlights.length"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
class="w-[1px] h-[5rem] bg-slate-200"
|
||||
v-if="index + 1 != highlights.length"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
// state
|
||||
|
||||
const isSideShow = ref(false);
|
||||
|
||||
const isLocked = useScrollLock(window);
|
||||
|
||||
// watch
|
||||
|
||||
watch(
|
||||
() => isSideShow.value,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
isLocked.value = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative z-[90]">
|
||||
<div @click="isSideShow = true">
|
||||
<slot name="button" />
|
||||
</div>
|
||||
<div v-show="isSideShow" class="fixed inset-0">
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="isSideShow"
|
||||
id="side-overlay"
|
||||
class="size-full bg-black/20"
|
||||
@click="isSideShow = !isSideShow"
|
||||
></div>
|
||||
</Transition>
|
||||
<Transition name="fade-right">
|
||||
<div
|
||||
v-if="isSideShow"
|
||||
id="side-content"
|
||||
class="hidden md:flex w-1/3 bg-white h-full rounded-e-[1.5rem] overflow-hidden absolute top-0 min-md:flex-col"
|
||||
>
|
||||
<div
|
||||
class="w-full flex justify-between items-center py-[2.5rem] px-[3rem]"
|
||||
>
|
||||
<span class="typo-h-5">
|
||||
{{ title }}
|
||||
</span>
|
||||
|
||||
<button
|
||||
@click="isSideShow = !isSideShow"
|
||||
class="size-[3.5rem] -me-5 flex-center rounded-full cursor-pointer"
|
||||
>
|
||||
<Icon
|
||||
name="ci:close"
|
||||
size="24"
|
||||
class="**:stroke-black"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="size-full flex flex-col grow overflow-y-auto p-[3rem] pt-0"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,187 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import { PRODUCT_RANGE } from "~/constants";
|
||||
|
||||
// state
|
||||
|
||||
const params = useUrlSearchParams("history");
|
||||
|
||||
const sort_filter = ref(["جدیدترین ها", "گران ترین ها", "ارزان ترین ها"]);
|
||||
|
||||
const options = [
|
||||
{
|
||||
name: "میوه",
|
||||
children: [
|
||||
{ name: "سیب" },
|
||||
{ name: "موز" },
|
||||
{ name: "پرتقال" },
|
||||
{ name: "طالبی" },
|
||||
{ name: "انگور" },
|
||||
{ name: "هندوانه" },
|
||||
{ name: "خربزه" },
|
||||
{ name: "گلابی" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "سبزیجات",
|
||||
children: [
|
||||
{ name: "کلم" },
|
||||
{ name: "بروکلی" },
|
||||
{ name: "هویج" },
|
||||
{ name: "کاهو" },
|
||||
{ name: "اسفناج" },
|
||||
{ name: "چوی بوک" },
|
||||
{ name: "گل کلم" },
|
||||
{ name: "سیب زمینی" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// methods
|
||||
|
||||
const resetFilters = () => {
|
||||
params.sort = null;
|
||||
params.categories = [];
|
||||
params.range = [PRODUCT_RANGE.min, PRODUCT_RANGE.max];
|
||||
params.has_discount = false;
|
||||
params.in_stock = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="size-full flex flex-col gap-14 justify-between">
|
||||
<div class="w-full flex flex-col gap-10">
|
||||
<div class="flex items-center justify-between w-full gap-5">
|
||||
<div
|
||||
class="flex items-center justify-start gap-2 text-lg w-full"
|
||||
>
|
||||
<Icon name="ci:filter-list" size="24" />
|
||||
ترتیب بر اساس
|
||||
</div>
|
||||
<div class="w-full flex items-start justify-end gap-2">
|
||||
<button
|
||||
v-for="(sort, index) in sort_filter"
|
||||
:key="index"
|
||||
@click="params.sort = sort"
|
||||
:class="
|
||||
params.sort == sort
|
||||
? 'bg-black text-white'
|
||||
: 'bg-slate-100'
|
||||
"
|
||||
class="py-1 px-3 cursor-pointer text-nowrap transition-all rounded-full text-sm"
|
||||
>
|
||||
{{ sort }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full gap-5">
|
||||
<div
|
||||
class="flex items-center justify-start gap-2 text-lg w-full"
|
||||
>
|
||||
<Icon name="ci:grid" size="24" />
|
||||
دسته بندی
|
||||
</div>
|
||||
<ComboBox :options="options" v-model="params.categories" />
|
||||
<div class="w-full flex flex-wrap gap-2 px-[1rem]">
|
||||
<span
|
||||
v-for="(sort_param, index) in params.categories"
|
||||
:key="index"
|
||||
class="py-1 px-3 cursor-pointer text-nowrap bg-slate-100 rounded-full text-sm"
|
||||
>
|
||||
{{ sort_param }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full gap-5">
|
||||
<div
|
||||
class="flex items-center justify-start gap-2 text-lg w-full"
|
||||
>
|
||||
<Icon name="ci:scan-box" size="24" />
|
||||
محدوده قیمت
|
||||
</div>
|
||||
<SliderRoot
|
||||
v-model="params.range"
|
||||
class="relative flex items-center select-none touch-none h-5"
|
||||
dir="rtl"
|
||||
:min="PRODUCT_RANGE.min"
|
||||
:max="PRODUCT_RANGE.max"
|
||||
:step="1"
|
||||
>
|
||||
<SliderTrack
|
||||
class="bg-black/10 relative grow rounded-full h-[3px]"
|
||||
>
|
||||
<SliderRange
|
||||
class="absolute bg-black rounded-full h-full"
|
||||
/>
|
||||
</SliderTrack>
|
||||
<SliderThumb
|
||||
v-for="thumb in Object.keys(PRODUCT_RANGE)"
|
||||
:key="thumb"
|
||||
class="block w-5 h-5 bg-white border-2 border-slate-400 rounded-[10px] hover:bg-slate-100 focus:outline-none focus:shadow-[0_0_0_5px] focus:shadow-black/60"
|
||||
/>
|
||||
</SliderRoot>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-center gap-2">
|
||||
<span class="text-sm text-black">حداقل</span>
|
||||
<span class="text-sm text-black">
|
||||
{{
|
||||
"range" in params
|
||||
? params.range[0].toLocaleString()
|
||||
: PRODUCT_RANGE.min
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-center gap-2">
|
||||
<span class="text-sm text-black">حداکثر</span>
|
||||
<span class="text-sm text-black">
|
||||
{{
|
||||
"range" in params
|
||||
? params.range[1].toLocaleString()
|
||||
: PRODUCT_RANGE.max
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between w-full gap-5">
|
||||
<span class="text-black">فقط کالاهای تخفیف دار</span>
|
||||
<SwitchRoot
|
||||
v-model="params.has_discount"
|
||||
class="w-[3rem] h-[1.8rem] flex data-[state=unchecked]:bg-slate-200 data-[state=checked]:bg-stone-800 border border-slate-300 data-[state=checked]:border-stone-700 rounded-full relative transition-all focus-within:outline-none"
|
||||
>
|
||||
<SwitchThumb
|
||||
class="size-6 my-auto bg-white text-sm ms-1 flex items-center justify-center shadow-xl rounded-full transition-transform translate-x-0.5 will-change-transform data-[state=checked]:-translate-x-[68%]"
|
||||
/>
|
||||
</SwitchRoot>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between w-full gap-5">
|
||||
<span class="text-black">فقط کالاهای موجود</span>
|
||||
|
||||
<SwitchRoot
|
||||
v-model="params.in_stock"
|
||||
class="w-[3rem] h-[1.8rem] flex data-[state=unchecked]:bg-slate-200 data-[state=checked]:bg-stone-800 border border-slate-300 data-[state=checked]:border-stone-700 rounded-full relative transition-all focus-within:outline-none"
|
||||
>
|
||||
<SwitchThumb
|
||||
class="size-6 my-auto bg-white text-sm ms-1 flex items-center justify-center shadow-xl rounded-full transition-transform translate-x-0.5 will-change-transform data-[state=checked]:-translate-x-[68%]"
|
||||
/>
|
||||
</SwitchRoot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
end-icon="ci:close"
|
||||
variant="solid"
|
||||
@click="resetFilters"
|
||||
class="rounded-full py-4 !cursor-pointer"
|
||||
>
|
||||
بازنشانی به پیش فرض
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -12,7 +12,7 @@ const {} = toRefs(props);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="h-[100svh] mt-20 px-20">
|
||||
<section class="mt-20 px-20">
|
||||
<div class="flex items-center justify-between mb-20">
|
||||
<span class="typo-h-4 text-black">
|
||||
مقالات اخیر سایت
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
// import
|
||||
|
||||
import { useDraggable } from "@vueuse/core";
|
||||
|
||||
// state
|
||||
|
||||
const clipPathPercent = ref(49);
|
||||
@@ -13,39 +8,43 @@ const previewContainerEl = ref<HTMLElement | null>(null);
|
||||
|
||||
const { x: dragAxisX } = useDraggable(draggableEl, {
|
||||
initialValue: { x: 0, y: 0 },
|
||||
axis: "x"
|
||||
axis: "x",
|
||||
});
|
||||
|
||||
// watch
|
||||
|
||||
watch(() => dragAxisX.value, (newValue) => {
|
||||
const clientRect = previewContainerEl.value?.getBoundingClientRect()!;
|
||||
const percent = clientRect.width / 100;
|
||||
const clipPercent = (newValue - clientRect.x - 8) / percent;
|
||||
if (clipPercent >= 5 && clipPercent <= 95) {
|
||||
clipPathPercent.value = clipPercent;
|
||||
watch(
|
||||
() => dragAxisX.value,
|
||||
(newValue) => {
|
||||
const clientRect = previewContainerEl.value?.getBoundingClientRect()!;
|
||||
const percent = clientRect.width / 100;
|
||||
const clipPercent = (newValue - clientRect.x - 8) / percent;
|
||||
if (clipPercent >= 5 && clipPercent <= 95) {
|
||||
clipPathPercent.value = clipPercent;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="flex flex-col items-center gap-3 mb-16">
|
||||
<span class="typo-p-md text-slate-500">یک متن تست لورم</span>
|
||||
<span class="typo-h-3 text-black">تفاوت محصول را ببینید در اینجا</span>
|
||||
<span class="typo-h-3 text-black"
|
||||
>تفاوت محصول را ببینید در اینجا</span
|
||||
>
|
||||
</div>
|
||||
<div ref="previewContainerEl" class="rounded-200 overflow-hidden h-[90svh] relative">
|
||||
|
||||
<div
|
||||
ref="previewContainerEl"
|
||||
class="rounded-200 overflow-hidden h-[90svh] relative"
|
||||
>
|
||||
<img
|
||||
src="/img/hero-bg.jpg"
|
||||
class="select-none absolute size-full object-cover"
|
||||
alt=""
|
||||
/>
|
||||
|
||||
<div
|
||||
class="absolute size-full right-0 w-full"
|
||||
>
|
||||
<div class="absolute size-full right-0 w-full">
|
||||
<img
|
||||
src="/img/hero-bg.jpg"
|
||||
class="overlay-image select-none absolute object-cover size-full hue-rotate-200 brightness-35"
|
||||
@@ -53,7 +52,7 @@ watch(() => dragAxisX.value, (newValue) => {
|
||||
/>
|
||||
<div
|
||||
:style="{
|
||||
left: `${clipPathPercent}%`
|
||||
left: `${clipPathPercent}%`,
|
||||
}"
|
||||
ref="draggableEl"
|
||||
class="select-none w-2 h-full bg-white absolute left-0 flex items-center justify-center"
|
||||
@@ -61,12 +60,18 @@ watch(() => dragAxisX.value, (newValue) => {
|
||||
<div
|
||||
class="cursor-grab hover:scale-115 transition-transform rounded-full absolute bg-white size-11 flex items-center justify-center"
|
||||
>
|
||||
<Icon name="ci:arrows" size="24" class="**:stroke-black" />
|
||||
<Icon
|
||||
name="ci:arrows"
|
||||
size="24"
|
||||
class="**:stroke-black"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 p-10 w-full flex justify-between items-end bg-linear-to-t from-black/55 to-transparent">
|
||||
<div
|
||||
class="absolute bottom-0 p-10 w-full flex justify-between items-end bg-linear-to-t from-black/55 to-transparent"
|
||||
>
|
||||
<div class="flex flex-col gap-2 text-white">
|
||||
<span class="typo-p-md">رنگ محصول</span>
|
||||
<span class="typo-h-3">نارنجی</span>
|
||||
@@ -76,7 +81,6 @@ watch(() => dragAxisX.value, (newValue) => {
|
||||
<span class="typo-h-3">سفید</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -90,4 +94,4 @@ watch(() => dragAxisX.value, (newValue) => {
|
||||
v-bind('clipPathPercent + "%"') 100%
|
||||
);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
circle?: boolean,
|
||||
size?: number
|
||||
}
|
||||
|
||||
// props
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 200
|
||||
});
|
||||
const { circle } = toRefs(props);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
height: `${size}px`,
|
||||
width: circle ? `${size}px` : '100%',
|
||||
}"
|
||||
class="relative flex items-center w-full justify-center shrink-0"
|
||||
:class="{
|
||||
'rounded-full overflow-hidden': circle,
|
||||
}"
|
||||
>
|
||||
<img
|
||||
:style="{
|
||||
maskImage: 'radial-gradient(black, transparent 70%)'
|
||||
}"
|
||||
src="/public/img/ai-loading-2.gif"
|
||||
class="size-full object-cover absolute pt-2"
|
||||
alt="ai-loading"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,179 @@
|
||||
<script setup lang="ts">
|
||||
// import
|
||||
|
||||
import AiLoading from "~/components/product/ChatBox/AiLoading.vue";
|
||||
import useGetChat from "~/composables/api/chat/useGetChat";
|
||||
import ChatInput from "~/components/product/ChatBox/ChatInput.vue";
|
||||
import { useIsMutating } from "@tanstack/vue-query";
|
||||
import { MUTATION_KEYS } from "~/constants";
|
||||
import CloseButton from "~/components/product/ChatBox/CloseButton.vue";
|
||||
|
||||
// provide-inject
|
||||
|
||||
const { isOpen } = inject("isOpen") as any;
|
||||
|
||||
// state
|
||||
|
||||
const id = ref(1);
|
||||
|
||||
const chatContainerEl = ref<HTMLElement | null>(null);
|
||||
|
||||
const lastMessageBeforeUpdate = ref(0);
|
||||
|
||||
const {
|
||||
data: chat,
|
||||
isPending: isChatPending,
|
||||
isFetchingNextPage: isNextChatPagePending,
|
||||
hasNextPage: hasMoreChat,
|
||||
fetchNextPage: loadMoreChat,
|
||||
} = useGetChat(id, isOpen);
|
||||
const isCreateMessagePending = useIsMutating({
|
||||
mutationKey: [MUTATION_KEYS.create_chat],
|
||||
});
|
||||
|
||||
const canLoadMoreChat = ref(false);
|
||||
|
||||
const isChatScrollLocked = useScrollLock(chatContainerEl);
|
||||
const { y: chatContainerScrollY } = useScroll(chatContainerEl, {
|
||||
behavior: "smooth",
|
||||
});
|
||||
|
||||
useInfiniteScroll(
|
||||
chatContainerEl,
|
||||
async () => {
|
||||
if (hasMoreChat.value && !isChatPending.value) {
|
||||
lastMessageBeforeUpdate.value = chatMessages.value
|
||||
? chatMessages.value[0].id
|
||||
: 0;
|
||||
await loadMoreChat();
|
||||
}
|
||||
},
|
||||
{
|
||||
distance: 10,
|
||||
direction: "top",
|
||||
throttle: 1000,
|
||||
canLoadMore: () => canLoadMoreChat.value,
|
||||
}
|
||||
);
|
||||
|
||||
// method
|
||||
|
||||
const scrollToBottom = () => {
|
||||
chatContainerScrollY.value = chatContainerEl.value?.scrollHeight ?? 0;
|
||||
};
|
||||
|
||||
// computed
|
||||
|
||||
const chatMessages = computed(() => {
|
||||
return chat.value?.pages
|
||||
.flatMap((page) => {
|
||||
return page.results;
|
||||
})
|
||||
.reverse();
|
||||
});
|
||||
|
||||
// watch
|
||||
|
||||
watch(
|
||||
() => isCreateMessagePending.value,
|
||||
(newValue) => {
|
||||
requestAnimationFrame(() => {
|
||||
scrollToBottom();
|
||||
isChatScrollLocked.value = !!newValue;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => chat.value,
|
||||
() => {
|
||||
if (canLoadMoreChat.value) {
|
||||
const scrollTopOld = chatContainerEl.value!.scrollTop - 100;
|
||||
requestAnimationFrame(() => {
|
||||
const lastChatMessageEl = document.querySelector(
|
||||
`#message-container-${lastMessageBeforeUpdate.value}`
|
||||
) as HTMLElement;
|
||||
lastChatMessageEl?.scrollIntoView();
|
||||
chatContainerEl.value!.scrollTop =
|
||||
chatContainerEl.value!.scrollTop + scrollTopOld;
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
whenever(
|
||||
() => !!chatContainerEl.value,
|
||||
() => {
|
||||
requestAnimationFrame(() => {
|
||||
scrollToBottom();
|
||||
});
|
||||
setTimeout(() => {
|
||||
canLoadMoreChat.value = true;
|
||||
}, 2000);
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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"
|
||||
>
|
||||
<CloseButton :disabled="!!isCreateMessagePending" />
|
||||
|
||||
<Transition name="zoom" mode="out-in">
|
||||
<div
|
||||
v-if="!isChatPending"
|
||||
class="p-4.5 h-full flex flex-col justify-between gap-4"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
maskImage:
|
||||
'linear-gradient(to top, transparent, black 5%, black, black)',
|
||||
}"
|
||||
class="hide-scrollbar flex flex-col py-7 gap-6 h-full overflow-y-auto"
|
||||
ref="chatContainerEl"
|
||||
>
|
||||
<div
|
||||
v-if="hasMoreChat"
|
||||
class="py-2 flex items-center justify-center"
|
||||
>
|
||||
<Icon name="svg-spinners:3-dots-fade" size="24" />
|
||||
</div>
|
||||
<ChatMessage
|
||||
v-for="(message, index) in chatMessages"
|
||||
:key="message.id"
|
||||
:id="message.id"
|
||||
:reverse="message.sender === 'ai'"
|
||||
:content="message.content"
|
||||
:isLast="chatMessages?.length === index + 1"
|
||||
@textUpdate="scrollToBottom"
|
||||
/>
|
||||
<ChatMessage
|
||||
v-if="!!isCreateMessagePending"
|
||||
:id="Date.now() + 1"
|
||||
reverse
|
||||
content=""
|
||||
isLast
|
||||
@textUpdate="scrollToBottom"
|
||||
:loadingContent="!!isCreateMessagePending"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ChatInput />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full flex items-center justify-center absolute inset-0"
|
||||
>
|
||||
<AiLoading />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
// types
|
||||
|
||||
type Props = {}
|
||||
|
||||
// props
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const {} = toRefs(props);
|
||||
|
||||
// state
|
||||
|
||||
const isOpen = ref(false);
|
||||
|
||||
// method
|
||||
|
||||
const closeChat = () => isOpen.value = false;
|
||||
|
||||
// provide-inject
|
||||
|
||||
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"
|
||||
>
|
||||
<Icon
|
||||
name="streamline:artificial-intelligence-spark"
|
||||
class="**:stroke-white"
|
||||
size="26"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<ChatBoxContainer :isOpen="isOpen" />
|
||||
|
||||
</template>
|
||||
@@ -0,0 +1,305 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
// types
|
||||
|
||||
import AiLoading from "~/components/product/ChatBox/AiLoading.vue";
|
||||
import useCreateChatMessage from "~/composables/api/chat/useCreateChatMessage";
|
||||
|
||||
// state
|
||||
|
||||
const { $queryClient: queryClient } = useNuxtApp();
|
||||
|
||||
const { addToast } = useToast();
|
||||
|
||||
const { mutateAsync: createMessage, isPending: isCreatingMessage } = useCreateChatMessage(queryClient);
|
||||
|
||||
const chatInputEl = ref<HTMLInputElement | null>(null);
|
||||
|
||||
|
||||
// method
|
||||
|
||||
const sendMessage = async () => {
|
||||
const value = chatInputEl.value!.value;
|
||||
|
||||
if (value && value.length > 0) {
|
||||
try {
|
||||
chatInputEl.value!.value = "";
|
||||
|
||||
await createMessage({
|
||||
new_message: value,
|
||||
productId: 1
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
addToast({
|
||||
message: "مشکلی پیش آمده",
|
||||
options: {
|
||||
status: "error"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div id="poda" class="poda-rotate">
|
||||
<div
|
||||
class="glow w-full"
|
||||
:class="isCreatingMessage ? '' : '!opacity-0'"
|
||||
/>
|
||||
<div
|
||||
class="darkBorderBg w-full"
|
||||
:class="isCreatingMessage ? '' : '!opacity-0'"
|
||||
/>
|
||||
<div
|
||||
class="darkBorderBg w-full"
|
||||
:class="isCreatingMessage ? '' : '!opacity-0'"
|
||||
/>
|
||||
<div
|
||||
class="darkBorderBg w-full"
|
||||
:class="isCreatingMessage ? '' : '!opacity-0'"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="white w-full"
|
||||
:class="isCreatingMessage ? '' : '!opacity-0'"
|
||||
/>
|
||||
|
||||
<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="isCreatingMessage ? 'border-transparent shadow-black/10 bg-white/85 backdrop-blur-xl' : 'border-slate-200 shadow-transparent bg-white'"
|
||||
>
|
||||
<input
|
||||
ref="chatInputEl"
|
||||
:disabled="isCreatingMessage"
|
||||
:placeholder="isCreatingMessage ? 'دارم فکر میکنم...' : 'سوال خود را بپرسید'"
|
||||
type="text"
|
||||
name="text"
|
||||
class="focus:outline-none h-full typo-p-sm w-full border-none"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="isCreatingMessage"
|
||||
@click="sendMessage"
|
||||
class="disabled:cursor-default cursor-pointer outline-none transition-all duration-350 size-[35px] shrink-0 rounded-full flex items-center justify-center"
|
||||
:class="isCreatingMessage ? 'bg-transparent' : 'bg-black'"
|
||||
>
|
||||
<TransitionGroup name="fade-down">
|
||||
<AiLoading v-if="isCreatingMessage" circle :size="75" class="mb-1" />
|
||||
<Icon v-else name="iconamoon:send-light" class="absolute **:stroke-white" />
|
||||
</TransitionGroup>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.white,
|
||||
.darkBorderBg,
|
||||
.glow {
|
||||
max-height: 70px;
|
||||
/* max-width: 314px; */
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
/* Border Radius */
|
||||
border-radius: 99999px;
|
||||
filter: blur(3px);
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.4s;
|
||||
}
|
||||
|
||||
#poda {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.white {
|
||||
max-height: 63px;
|
||||
max-width: 100%;
|
||||
border-radius: 10px;
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.white::before {
|
||||
content: "";
|
||||
z-index: -2;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) rotate(83deg);
|
||||
position: absolute;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 0;
|
||||
filter: brightness(1.4);
|
||||
background-image: conic-gradient(
|
||||
transparent 0%,
|
||||
#a099d8,
|
||||
transparent 8%,
|
||||
transparent 50%,
|
||||
#dfa2da,
|
||||
transparent 58%
|
||||
);
|
||||
/* animation: rotate 4s linear infinite; */
|
||||
transition: all 2s;
|
||||
}
|
||||
|
||||
.darkBorderBg {
|
||||
max-height: 65px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.darkBorderBg::before {
|
||||
content: "";
|
||||
z-index: -2;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) rotate(82deg);
|
||||
position: absolute;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 0;
|
||||
background-image: conic-gradient(
|
||||
transparent,
|
||||
#998fdc,
|
||||
transparent 10%,
|
||||
transparent 50%,
|
||||
#cf7bba,
|
||||
transparent 60%
|
||||
);
|
||||
transition: all 2s;
|
||||
}
|
||||
|
||||
/*
|
||||
#poda:hover > .darkBorderBg::before {
|
||||
transform: translate(-50%, -50%) rotate(262deg);
|
||||
}
|
||||
|
||||
#poda:hover > .glow::before {
|
||||
transform: translate(-50%, -50%) rotate(240deg);
|
||||
}
|
||||
|
||||
#poda:hover > .white::before {
|
||||
transform: translate(-50%, -50%) rotate(263deg);
|
||||
}
|
||||
|
||||
#poda:hover > .border-input::before {
|
||||
transform: translate(-50%, -50%) rotate(250deg);
|
||||
}
|
||||
|
||||
#poda:hover > .darkBorderBg::before {
|
||||
transform: translate(-50%, -50%) rotate(-98deg);
|
||||
}
|
||||
|
||||
#poda:hover > .glow::before {
|
||||
transform: translate(-50%, -50%) rotate(-120deg);
|
||||
}
|
||||
|
||||
#poda:hover > .white::before {
|
||||
transform: translate(-50%, -50%) rotate(-97deg);
|
||||
}
|
||||
|
||||
#poda:hover > .border-input::before {
|
||||
transform: translate(-50%, -50%) rotate(-110deg);
|
||||
}
|
||||
*/
|
||||
|
||||
.poda-rotate > .darkBorderBg::before {
|
||||
animation: rotate-dark-border-bg 2.5s 0.1s linear infinite;
|
||||
}
|
||||
|
||||
.poda-rotate > .glow::before {
|
||||
animation: rotate-glow 2.5s 0.1s linear infinite;
|
||||
}
|
||||
|
||||
.poda-rotate > .white::before {
|
||||
animation: rotate-white 2.5s 0.1s linear infinite;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
#poda:focus-within > .darkBorderBg::before {
|
||||
transform: translate(-50%, -50%) rotate(442deg);
|
||||
transition: all 4s;
|
||||
}
|
||||
|
||||
#poda:focus-within > .glow::before {
|
||||
transform: translate(-50%, -50%) rotate(420deg);
|
||||
transition: all 4s;
|
||||
}
|
||||
|
||||
#poda:focus-within > .white::before {
|
||||
transform: translate(-50%, -50%) rotate(443deg);
|
||||
transition: all 4s;
|
||||
}
|
||||
|
||||
#poda:focus-within > .border-input::before {
|
||||
transform: translate(-50%, -50%) rotate(430deg);
|
||||
transition: all 4s;
|
||||
}
|
||||
*/
|
||||
|
||||
.glow {
|
||||
overflow: hidden;
|
||||
filter: blur(30px);
|
||||
opacity: 0.4;
|
||||
max-height: 130px;
|
||||
max-width: 354px;
|
||||
}
|
||||
|
||||
.glow:before {
|
||||
content: "";
|
||||
z-index: -2;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) rotate(60deg);
|
||||
position: absolute;
|
||||
width: 999px;
|
||||
height: 999px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 0;
|
||||
/*border color, change middle color*/
|
||||
background-image: conic-gradient(
|
||||
transparent,
|
||||
#998fdc 5%,
|
||||
transparent 38%,
|
||||
transparent 50%,
|
||||
#cf7bba 60%,
|
||||
transparent 87%
|
||||
);
|
||||
/* change speed here */
|
||||
/* animation: rotate 4s 0.3s linear infinite; */
|
||||
transition: all 2s;
|
||||
}
|
||||
|
||||
@keyframes rotate-dark-border-bg {
|
||||
100% {
|
||||
transform: translate(-50%, -50%) rotate(442deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate-glow {
|
||||
100% {
|
||||
transform: translate(-50%, -50%) rotate(420deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate-white {
|
||||
100% {
|
||||
transform: translate(-50%, -50%) rotate(443deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
id: number,
|
||||
reverse?: boolean,
|
||||
content: string,
|
||||
isLast?: boolean,
|
||||
loadingContent?: boolean
|
||||
}
|
||||
|
||||
// props
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const { reverse, id, isLast, content } = toRefs(props);
|
||||
|
||||
// emit
|
||||
|
||||
const emit = defineEmits(["textUpdate"]);
|
||||
|
||||
// state
|
||||
|
||||
const { $gsap: gsap } = useNuxtApp();
|
||||
|
||||
// method
|
||||
|
||||
const showMessage = () => {
|
||||
gsap.fromTo(`#message-container-${id.value}`, {
|
||||
rotateX: -50,
|
||||
translateY: -40,
|
||||
opacity: 0
|
||||
}, {
|
||||
rotateX: 0,
|
||||
translateY: 0,
|
||||
opacity: 1,
|
||||
duration: 0.5,
|
||||
ease: "expo.out"
|
||||
});
|
||||
};
|
||||
|
||||
// lifecycle
|
||||
|
||||
onMounted(() => {
|
||||
if (isLast.value) {
|
||||
showMessage();
|
||||
}
|
||||
|
||||
if (reverse.value && isLast.value) {
|
||||
gsap.fromTo(`#chat-message-content-${id.value}`, {
|
||||
text: "",
|
||||
duration: 2.5,
|
||||
ease: "none"
|
||||
}, {
|
||||
text: { value: content.value, rtl: false },
|
||||
duration: 2.5,
|
||||
ease: "none",
|
||||
onUpdate: () => emit("textUpdate")
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="perspective-near">
|
||||
<div
|
||||
:id="`message-container-${id}`"
|
||||
class="flex gap-2.5 origin-top w-full"
|
||||
:class="reverse ? 'flex-row-reverse' : ''"
|
||||
>
|
||||
<div
|
||||
class="relative overflow-hidden flex items-center justify-center mt-px bg-slate-300 rounded-full size-[35px] shrink-0"
|
||||
>
|
||||
<img
|
||||
v-if="!reverse"
|
||||
src="/public/img/hero-bg.jpg"
|
||||
class="size-full object-cover absolute"
|
||||
alt="profile"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-150 px-4 py-3"
|
||||
:class="reverse ? 'bg-slate-100 text-slate-600' : 'bg-black text-white'"
|
||||
>
|
||||
<p
|
||||
v-if="!loadingContent"
|
||||
:id="`chat-message-content-${id}`"
|
||||
class="typo-p-sm font-normal whitespace-pre-wrap"
|
||||
>
|
||||
{{ content }}
|
||||
</p>
|
||||
|
||||
<Icon
|
||||
v-else
|
||||
name="svg-spinners:3-dots-move"
|
||||
size="20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean,
|
||||
}
|
||||
|
||||
// props
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const {} = toRefs(props);
|
||||
|
||||
// provide-inject
|
||||
|
||||
const { closeChat } = inject("isOpen") as any;
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="absolute bg-white border-b border-slate-100 px-5 h-[60px] top-0 z-20 flex justify-between w-full items-center">
|
||||
<span class="typo-p-sm">
|
||||
چت بات هوش مصنوعی
|
||||
</span>
|
||||
<button
|
||||
@click="closeChat"
|
||||
:disabled="disabled"
|
||||
class="hover:bg-slate-100 transition-all disabled:cursor-default cursor-pointer rounded-full size-[25px] flex items-center justify-center"
|
||||
>
|
||||
<Icon name="iconamoon:sign-times-duotone" class="**:stroke-slate-900" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,78 +1,18 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<DialogRoot modal>
|
||||
<DialogTrigger>
|
||||
<SideModal title="فیلتر محصولات">
|
||||
<template #button>
|
||||
<Button
|
||||
end-icon="ci:filter"
|
||||
variant="secondary"
|
||||
class="rounded-full"
|
||||
class="rounded-full py-4 !bg-slate-100 !cursor-pointer"
|
||||
>
|
||||
فیلتر محصولات
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogPortal to="#teleports">
|
||||
<DialogOverlay
|
||||
class="bg-black/60 backdrop-blur-sm data-[state=open]:animate-overlay-show fixed inset-0 z-30"
|
||||
/>
|
||||
<DialogContent
|
||||
class="data-[state=open]:animate-content-show absolute top-1/2 left-1/2 max-h-[85vh] w-[90vw] max-w-[450px] -translate-x-1/2 -translate-y-1/2 rounded-[6px] bg-white p-[25px] shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none z-[100]"
|
||||
>
|
||||
<DialogTitle
|
||||
class="flex items-center typo-h-4 text-black font-semibold"
|
||||
>
|
||||
Edit profile
|
||||
</DialogTitle>
|
||||
<DialogDescription
|
||||
class="text-mauve11 mt-[10px] mb-5 text-sm leading-normal"
|
||||
>
|
||||
Make changes to your profile here. Click save when you're
|
||||
done.
|
||||
</DialogDescription>
|
||||
<fieldset class="mb-[15px] flex items-center gap-5">
|
||||
<label
|
||||
class="text-black w-[90px] text-right text-sm"
|
||||
for="name"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
class="text-black bg-stone-50 shadow-green7 focus:shadow-gray-100 inline-flex h-[35px] w-full flex-1 items-center justify-center rounded-lg px-[10px] text-sm leading-none shadow-[0_0_0_1px] outline-none focus:shadow-[0_0_0_2px]"
|
||||
defaultValue="Pedro Duarte"
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset class="mb-[15px] flex items-center gap-5">
|
||||
<label
|
||||
class="text-black w-[90px] text-right text-sm"
|
||||
for="username"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
class="text-black bg-stone-50 shadow-green7 focus:shadow-gray-100 inline-flex h-[35px] w-full flex-1 items-center justify-center rounded-lg px-[10px] text-sm leading-none shadow-[0_0_0_1px] outline-none focus:shadow-[0_0_0_2px]"
|
||||
defaultValue="@peduarte"
|
||||
/>
|
||||
</fieldset>
|
||||
<div class="mt-[25px] flex justify-end">
|
||||
<DialogClose as-child>
|
||||
<button
|
||||
class="bg-green4 text-green11 text-sm hover:bg-green5 focus:shadow-green7 inline-flex h-[35px] items-center justify-center rounded-lg px-[15px] font-semibold leading-none focus:shadow-[0_0_0_2px] focus:outline-none"
|
||||
>
|
||||
Save changes
|
||||
</button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
<DialogClose
|
||||
class="text-black hover:bg-green4 focus:shadow-green7 absolute top-[10px] right-[10px] inline-flex h-[25px] w-[25px] appearance-none items-center justify-center rounded-full focus:shadow-[0_0_0_2px] focus:outline-none"
|
||||
aria-label="Close"
|
||||
>
|
||||
<Icon name="ci:close" />
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
</template>
|
||||
<FilterProducts />
|
||||
</SideModal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
// import
|
||||
|
||||
import type { ToastOptions } from "~/composables/useToast";
|
||||
|
||||
// type
|
||||
|
||||
type Props = {
|
||||
id: number;
|
||||
message: string,
|
||||
description?: string
|
||||
options: ToastOptions
|
||||
}
|
||||
|
||||
// props
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const { id } = toRefs(props);
|
||||
const { id, options } = toRefs(props);
|
||||
|
||||
// state
|
||||
|
||||
@@ -23,14 +27,49 @@ const open = ref(true);
|
||||
|
||||
const onSwipeEnd = () => {
|
||||
setTimeout(() => {
|
||||
destroyToast(id);
|
||||
destroyToast(id.value);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
// computed
|
||||
|
||||
const statusIcon = computed(() => {
|
||||
const status = options.value.status;
|
||||
|
||||
switch (status) {
|
||||
case "success":
|
||||
return {
|
||||
name: "duo-icons:check-circle",
|
||||
class: "**:fill-success-500 [filter:drop-shadow(0_0_10px_var(--color-success-500))]"
|
||||
};
|
||||
case "error":
|
||||
return {
|
||||
name: "duo-icons:alert-triangle",
|
||||
class: "**:fill-danger-500 [filter:drop-shadow(0_0_10px_var(--color-danger-500))]"
|
||||
};
|
||||
case "info":
|
||||
return {
|
||||
name: "duo-icons:info",
|
||||
class: "**:fill-cyan-500 [filter:drop-shadow(0_0_10px_var(--color-cyan-500))]"
|
||||
};
|
||||
case "warning":
|
||||
return {
|
||||
name: "duo-icons:alert-octagon",
|
||||
class: "**:fill-warning-500 [filter:drop-shadow(0_0_10px_var(--color-warning-500))]"
|
||||
};
|
||||
default:
|
||||
return {
|
||||
name: "duo-icons:info",
|
||||
class: "**:fill-slate-500 [filter:drop-shadow(0_0_10px_var(--color-slate-500))]"
|
||||
};
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// watch
|
||||
|
||||
watch(() => open.value, (value) => {
|
||||
if (!value) onSwipeEnd()
|
||||
if (!value) onSwipeEnd();
|
||||
});
|
||||
|
||||
// lifecycle
|
||||
@@ -38,25 +77,31 @@ watch(() => open.value, (value) => {
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
open.value = false;
|
||||
}, 4000);
|
||||
}, options.value.duration ?? 4000);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ToastRoot
|
||||
:duration="options.duration ?? 4000"
|
||||
@swipeEnd="onSwipeEnd"
|
||||
v-model:open="open"
|
||||
class="bg-white rounded-lg shadow-xl shadow-black/5 border-[0.5px] border-black p-[15px] grid [grid-template-areas:_'title_action'_'description_action'] grid-cols-[auto_max-content] gap-x-[15px] items-center data-[state=open]:animate-toast-in data-[state=closed]:animate-toast-hide data-[swipe=move]:translate-x-[var(--reka-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=cancel]:transition-[transform_200ms_ease-out] data-[swipe=end]:animate-toast-out"
|
||||
class="bg-white shadow-md shadow-black/3 border-t-[0.5px] border-slate-200 border-black p-4 grid [grid-template-areas:_'title_action'_'description_action'] grid-cols-[auto_max-content] gap-x-[15px] items-center data-[state=open]:animate-toast-in data-[state=closed]:animate-toast-hide data-[swipe=move]:translate-x-[var(--reka-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=cancel]:transition-[transform_200ms_ease-out] data-[swipe=end]:animate-toast-out"
|
||||
:class="options.description ? 'rounded-150' : 'rounded-full'"
|
||||
>
|
||||
<ToastTitle class="[grid-area:_title] mb-[5px] font-medium text-black text-sm">
|
||||
{{ message }}
|
||||
<ToastTitle
|
||||
:class="[ { 'mb-1.5' : options.description } ]"
|
||||
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" />
|
||||
<span>{{ message }}</span>
|
||||
</ToastTitle>
|
||||
<ToastDescription v-if="description" as-child>
|
||||
<ToastDescription v-if="options.description" as-child>
|
||||
<div
|
||||
class="[grid-area:_description] m-0 text-black leading-[1.3]"
|
||||
class="[grid-area:_description] m-0 mr-8 text-slate-500 typo-p-sm text-justify"
|
||||
>
|
||||
{{ description }}
|
||||
{{ options.description }}
|
||||
</div>
|
||||
</ToastDescription>
|
||||
</ToastRoot>
|
||||
|
||||
@@ -14,6 +14,6 @@ const { toasts } = useToast();
|
||||
:key="toast.id"
|
||||
:id="toast.id"
|
||||
:message="toast.message"
|
||||
:description="toast.description"
|
||||
:options="toast.options"
|
||||
/>
|
||||
</template>
|
||||
@@ -13,7 +13,7 @@ export type OtpRequest = {
|
||||
// methods
|
||||
|
||||
export const handleOtp = async (variables: OtpRequest) => {
|
||||
const { data } = await axios.post<OtpRequest>(`${API_ENDPOINTS.account.send_otp}`, variables);
|
||||
const { data } = await axios.post(`${API_ENDPOINTS.account.send_otp}`, variables);
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export type SignInRequest = {
|
||||
// methods
|
||||
|
||||
export const handleSignIn = async (variables: SignInRequest) => {
|
||||
const { data } = await axios.post<SignInRequest>(`${API_ENDPOINTS.auth.signin}/`, variables);
|
||||
const { data } = await axios.post(`${API_ENDPOINTS.auth.signin}/`, variables);
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// imports
|
||||
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import axios from "~/configs/axios.config";
|
||||
import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type CreateBranchRequest = {
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
// methods
|
||||
|
||||
export const handleCreateBranch = async ({ name, description }: CreateBranchRequest) => {
|
||||
const payload: CreateBranchRequest = {
|
||||
name,
|
||||
description,
|
||||
};
|
||||
|
||||
await axios.post<CreateBranchRequest>(API_ENDPOINTS.branch.createBranch, payload);
|
||||
};
|
||||
|
||||
// composable
|
||||
|
||||
const useCreateBranch = () => {
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateBranchRequest) => handleCreateBranch(data),
|
||||
});
|
||||
};
|
||||
|
||||
export default useCreateBranch;
|
||||
@@ -0,0 +1,54 @@
|
||||
// imports
|
||||
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import axios from "~/configs/axios.config";
|
||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
import type { ComputedRef } from "vue";
|
||||
|
||||
// types
|
||||
|
||||
export type GetBranchResponse = Branch;
|
||||
|
||||
// methods
|
||||
|
||||
export const handleGetBranch = async (
|
||||
branchId: string,
|
||||
page: string | undefined,
|
||||
folderId: string | undefined,
|
||||
sort: string | undefined,
|
||||
signal: AbortSignal
|
||||
) => {
|
||||
|
||||
const { data } = await axios.get<GetBranchResponse>(`${API_ENDPOINTS.branch.get}/${branchId}`, {
|
||||
signal,
|
||||
params: {
|
||||
sort_by: sort,
|
||||
folder_id: folderId,
|
||||
offset: ((!!page ? Number(page) : 1) * 20) - 20,
|
||||
limit: 20
|
||||
}
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
// composable
|
||||
|
||||
const useGetBranch = (
|
||||
branchId: ComputedRef<string>,
|
||||
page: ComputedRef<string | undefined>,
|
||||
folderId: ComputedRef<string | undefined>,
|
||||
sort: ComputedRef<string | undefined>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: [QUERY_KEYS.branch, branchId, page, folderId, sort],
|
||||
queryFn: ({ signal }) => handleGetBranch(
|
||||
branchId.value,
|
||||
page.value,
|
||||
folderId.value,
|
||||
sort.value,
|
||||
signal
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
export default useGetBranch;
|
||||
@@ -0,0 +1,29 @@
|
||||
// imports
|
||||
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import axios from "~/configs/axios.config";
|
||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type GetBranchesResponse = Branch[];
|
||||
|
||||
// methods
|
||||
|
||||
export const handleGetBranches = async () => {
|
||||
const { data } = await axios.get<GetBranchesResponse>(`${API_ENDPOINTS.branch.getAll}`);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
// composable
|
||||
|
||||
const useGetBranches = () => {
|
||||
return useQuery({
|
||||
staleTime: 60 * 1000,
|
||||
queryKey: [QUERY_KEYS.branches],
|
||||
queryFn: () => handleGetBranches()
|
||||
});
|
||||
};
|
||||
|
||||
export default useGetBranches;
|
||||
@@ -0,0 +1,31 @@
|
||||
// imports
|
||||
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import axios from "~/configs/axios.config";
|
||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type GetUserBranchesResponse = Branch[];
|
||||
|
||||
// methods
|
||||
|
||||
export const handleGetUserBranches = async () => {
|
||||
const { data } = await axios.get<GetUserBranchesResponse>(
|
||||
`${API_ENDPOINTS.branch.getUserBranches}`
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
// composable
|
||||
|
||||
const useGetUserBranches = () => {
|
||||
return useQuery({
|
||||
staleTime: 60 * 1000,
|
||||
queryKey: [QUERY_KEYS.userBranches],
|
||||
queryFn: () => handleGetUserBranches(),
|
||||
});
|
||||
};
|
||||
|
||||
export default useGetUserBranches;
|
||||
@@ -0,0 +1,116 @@
|
||||
// imports
|
||||
|
||||
import { QueryClient, useMutation } from "@tanstack/vue-query";
|
||||
import type { InfiniteData } from "@tanstack/vue-query";
|
||||
import axios from "~/configs/axios.config";
|
||||
import { API_ENDPOINTS, MUTATION_KEYS, QUERY_KEYS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type CreateChatMessageRequest = {
|
||||
productId: string | number;
|
||||
new_message: string;
|
||||
};
|
||||
|
||||
export type CreateChatMessageResponse = Chat[]
|
||||
|
||||
// methods
|
||||
|
||||
export const handleCreateChatMessage = async (variables: CreateChatMessageRequest) => {
|
||||
const { data } = await axios.post<CreateChatMessageResponse>(`${API_ENDPOINTS.chat.new_message}/${variables.productId}`, variables, {
|
||||
headers: {
|
||||
Authorization: `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoyMTY3ODE2OTAwLCJpYXQiOjE3MzU4MTY5MDAsImp0aSI6ImQwN2E2Y2Y2NzgwZjRlNTE5NWIzOGQxMTAzYzU4NDQ3IiwidXNlcl9pZCI6NX0.slwd7ZSV7nUXEuDTYwwHUOo9ekCefwEEL4kVv2vSTFo`
|
||||
}
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
// composable
|
||||
|
||||
const useCreateChatMessage = (queryClient: QueryClient) => {
|
||||
return useMutation({
|
||||
mutationKey: [MUTATION_KEYS.create_chat],
|
||||
mutationFn: (variables: CreateChatMessageRequest) => handleCreateChatMessage(variables),
|
||||
onMutate: (newMessage) => {
|
||||
const prevData = queryClient.getQueriesData({ queryKey: [QUERY_KEYS.chat] });
|
||||
|
||||
queryClient.setQueryData<InfiniteData<ApiPaginated<Chat>>>([QUERY_KEYS.chat], (oldData) => {
|
||||
const lastPage = oldData!.pages[oldData!.pages.length - 1];
|
||||
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
count: lastPage.count,
|
||||
next: lastPage.next,
|
||||
previous: lastPage.previous,
|
||||
results: [
|
||||
{
|
||||
id: Date.now(),
|
||||
content: newMessage.new_message,
|
||||
sender: "user"
|
||||
}
|
||||
]
|
||||
},
|
||||
...oldData!.pages
|
||||
],
|
||||
pageParams: [
|
||||
...oldData!.pageParams,
|
||||
{
|
||||
limit: 10,
|
||||
offset: 0
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
return { prevData: prevData ? prevData[0][1] : undefined };
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
|
||||
queryClient.setQueryData<InfiniteData<ApiPaginated<Chat>>>([QUERY_KEYS.chat], (oldData) => {
|
||||
if (oldData) {
|
||||
const lastPage = oldData!.pages[oldData!.pages.length - 1];
|
||||
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
count: lastPage.count,
|
||||
next: lastPage.next,
|
||||
previous: lastPage.previous,
|
||||
results: {
|
||||
...response[0],
|
||||
id: Date.now()
|
||||
}
|
||||
},
|
||||
...oldData!.pages
|
||||
],
|
||||
pageParams: [
|
||||
...oldData!.pageParams,
|
||||
{
|
||||
limit: 10,
|
||||
offset: 0
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
return oldData;
|
||||
});
|
||||
|
||||
},
|
||||
onError: (err, newMessage, context) => {
|
||||
if (context) {
|
||||
queryClient.setQueryData(
|
||||
[QUERY_KEYS.chat],
|
||||
context.prevData
|
||||
);
|
||||
}
|
||||
},
|
||||
onSettled: (newMessage) => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.chat] });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default useCreateChatMessage;
|
||||
@@ -0,0 +1,60 @@
|
||||
// imports
|
||||
|
||||
import { useInfiniteQuery, useQuery } from "@tanstack/vue-query";
|
||||
import axios from "~/configs/axios.config";
|
||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
import type { ComputedRef } from "vue";
|
||||
|
||||
// types
|
||||
|
||||
export type GetBranchResponse = ApiPaginated<Chat>;
|
||||
|
||||
// methods
|
||||
|
||||
export const handleGetChat = async ({ productId, limit, offset }: {
|
||||
productId: number | string,
|
||||
limit: number,
|
||||
offset: number
|
||||
}) => {
|
||||
const { data } = await axios.get<GetBranchResponse>(`${API_ENDPOINTS.chat.messages}/${productId}`, {
|
||||
params: {
|
||||
offset,
|
||||
limit
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoyMTY3ODE2OTAwLCJpYXQiOjE3MzU4MTY5MDAsImp0aSI6ImQwN2E2Y2Y2NzgwZjRlNTE5NWIzOGQxMTAzYzU4NDQ3IiwidXNlcl9pZCI6NX0.slwd7ZSV7nUXEuDTYwwHUOo9ekCefwEEL4kVv2vSTFo`
|
||||
}
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
// composable
|
||||
|
||||
const useGetBranch = (
|
||||
productId: Ref<string | number>,
|
||||
enabled: Ref<boolean>
|
||||
) => {
|
||||
return useInfiniteQuery({
|
||||
enabled,
|
||||
queryKey: [QUERY_KEYS.chat],
|
||||
initialPageParam: {
|
||||
limit: 10,
|
||||
offset: 0
|
||||
},
|
||||
queryFn: ({ pageParam }) => handleGetChat({
|
||||
limit: pageParam.limit,
|
||||
offset: pageParam.offset,
|
||||
productId: productId.value
|
||||
}),
|
||||
getNextPageParam: (lastPage, pages) => {
|
||||
if (!lastPage.next) return undefined;
|
||||
|
||||
return {
|
||||
limit: 10,
|
||||
offset: pages.length * 10
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default useGetBranch;
|
||||
@@ -0,0 +1,45 @@
|
||||
// imports
|
||||
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import axios from "~/configs/axios.config";
|
||||
import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type AddDocRequest = {
|
||||
name: string,
|
||||
parent?: string,
|
||||
branch: string | undefined,
|
||||
type: {
|
||||
title: "File" | "Folder",
|
||||
icon: "bi:folder" | "bi:file-earmark"
|
||||
},
|
||||
content: File | undefined
|
||||
};
|
||||
|
||||
// methods
|
||||
|
||||
export const handleAddDoc = async (variables: AddDocRequest & { updateUploadProgress: (p: number) => void }) => {
|
||||
const { data } = await axios.post<AddDocRequest>(`${API_ENDPOINTS.branch.getDoc}/`, {
|
||||
...variables,
|
||||
type: variables.type.title.toLocaleLowerCase()
|
||||
}, {
|
||||
onUploadProgress: (progressEvent) => {
|
||||
variables.updateUploadProgress(progressEvent.progress!);
|
||||
},
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
// composable
|
||||
|
||||
const useAddDoc = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: AddDocRequest & { updateUploadProgress: (p: number) => void }) => handleAddDoc(variables)
|
||||
});
|
||||
};
|
||||
|
||||
export default useAddDoc;
|
||||
@@ -0,0 +1,27 @@
|
||||
// imports
|
||||
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import axios from "~/configs/axios.config";
|
||||
import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type DeleteDocRequest = {
|
||||
id: number
|
||||
};
|
||||
|
||||
// methods
|
||||
|
||||
export const handleDeleteDoc = async ({ id }: { id: string | undefined }) => {
|
||||
await axios.delete<DeleteDocRequest>(`${API_ENDPOINTS.branch.getDoc}/${id}`);
|
||||
};
|
||||
|
||||
// composable
|
||||
|
||||
const useDeleteDoc = (id: Ref<string | undefined>) => {
|
||||
return useMutation({
|
||||
mutationFn: () => handleDeleteDoc({ id: id.value })
|
||||
});
|
||||
};
|
||||
|
||||
export default useDeleteDoc;
|
||||
@@ -0,0 +1,29 @@
|
||||
// imports
|
||||
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import axios from "~/configs/axios.config";
|
||||
import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type EditDocRequest = {
|
||||
id: number
|
||||
};
|
||||
|
||||
// methods
|
||||
|
||||
export const handleEditDoc = async ({ id, name }: { id: string | undefined, name: string | undefined }) => {
|
||||
await axios.patch<EditDocRequest>(`${API_ENDPOINTS.branch.getDoc}/${id}`, { name });
|
||||
};
|
||||
|
||||
// composable
|
||||
|
||||
const useEditDoc = (id: Ref<string | undefined>) => {
|
||||
return useMutation({
|
||||
mutationFn: ({ name }: { name: Ref<string | undefined> }) => {
|
||||
return handleEditDoc({ id: id.value, name: name.value });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default useEditDoc;
|
||||
@@ -0,0 +1,33 @@
|
||||
// imports
|
||||
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import axios from "~/configs/axios.config";
|
||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type GetDocResponse = DocumentStructure;
|
||||
|
||||
// methods
|
||||
|
||||
export const handleGetDoc = async (id : string | undefined) => {
|
||||
const { data } = await axios.get<GetDocResponse>(`${API_ENDPOINTS.branch.getDoc}/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
// composable
|
||||
|
||||
const useGetDoc = (id: ComputedRef<string | undefined>) => {
|
||||
|
||||
const enabled = computed(() => {
|
||||
return !!id.value
|
||||
});
|
||||
|
||||
return useQuery({
|
||||
enabled,
|
||||
queryKey: [QUERY_KEYS.document, id],
|
||||
queryFn: () => handleGetDoc(id.value)
|
||||
});
|
||||
};
|
||||
|
||||
export default useGetDoc;
|
||||
@@ -0,0 +1,32 @@
|
||||
// imports
|
||||
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import axios from "~/configs/axios.config";
|
||||
import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type MoveDocRequest = {
|
||||
itemsToMove: number[] | string[],
|
||||
parent: number | string
|
||||
};
|
||||
|
||||
// methods
|
||||
|
||||
export const handleMoveDoc = async ({ itemsToMove, parent }: MoveDocRequest) => {
|
||||
const apiUrl = `${API_ENDPOINTS.branch.moveDoc}?new_parent_id=${parent}&${itemsToMove.map(i => `patch_list=${i}&`)}`
|
||||
const splittedUrl = apiUrl.split("");
|
||||
splittedUrl.pop()
|
||||
|
||||
await axios.patch<MoveDocRequest>(splittedUrl.join("").replaceAll(",", ""));
|
||||
};
|
||||
|
||||
// composable
|
||||
|
||||
const useMoveDoc = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: MoveDocRequest) => handleMoveDoc(variables)
|
||||
});
|
||||
};
|
||||
|
||||
export default useMoveDoc;
|
||||
@@ -0,0 +1,40 @@
|
||||
// imports
|
||||
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import axios from "~/configs/axios.config";
|
||||
import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type ReplyDocRequest = {
|
||||
user_id: number;
|
||||
message: string;
|
||||
reply_id: number;
|
||||
};
|
||||
|
||||
export type ReplyDocResponse = {
|
||||
chat_id: number;
|
||||
};
|
||||
|
||||
// methods
|
||||
|
||||
export const handleReplyDoc = async ({ user_id, message, reply_id }: ReplyDocRequest) => {
|
||||
const payload = {
|
||||
user_id,
|
||||
message,
|
||||
item_id: reply_id,
|
||||
};
|
||||
|
||||
const { data } = await axios.post<ReplyDocResponse>(API_ENDPOINTS.branch.replyDoc, payload);
|
||||
return data;
|
||||
};
|
||||
|
||||
// composable
|
||||
|
||||
const useReplyDoc = () => {
|
||||
return useMutation({
|
||||
mutationFn: (data: ReplyDocRequest) => handleReplyDoc(data),
|
||||
});
|
||||
};
|
||||
|
||||
export default useReplyDoc;
|
||||
@@ -0,0 +1,78 @@
|
||||
// imports
|
||||
|
||||
import { useInfiniteQuery } from "@tanstack/vue-query";
|
||||
import axios from "~/configs/axios.config";
|
||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
import type { ComputedRef } from "vue";
|
||||
|
||||
// types
|
||||
|
||||
export type SearchFileResponse = Branch;
|
||||
|
||||
type HandlerProps = typeof initialPageParam & {
|
||||
signal: AbortSignal,
|
||||
search: string,
|
||||
id: number | undefined;
|
||||
sort: string | undefined;
|
||||
}
|
||||
|
||||
// state
|
||||
|
||||
const initialPageParam = {
|
||||
limit: 10,
|
||||
offset: 0
|
||||
};
|
||||
|
||||
// methods
|
||||
|
||||
export const handleSearchFile = async ({ search, offset, limit, id, signal, sort }: HandlerProps) => {
|
||||
const { data } = await axios.get<SearchFileResponse>(`${API_ENDPOINTS.branch.get}/${id}`, {
|
||||
params: {
|
||||
offset,
|
||||
limit,
|
||||
search,
|
||||
sort_by: sort
|
||||
},
|
||||
signal
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
// composable
|
||||
|
||||
const useSearchFile = (search: Ref<string>, id: Ref<number | undefined>, sort: ComputedRef<string | undefined>) => {
|
||||
|
||||
const enabled = computed(() => {
|
||||
return search.value.trim() != "" && !!id.value;
|
||||
});
|
||||
|
||||
return useInfiniteQuery({
|
||||
enabled,
|
||||
retry: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
queryKey: [QUERY_KEYS.searchFile, search, id, sort],
|
||||
queryFn: ({ pageParam, signal }) => handleSearchFile({
|
||||
...pageParam,
|
||||
signal,
|
||||
search: search.value,
|
||||
sort: sort.value,
|
||||
id: id.value
|
||||
}),
|
||||
initialPageParam,
|
||||
getNextPageParam: (lastPage, pages) => {
|
||||
const page = pages.length + 1;
|
||||
|
||||
const limit = initialPageParam.limit;
|
||||
|
||||
const nextPageParams = {
|
||||
offset: page * limit - limit,
|
||||
limit
|
||||
};
|
||||
|
||||
return lastPage?.structure.next ? nextPageParams : undefined;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default useSearchFile;
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { FastAverageColorResult } from "fast-average-color";
|
||||
import { FastAverageColor } from "fast-average-color";
|
||||
|
||||
export const useImageColor = (img: string) => {
|
||||
const fac = new FastAverageColor();
|
||||
const colorObject = ref<FastAverageColorResult>();
|
||||
const isPending = ref(false);
|
||||
|
||||
const extractImageColor = async () => {
|
||||
isPending.value = true;
|
||||
|
||||
const imageEl = document.querySelector(img) as HTMLImageElement;
|
||||
|
||||
try {
|
||||
const color = await fac.getColorAsync(imageEl);
|
||||
isPending.value = false;
|
||||
colorObject.value = color;
|
||||
} catch (e) {
|
||||
isPending.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
extractImageColor();
|
||||
});
|
||||
|
||||
return {
|
||||
colorObject,
|
||||
extractImageColor,
|
||||
isPending
|
||||
};
|
||||
};
|
||||
@@ -1,23 +1,22 @@
|
||||
export type ToastOptions = {
|
||||
description?: string;
|
||||
duration?: number;
|
||||
status?: "success" | "error" | "info" | "warning",
|
||||
}
|
||||
|
||||
type Toast = {
|
||||
id: number;
|
||||
message: string;
|
||||
description?: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
message: string,
|
||||
description?: string,
|
||||
options?: Omit<Toast, "id" | "message">
|
||||
options?: ToastOptions
|
||||
}
|
||||
|
||||
const toasts = ref<Toast[]>([]);
|
||||
|
||||
export function useToast() {
|
||||
const addToast = ({ message, description, options = {} }: Props) => {
|
||||
const addToast = ({ message, options = {} }: Omit<Toast, "id">) => {
|
||||
const id = Date.now();
|
||||
|
||||
toasts.value.push({ id, message, description, ...options });
|
||||
toasts.value.push({ id, message, options });
|
||||
};
|
||||
|
||||
const destroyToast = (id: number) => {
|
||||
|
||||
+16
-80
@@ -1,90 +1,26 @@
|
||||
export const API_ENDPOINTS = {
|
||||
account : {
|
||||
send_otp : "/accounts/send_otp",
|
||||
account: {
|
||||
send_otp: "/accounts/send_otp",
|
||||
},
|
||||
auth: {
|
||||
signin: "/token",
|
||||
logout: "/accounts/logout",
|
||||
}
|
||||
},
|
||||
chat: {
|
||||
messages: "/chat/product",
|
||||
new_message: "/chat/product",
|
||||
},
|
||||
};
|
||||
|
||||
export const QUERY_KEYS = {
|
||||
|
||||
chat: "chat",
|
||||
};
|
||||
|
||||
export const FILE_FORMATS = [
|
||||
{
|
||||
ext: [".jpg", ".jpeg", ".png", ".svg", ".bmp", ".webp", ".gif", ".tiff", ".heif", ".raw"],
|
||||
icon: "bi:file-earmark-image",
|
||||
},
|
||||
{
|
||||
ext: [".mp4", ".mkv", ".avi", ".mov", ".wmv", ".flv", ".webm", ".mpeg", ".3gp", ".mts"],
|
||||
icon: "bi:file-earmark-play",
|
||||
},
|
||||
{
|
||||
ext: [".zip", ".rar", ".tar.gz", ".dmg", ".7z", ".bz2", ".tar", ".gz", ".xz"],
|
||||
icon: "bi:file-earmark-zip",
|
||||
},
|
||||
{
|
||||
ext: [".mp3", ".wav", ".aac", ".flac", ".ogg", ".wma", ".alac", ".m4a", ".opus", ".aiff"],
|
||||
icon: "bi:file-earmark-music",
|
||||
},
|
||||
{
|
||||
ext: [
|
||||
".py",
|
||||
".js",
|
||||
".java",
|
||||
".cpp",
|
||||
".c",
|
||||
".html",
|
||||
".css",
|
||||
".php",
|
||||
".rb",
|
||||
".swift",
|
||||
".go",
|
||||
".sh",
|
||||
".md",
|
||||
".rust",
|
||||
".pl",
|
||||
],
|
||||
icon: "bi:file-earmark-code",
|
||||
},
|
||||
{
|
||||
ext: [
|
||||
".json",
|
||||
".xml",
|
||||
".sql",
|
||||
".csv",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".avro",
|
||||
".hdf5",
|
||||
".yaml",
|
||||
".tsv",
|
||||
".ods",
|
||||
".db",
|
||||
".sqlite",
|
||||
],
|
||||
icon: "bi:file-earmark-spreadsheet",
|
||||
},
|
||||
{
|
||||
ext: [".pdf", ".epub", ".xps", ".djvu", ".cbz", ".cbt"],
|
||||
icon: "bi:file-earmark-pdf",
|
||||
},
|
||||
{
|
||||
ext: [".ppt", ".pptx", ".odp", ".pps", ".ppsx", ".key"],
|
||||
icon: "bi:file-earmark-slides",
|
||||
},
|
||||
{
|
||||
ext: [".txt", ".doc", ".docx", ".odt", ".rtf", ".wps", ".wpd"],
|
||||
icon: "bi:file-earmark-text",
|
||||
},
|
||||
{
|
||||
ext: [".lock", ".lck"],
|
||||
icon: "bi:file-earmark-lock",
|
||||
},
|
||||
{
|
||||
ext: [".exe", ".bin", ".elf", ".dll", ".iso", ".dmg", ".swf", ".o", ".obg", ".pe", ".app"],
|
||||
icon: "bi:file-earmark-binary",
|
||||
},
|
||||
];
|
||||
export const MUTATION_KEYS = {
|
||||
create_chat: "create_chat",
|
||||
};
|
||||
|
||||
export const PRODUCT_RANGE = {
|
||||
min: 0,
|
||||
max: 100000,
|
||||
};
|
||||
|
||||
@@ -43,11 +43,12 @@ export default defineNuxtConfig({
|
||||
],
|
||||
"@nuxt/icon",
|
||||
"reka-ui/nuxt",
|
||||
"@vueuse/nuxt",
|
||||
],
|
||||
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
API_BASE_URL: "http://38.60.202.91:8000",
|
||||
API_BASE_URL: "http://38.60.202.91:8001",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Generated
+150
-45
@@ -13,7 +13,9 @@
|
||||
"@tanstack/vue-query-devtools": "^5.62.3",
|
||||
"@vuelidate/core": "^2.0.3",
|
||||
"@vuelidate/validators": "^2.0.4",
|
||||
"@vueuse/nuxt": "^12.3.0",
|
||||
"axios": "^1.7.9",
|
||||
"fast-average-color": "^9.4.0",
|
||||
"gsap": "^3.12.5",
|
||||
"nuxt": "^3.14.1592",
|
||||
"reka-ui": "^1.0.0-alpha.6",
|
||||
@@ -3399,14 +3401,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz",
|
||||
"integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==",
|
||||
"version": "12.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.3.0.tgz",
|
||||
"integrity": "sha512-cnV8QDKZrsyKC7tWjPbeEUz2cD9sa9faxF2YkR8QqNwfofgbOhmfIgvSYmkp+ttSvfOw4E6hLcQx15mRPr0yBA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.20",
|
||||
"@vueuse/metadata": "12.0.0",
|
||||
"@vueuse/shared": "12.0.0",
|
||||
"@vueuse/metadata": "12.3.0",
|
||||
"@vueuse/shared": "12.3.0",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"funding": {
|
||||
@@ -3414,18 +3416,98 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.0.0.tgz",
|
||||
"integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==",
|
||||
"version": "12.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.3.0.tgz",
|
||||
"integrity": "sha512-M/iQHHjMffOv2npsw2ihlUx1CTiBwPEgb7DzByLq7zpg1+Ke8r7s9p5ybUWc5OIeGewtpY4Xy0R2cKqFqM8hFg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/nuxt": {
|
||||
"version": "12.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/nuxt/-/nuxt-12.3.0.tgz",
|
||||
"integrity": "sha512-xlpYIroHvdrwyZRHvXKekdFLH0IIPaOLKkt8VcVFZLHC3CgPiiwqGDK2Uvea8Hb51zhTWodYpd5Y2jCcsjZmBw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nuxt/kit": "^3.15.0",
|
||||
"@vueuse/core": "12.3.0",
|
||||
"@vueuse/metadata": "12.3.0",
|
||||
"local-pkg": "^0.5.1",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"nuxt": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/nuxt/node_modules/@nuxt/kit": {
|
||||
"version": "3.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.15.1.tgz",
|
||||
"integrity": "sha512-7cVWjzfz3L6CsZrg6ppDZa7zGrZxCSfZjEQDIvVFn4mFKtJlK9k2izf5EewL6luzWwIQojkZAC3iq/1wtgI0Xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nuxt/schema": "3.15.1",
|
||||
"c12": "^2.0.1",
|
||||
"consola": "^3.3.3",
|
||||
"defu": "^6.1.4",
|
||||
"destr": "^2.0.3",
|
||||
"globby": "^14.0.2",
|
||||
"ignore": "^7.0.0",
|
||||
"jiti": "^2.4.2",
|
||||
"klona": "^2.0.6",
|
||||
"knitwork": "^1.2.0",
|
||||
"mlly": "^1.7.3",
|
||||
"ohash": "^1.1.4",
|
||||
"pathe": "^2.0.0",
|
||||
"pkg-types": "^1.3.0",
|
||||
"scule": "^1.3.0",
|
||||
"semver": "^7.6.3",
|
||||
"ufo": "^1.5.4",
|
||||
"unctx": "^2.4.1",
|
||||
"unimport": "^3.14.5",
|
||||
"untyped": "^1.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/nuxt/node_modules/@nuxt/schema": {
|
||||
"version": "3.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-3.15.1.tgz",
|
||||
"integrity": "sha512-n5kOHt8uUyUM9z4Wu/8tIZkBYh3KTCGvyruG6oD9bfeT4OaS21+X3M7XsTXFMe+eYBZA70IFFlWn1JJZIPsKeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"consola": "^3.3.3",
|
||||
"defu": "^6.1.4",
|
||||
"pathe": "^2.0.0",
|
||||
"std-env": "^3.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/nuxt/node_modules/ignore": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.0.tgz",
|
||||
"integrity": "sha512-lcX8PNQygAa22u/0BysEY8VhaFRzlOkvdlKczDPnJvrkJD1EuqzEky5VYYKM2iySIuaVIDv9N190DfSreSLw2A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/nuxt/node_modules/pathe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.0.tgz",
|
||||
"integrity": "sha512-G7n4uhtk9qJt2hlD+UFfsIGY854wpF+zs2bUbQ3CQEUTcn7v25LRsrmurOxTo4bJgjE4qkyshd9ldsEuY9M6xg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.0.0.tgz",
|
||||
"integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==",
|
||||
"version": "12.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.3.0.tgz",
|
||||
"integrity": "sha512-X3YD35GUeW0d5Gajcwv9jdLAJTV2Jdb/Ll6Ii2JIYcKLYZqv5wxyLeKtiQkqWmHg3v0J0ZWjDUMVOw2E7RCXfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13"
|
||||
@@ -4372,9 +4454,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/consola": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz",
|
||||
"integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/consola/-/consola-3.3.3.tgz",
|
||||
"integrity": "sha512-Qil5KwghMzlqd51UXM0b6fyaGHtOC22scxrwrz4A2882LyUMwQjnvaedN1HAeXzphspQ6CpHkzMAWxBTUruDLg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.10.0"
|
||||
@@ -5162,6 +5244,15 @@
|
||||
"ufo": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-average-color": {
|
||||
"version": "9.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-average-color/-/fast-average-color-9.4.0.tgz",
|
||||
"integrity": "sha512-bvM8vV6YwK07dPbzFz77zJaBcfF6ABVfgNwaxVgXc2G+o0e/tzLCF9WU8Ryp1r0Nkk6JuJNsWCzbb4cLOMlB+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -6107,9 +6198,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.1.tgz",
|
||||
"integrity": "sha512-yPBThwecp1wS9DmoA4x4KR2h3QoslacnDR8ypuFM962kI4/456Iy1oHx2RAgh4jfZNdn0bctsdadceiBUgpU1g==",
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
||||
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
@@ -6203,9 +6294,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/knitwork": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/knitwork/-/knitwork-1.1.0.tgz",
|
||||
"integrity": "sha512-oHnmiBUVHz1V+URE77PNot2lv3QiYU2zQf1JjOVkMt3YDKGbu8NAFr+c4mcNOhdsGrB/VpVbRwPwhiXrPhxQbw==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/knitwork/-/knitwork-1.2.0.tgz",
|
||||
"integrity": "sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/kolorist": {
|
||||
@@ -6622,9 +6713,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.14",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz",
|
||||
"integrity": "sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==",
|
||||
"version": "0.30.17",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
@@ -7655,13 +7746,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.1.tgz",
|
||||
"integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.0.tgz",
|
||||
"integrity": "sha512-kS7yWjVFCkIw9hqdJBoMxDdzEngmkr5FXeWZZfQ6GoYacjVnsW6l2CcYW/0ThD0vF4LPJgVYnrg4d0uuhwYQbg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"confbox": "^0.1.8",
|
||||
"mlly": "^1.7.2",
|
||||
"mlly": "^1.7.3",
|
||||
"pathe": "^1.1.2"
|
||||
}
|
||||
},
|
||||
@@ -9270,15 +9361,28 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unctx": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/unctx/-/unctx-2.3.1.tgz",
|
||||
"integrity": "sha512-PhKke8ZYauiqh3FEMVNm7ljvzQiph0Mt3GBRve03IJm7ukfaON2OBK795tLwhbyfzknuRRkW0+Ze+CQUmzOZ+A==",
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/unctx/-/unctx-2.4.1.tgz",
|
||||
"integrity": "sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.8.2",
|
||||
"acorn": "^8.14.0",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.0",
|
||||
"unplugin": "^1.3.1"
|
||||
"magic-string": "^0.30.17",
|
||||
"unplugin": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unctx/node_modules/unplugin": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.1.2.tgz",
|
||||
"integrity": "sha512-Q3LU0e4zxKfRko1wMV2HmP8lB9KWislY7hxXpxd+lGx0PRInE4vhMBVEZwpdVYHvtqzhSrzuIfErsob6bQfCzw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.14.0",
|
||||
"webpack-virtual-modules": "^0.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
@@ -9340,15 +9444,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/unimport": {
|
||||
"version": "3.14.3",
|
||||
"resolved": "https://registry.npmjs.org/unimport/-/unimport-3.14.3.tgz",
|
||||
"integrity": "sha512-yEJps4GW7jBdoQlxEV0ElBCJsJmH8FdZtk4oog0y++8hgLh0dGnDpE4oaTc0Lfx4N5rRJiGFUWHrBqC8CyUBmQ==",
|
||||
"version": "3.14.5",
|
||||
"resolved": "https://registry.npmjs.org/unimport/-/unimport-3.14.5.tgz",
|
||||
"integrity": "sha512-tn890SwFFZxqaJSKQPPd+yygfKSATbM8BZWW1aCR2TJBTs1SDrmLamBueaFtYsGjHtQaRgqEbQflOjN2iW12gA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.1.3",
|
||||
"acorn": "^8.14.0",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"estree-walker": "^3.0.3",
|
||||
"fast-glob": "^3.3.2",
|
||||
"local-pkg": "^0.5.1",
|
||||
"magic-string": "^0.30.14",
|
||||
"mlly": "^1.7.3",
|
||||
@@ -9357,7 +9462,6 @@
|
||||
"pkg-types": "^1.2.1",
|
||||
"scule": "^1.3.0",
|
||||
"strip-literal": "^2.1.1",
|
||||
"tinyglobby": "^0.2.10",
|
||||
"unplugin": "^1.16.0"
|
||||
}
|
||||
},
|
||||
@@ -9617,17 +9721,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/untyped": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/untyped/-/untyped-1.5.1.tgz",
|
||||
"integrity": "sha512-reBOnkJBFfBZ8pCKaeHgfZLcehXtM6UTxc+vqs1JvCps0c4amLNp3fhdGBZwYp+VLyoY9n3X5KOP7lCyWBUX9A==",
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/untyped/-/untyped-1.5.2.tgz",
|
||||
"integrity": "sha512-eL/8PlhLcMmlMDtNPKhyyz9kEBDS3Uk4yMu/ewlkT2WFbtzScjHWPJLdQLmaGPUKjXzwe9MumOtOgc4Fro96Kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.25.7",
|
||||
"@babel/standalone": "^7.25.7",
|
||||
"@babel/types": "^7.25.7",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/standalone": "^7.26.4",
|
||||
"@babel/types": "^7.26.3",
|
||||
"citty": "^0.1.6",
|
||||
"defu": "^6.1.4",
|
||||
"jiti": "^2.3.1",
|
||||
"mri": "^1.2.0",
|
||||
"jiti": "^2.4.1",
|
||||
"knitwork": "^1.2.0",
|
||||
"scule": "^1.3.0"
|
||||
},
|
||||
"bin": {
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
"@tanstack/vue-query-devtools": "^5.62.3",
|
||||
"@vuelidate/core": "^2.0.3",
|
||||
"@vuelidate/validators": "^2.0.4",
|
||||
"@vueuse/nuxt": "^12.3.0",
|
||||
"axios": "^1.7.9",
|
||||
"fast-average-color": "^9.4.0",
|
||||
"gsap": "^3.12.5",
|
||||
"nuxt": "^3.14.1592",
|
||||
"reka-ui": "^1.0.0-alpha.6",
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="mt-20">
|
||||
<span class="typo-h-3 text-black">دسته بندی ها</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-4 w-full mt-12">
|
||||
<CategoryCard
|
||||
:id="1"
|
||||
category="یک دسته بندی تست"
|
||||
picture="/img/product-1.jpg"
|
||||
:count="20"
|
||||
description="یک دسته بندی تستasdasd"
|
||||
dark-layer
|
||||
/>
|
||||
<CategoryCard
|
||||
:id="2"
|
||||
category="یک دسته بندی تست"
|
||||
picture="/img/product-2.jpg"
|
||||
:count="20"
|
||||
description="یک دسته بندی تستasdasd"
|
||||
/>
|
||||
<CategoryCard
|
||||
:id="3"
|
||||
category="یک دسته بندی تست"
|
||||
picture="/img/product-3.jpg"
|
||||
:count="20"
|
||||
description="یک دسته بندی تستasdasd"
|
||||
/>
|
||||
<CategoryCard
|
||||
:id="8"
|
||||
category="یک دسته بندی تست"
|
||||
picture="/img/product-4.jpg"
|
||||
:count="20"
|
||||
description="یک دسته بندی تستasdasd"
|
||||
/>
|
||||
<CategoryCard
|
||||
:id="4"
|
||||
category="یک دسته بندی تست"
|
||||
picture="/img/product-5.jpg"
|
||||
:count="20"
|
||||
description="یک دسته بندی تستasdasd"
|
||||
/>
|
||||
<CategoryCard
|
||||
:id="5"
|
||||
category="یک دسته بندی تست"
|
||||
picture="/img/product-1.jpg"
|
||||
:count="20"
|
||||
description="یک دسته بندی تستasdasd"
|
||||
/>
|
||||
<CategoryCard
|
||||
:id="6"
|
||||
category="یک دسته بندی تست"
|
||||
picture="/img/product-2.jpg"
|
||||
:count="20"
|
||||
description="یک دسته بندی تستasdasd"
|
||||
/>
|
||||
<CategoryCard
|
||||
:id="7"
|
||||
category="یک دسته بندی تست"
|
||||
picture="/img/product-3.jpg"
|
||||
:count="20"
|
||||
description="یک دسته بندی تستasdasd"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts" setup></script>
|
||||
<script lang="ts" setup>
|
||||
import ChatButton from "~/components/product/ChatBox/ChatButton.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full flex flex-col">
|
||||
@@ -7,5 +9,6 @@
|
||||
<ProductComments />
|
||||
<ProductDetails />
|
||||
<RelatedProducts title="محصولات مشابه" />
|
||||
<ChatButton />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,20 +1,46 @@
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
// import
|
||||
|
||||
import { PRODUCT_RANGE } from "~/constants";
|
||||
|
||||
// state
|
||||
|
||||
const params = useUrlSearchParams("history");
|
||||
|
||||
// life-cycle
|
||||
|
||||
onMounted(() => {
|
||||
if (!("range" in params)) {
|
||||
params.range = [];
|
||||
params.range[0] = PRODUCT_RANGE.min;
|
||||
params.range[1] = PRODUCT_RANGE.max;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full container flex flex-col">
|
||||
<div class="w-full flex justify-between items-center py-[5rem]">
|
||||
<div class="flex flex-col items-start gap-[1.5rem] text-black">
|
||||
<div class="flex-center gap-[.75rem]">
|
||||
<div class="w-full flex justify-end items-center py-[5rem]">
|
||||
<div
|
||||
class="flex flex-col items-start gap-[1.5rem] text-black w-full"
|
||||
>
|
||||
<!-- <div class="flex-center gap-[.75rem]">
|
||||
<span>خانه</span>
|
||||
<span>/</span>
|
||||
<span>محصولات</span>
|
||||
<span>/</span>
|
||||
<span>همه</span>
|
||||
</div>
|
||||
</div> -->
|
||||
<h1 class="typo-hero-2">همه محصولات</h1>
|
||||
</div>
|
||||
|
||||
<FilterButton />
|
||||
<div class="w-full flex items-center justify-end gap-4">
|
||||
<Input
|
||||
placeholder="جست و جو محصول ..."
|
||||
class="bg-slate-50 !border-slate-200 hover:border-slate-300 focus:!border-slate-800 !rounded-full w-8/12"
|
||||
/>
|
||||
<FilterButton />
|
||||
</div>
|
||||
</div>
|
||||
<ul class="w-full grid grid-cols-3 gap-[1.5rem]">
|
||||
<li v-for="i in 9" :key="i">
|
||||
|
||||
@@ -57,12 +57,22 @@ const sendOtpHandler = async () => {
|
||||
phone: `0${loginInfo.value.phone}`
|
||||
});
|
||||
|
||||
addToast({ message: "کد برای شما ارسال شد" });
|
||||
addToast({
|
||||
message: "کد برای شما ارسال شد",
|
||||
options: {
|
||||
status: "success"
|
||||
}
|
||||
});
|
||||
|
||||
showOtp.value = true;
|
||||
|
||||
} catch (e) {
|
||||
addToast({ message: "مشکلی پیش آمده" });
|
||||
addToast({
|
||||
message: "مشکلی پیش آمده",
|
||||
options: {
|
||||
status: "error"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { gsap } from 'gsap'
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||
import { ScrollToPlugin } from 'gsap/ScrollToPlugin'
|
||||
import { TextPlugin } from "gsap/TextPlugin";
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
if (process.client) {
|
||||
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin)
|
||||
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin, TextPlugin)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 6H19M21 12H16M21 18H16M7 20V13.5612C7 13.3532 7 13.2492 6.97958 13.1497C6.96147 13.0615 6.93151 12.9761 6.89052 12.8958C6.84431 12.8054 6.77934 12.7242 6.64939 12.5617L3.35061 8.43826C3.22066 8.27583 3.15569 8.19461 3.10948 8.10417C3.06849 8.02393 3.03853 7.93852 3.02042 7.85026C3 7.75078 3 7.64677 3 7.43875V5.6C3 5.03995 3 4.75992 3.10899 4.54601C3.20487 4.35785 3.35785 4.20487 3.54601 4.10899C3.75992 4 4.03995 4 4.6 4H13.4C13.9601 4 14.2401 4 14.454 4.10899C14.6422 4.20487 14.7951 4.35785 14.891 4.54601C15 4.75992 15 5.03995 15 5.6V7.43875C15 7.64677 15 7.75078 14.9796 7.85026C14.9615 7.93852 14.9315 8.02393 14.8905 8.10417C14.8443 8.19461 14.7793 8.27583 14.6494 8.43826L11.3506 12.5617C11.2207 12.7242 11.1557 12.8054 11.1095 12.8958C11.0685 12.9761 11.0385 13.0615 11.0204 13.1497C11 13.2492 11 13.3532 11 13.5612V17L7 20Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.6 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 31 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 421 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 648 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
Vendored
+7
@@ -7,4 +7,11 @@ declare global {
|
||||
previous: string | null;
|
||||
results: T[];
|
||||
};
|
||||
|
||||
type Chat = {
|
||||
id: number,
|
||||
sender: "ai" | "user",
|
||||
content: string
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user