haditha_ui/components/haditha/AutoComplation.vue
2025-03-18 16:02:10 +03:30

782 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import type { InputMenuItem } from "@nuxt/ui";
import hadithaApi from "../../apis/hadithaApi";
import type { HadithResponseModel } from "../../types/hadithType";
import { useStorage } from "@vueuse/core";
import * as z from "zod";
import type { FormSubmitEvent } from "@nuxt/ui";
// #region props
const props = defineProps({
showFilter: {
default: false,
},
showPrevSearch: {
default: false,
},
});
// #endregion props
// #region emits
const emit = defineEmits(["response-ready"]);
// #endregion emits
// #region refs
const userSearchHistory = useStorage(
"userSearchHistory",
new Set() // Initial value
);
const searchTerm = useStorage<string>("searchPhrase", "");
const open = ref(false);
const loading = ref(false);
const httpService = useNuxtApp()["$http"];
const search_type = ref("normal");
const type_key = ref("hadith");
const synonymOne = ref(false);
// If you want to share state across multiple components,
// you can use the same key in useState. Nuxt will ensure
// that the state is shared and reactive across your application.
// const typingTimer = useState<number>("typingTimer", () => 0);
// const doneTypingInterval = useState<number>("doneTypingInterval", () => 1000);
const typingTimer = ref<number | any>(0);
const doneTypingInterval = ref<number>(1000);
// #endregion refs
// #region reactive
const state = reactive({
list: [],
vector: {
label: "معنایی",
value: "vector",
icon: "i-haditha-robot-indicator",
},
type: {
icon: "",
value: "normal",
label: "نوع",
items: [
{
label: "جستجو در همه",
onSelect(e: Event) {
console.info(e);
search_type.value = "normal";
},
},
{
label: "فقط در متن عربی حدیث",
onSelect(e: Event) {
search_type.value = "arabic";
},
},
{
label: "فقط در ترجمه ها",
onSelect(e: Event) {
search_type.value = "translations";
},
},
{
label: "فقط در شروح",
onSelect(e: Event) {
search_type.value = "descriptions";
},
},
],
},
synonym: {
value: "synonym",
label: "مترادف",
icon: "i-haditha-chevron-down",
items: [
{
label: "جستجو در همه",
slot: "arabic",
},
{
label: "فقط در متن عربی حدیث",
slot: "arabic",
},
{
label: "فقط در ترجمه ها",
slot: "arabic",
},
{
label: "فقط در شروح",
slot: "arabic",
},
],
},
type_key: {
label: "ترجمه",
value: "hadith",
},
phrase: {
label: "عین عبارت",
value: "normal",
},
});
// #endregion reactive
// #region watch
// watch(
// () => route,
// (newRoute, oldRoute) => {
// console.info(newRoute.query);
// if (newRoute.query) searchTerm.value = <string>newRoute.query.q;
// },
// { immediate: true }
// );
// #endregion emits
onMounted(() => {
if (searchTerm.value.length) sendQuery();
});
// #region methods
const clearSimilar = () => {
console.info("clearSimilar");
};
const onUpdateModel = (newVal: boolean | InputMenuItem | any) => {
console.info("onUpdateModel", newVal);
};
const onKeyDown = () => {
clearTimeout(typingTimer.value);
};
const onKeyUp = () => {
clearTimeout(typingTimer.value);
typingTimer.value = setTimeout(() => {
sendQuery();
}, doneTypingInterval.value);
};
const setType = (type: string) => {
search_type.value = type;
sendQuery();
};
const setKey = (type: string) => {
type_key.value = type;
sendQuery();
};
const sendQuery = async () => {
if (loading.value) return;
loading.value = true;
let url = hadithaApi.search.list;
url = url.replace("@index_key", "dhparag");
url = url.replace("@search_type", search_type.value); // normal, and , phrase, vector, synonym
url = url.replace("@type_key", type_key.value); // hadith, hadith_fa, hadith_ar, hadith_shr
url = url.replace("@offset", "0");
url = url.replace("@limit", "10");
url = url.replace("@listkey", "normal");
url = url.replace("@field_collapsed", "normal");
url = url.replace(
"@q=none",
searchTerm.value.length ? `q=${searchTerm.value}` : "q=none"
);
// const baseURL =
// config.public.NUXT_PUBLIC_BASE_URL + config.public.NUXT_PUBLIC_API_NAME;
// fetch search list from backend(ssr)
return await httpService
.postRequest(url)
.then((res) => {
// pass res and search query to the parent.
emit("response-ready", {
res: res,
searchQuery: searchTerm.value,
});
loading.value = false;
// check if search term is not empty
if (searchTerm.value) userSearchHistory.value.add(searchTerm.value); // Add the value to the Set
// close the history dropdown menu
open.value = false;
// store search phrase
useStorage("searchPhrase", searchTerm.value);
})
.finally(() => {
loading.value = false;
});
};
// ------------------- form -------------------
type Schema = z.output<typeof schema>;
const toast = useToast();
const schema = z.object({
name: z.string(),
});
const showForm = ref(false);
const isSynonymPopupOpen = ref(false);
const Formstate = reactive({
name: "",
});
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({
title: "Success",
description: "The form has been submitted.",
color: "success",
});
console.log(event.data);
}
// get synonyms
async function openSynonymPopup(type: string) {
search_type.value = type;
console.info("openSynonymPopup");
sendQuery().then(() => {
isSynonymPopupOpen.value = true;
});
}
// #endregion methods
</script>
<template>
<div class="haditha-search-root-wrapper">
<div class="haditha-search-root" :class="{ 'no-backdrop': showPrevSearch }">
<!-- وقتی کاربر در صفحه نمایش بر روی مشابه کلیک میکند و به صفحه جستجو وارد میشود. -->
<div v-if="showPrevSearch" class="prev-search-item flex items-center">
<span class="total">۴۷ مشابه </span>
<span class="text me-auto">
عَنِ الْحَسَنِ بْنِ عَلِيِّ بْنِ يُوسُفَ، عَنْ جَدِّهِ، قَالَ:
</span>
<UButton
icon="i-lucide:x"
color="neutral"
variant="ghost"
class="clear-similar-btn"
@click="clearSimilar"
/>
</div>
<!-- <client-only> -->
<div class="search-input">
<UInputMenu
class="w-full focus:placeholder-gray-800"
:items="<any>Array.from(userSearchHistory)"
v-model="searchTerm"
v-model:open="open"
v-model:search-term="searchTerm"
placeholder="هوشمند جستجو کنید..."
:ui="{
base: 'haditha-search-input',
}"
:content="{
align: 'start',
side: 'bottom',
sideOffset: 4,
}"
:loading="loading"
highlight
highlightOnHover
@focus="open = true"
@blur="open = false"
@change="sendQuery"
@update:modelValue="onUpdateModel"
@update:searchTerm="onUpdateModel"
@keydown="onKeyDown"
@keyup="onKeyUp"
@keydown.enter="sendQuery"
>
</UInputMenu>
</div>
<UButton
class="my-trailing-button"
@click.prevent="sendQuery"
icon="i-haditha-search"
>
<!-- <UIcon name="i-lucide-search" /> -->
</UButton>
<!-- </client-only> -->
</div>
<div
class="search-filter flex items-center my-3 justify-between"
v-if="props.showFilter && searchTerm.length"
>
<div class="flex items-center space-x-2">
<!-- معنایی -->
<!-- @click.self="search_type = 'vector'" -->
<UButton
@click.self="setType('vector')"
:class="{ active: search_type == 'vector' }"
type="button"
class="filter-item"
:icon="state.vector.icon"
>
{{ state.vector.label }}
<UIcon
v-if="search_type == 'vector'"
@click.self="search_type = 'normal'"
name="i-haditha-close-bg-circle"
size="20px"
>
</UIcon>
</UButton>
<!-- مترادف -->
<UPopover
:content="{
align: 'start',
side: 'bottom',
sideOffset: 8,
}"
:ui="{
content: 'popover-root-content',
}"
v-model:open="isSynonymPopupOpen"
>
<UButton
@click="openSynonymPopup('synonym')"
class="filter-item"
:label="state.synonym.label"
trailingIcon="i-haditha-dropdown-chevron-down"
/>
<template #content>
<div class="synonymItem px-2 py-4">
<div class="flex justify-between items-center p-3 mb-2">
<span class="title"> نماز </span>
<USwitch dir="ltr" v-model="synonymOne" />
</div>
<div class="flex items-center px-2">
<UButton
type="button"
@click="true"
class="me-2.5 mb-3.5 promotion-item"
:class="{ active: false }"
>
صلات
<!-- <UIcon name="i-haditha-close-bg-circle" size="12px"> </UIcon> -->
</UButton>
<UButton
type="button"
@click="true"
class="me-2.5 mb-3.5 promotion-item"
:class="{ active: false }"
>
عبادت
<!-- <UIcon name="i-haditha-close-bg-circle" size="12px"> </UIcon> -->
</UButton>
<UButton
type="button"
@click="true"
class="me-2.5 mb-3.5 add-button"
:class="{ active: false }"
icon="i-haditha-add"
>
</UButton>
</div>
</div>
<USeparator class="px-2" color="neutral" type="solid" size="xs" />
<div class="synonymItem px-2 py-4">
<div class="flex justify-between items-center p-3 mb-2">
<span class="title"> نماز </span>
<USwitch dir="ltr" v-model="synonymOne" />
</div>
<div class="flex items-center px-2">
<UButton
type="button"
@click="true"
class="me-2.5 mb-3.5 promotion-item"
:class="{ active: false }"
>
صلات
<!-- <UIcon name="i-haditha-close-bg-circle" size="12px"> </UIcon> -->
</UButton>
<UButton
type="button"
@click="true"
class="me-2.5 mb-3.5 promotion-item"
:class="{ active: false }"
>
عبادت
<!-- <UIcon name="i-haditha-close-bg-circle" size="12px"> </UIcon> -->
</UButton>
<UButton
v-if="!showForm"
type="button"
@click="showForm = true"
class="me-2.5 mb-3.5 add-button"
:class="{ active: false }"
icon="i-haditha-add"
>
</UButton>
<UForm
v-else
:schema="schema"
:state="state"
class="w-25 me-2.5 mb-3.5"
>
<UFormField name="name" required size="md">
<UInput
v-model="Formstate.name"
placeholder="بنویسید ..."
/>
</UFormField>
</UForm>
</div>
</div>
<USeparator class="px-2" color="neutral" type="solid" size="xs" />
</template>
</UPopover>
<!-- ترجمه -->
<UButton
@click.self="setKey('hadith_fa')"
:class="{ active: type_key == 'hadith_fa' }"
type="button"
class="filter-item"
>
{{ state.type_key.label }}
<UIcon
v-if="type_key == 'hadith_fa'"
@click.self="setKey('hadith')"
name="i-haditha-close-bg-circle"
size="20px"
>
</UIcon>
</UButton>
<!-- عین عبارت -->
<UButton
@click.self="setType('phrase')"
:class="{ active: search_type == 'phrase' }"
class="filter-item"
>
{{ state.phrase.label }}
<UIcon
v-if="search_type == 'phrase'"
@click.self="search_type = 'normal'"
name="i-haditha-close-bg-circle"
size="20px"
>
</UIcon>
</UButton>
</div>
<div>
<!-- نوع -->
<UDropdownMenu
:items="state.type.items"
:content="{
align: 'start',
side: 'bottom',
sideOffset: 8,
}"
:ui="{
content: 'w-48',
}"
:class="{ active: state.type.value == 'hadith' }"
>
<UButton
class="filter-item"
:label="state.type.label"
trailingIcon="i-haditha-dropdown-chevron-down"
>
{{ state.type.label }}
<UIcon
v-if="state.type.value != 'normal'"
@click.self="state.type.value = 'normal'"
name="i-haditha-close-bg-circle"
size="20px"
>
</UIcon>
</UButton>
<!-- <template #item> item </template> -->
<!-- <template #item-label> item label </template> -->
</UDropdownMenu>
</div>
</div>
</div>
</template>
<style scoped>
.haditha-search-root-wrapper {
max-width: 656px;
width: 100%;
margin: 0 1em;
.haditha-search-root {
position: relative;
&::before {
content: "";
position: absolute;
left: 1em;
right: 1em;
top: 50%;
backdrop-filter: blur(60px);
background: linear-gradient(137.41deg, #ffffff -42.82%, #e5e0ff 87.9%);
filter: blur(60px);
width: 626px;
height: 68px;
z-index: 0;
}
.prev-search-item {
width: 328;
height: 49;
gap: 6px;
border-radius: 12px;
border-width: 0.5px;
padding-top: 8px;
padding-right: 12px;
padding-bottom: 8px;
padding-left: 12px;
background: #626b84;
border: 0.5px solid;
margin-bottom: 0.7em;
border-image-source: linear-gradient(
102.02deg,
#4be8ae 7.38%,
#00a762 91.78%
);
.total {
width: 53;
height: 24;
gap: 4px;
border-radius: 6px;
padding: 5px 7px;
background: #1b213266;
font-family: IRANSansX;
font-weight: 500;
font-size: 10px;
line-height: 15px;
letter-spacing: 0%;
text-align: right;
color: #ffffff;
}
.text {
font-family: Takrim;
font-weight: 400;
font-size: 16px;
line-height: 32px;
letter-spacing: 0%;
text-align: right;
color: #ffffff;
}
.clear-similar-btn {
width: 32px;
height: 32px;
gap: 4px;
border-radius: 60px;
padding-top: 11px;
padding-right: 6px;
padding-bottom: 11px;
padding-left: 6px;
background: #1b213266;
color: #fff;
}
}
.search-input {
position: relative;
}
}
}
</style>
<style>
.haditha-search-root-wrapper {
.my-trailing-button {
position: absolute;
z-index: 1;
width: 48px;
height: 48px;
justify-content: center;
align-items: center;
padding: 0;
border-radius: 50px;
background: linear-gradient(320.71deg, #b9fde0 6.56%, #e4f9f0 69.69%);
left: 12px;
top: 0;
bottom: 0;
margin: auto;
transition: all 0.2s ease-in-out;
&:hover {
transition: all 0.2s ease-in-out;
background: linear-gradient(320.71deg, #54ecaa 6.56%, #b6f0d9 69.69%);
}
& > span {
/* width: 18px; */
/* height: 18px; */
/* background-image: linear-gradient(
102.02deg,
#4be8ae 7.38%,
#00a762 91.78%
); */
}
}
.haditha-search-input {
z-index: 0;
height: 72px;
justify-content: space-between;
padding-top: 12px;
/* padding-right: 12px; */
padding-bottom: 12px;
padding-left: 12px;
border-radius: 12px;
border-width: 0.3px;
background-color: #fff;
border: 0.3px solid #e0e0e0;
box-shadow: 0px 1px 4px 0px #0000000d;
font-family: IRANSansX;
font-weight: 300;
font-size: 14px;
line-height: 21px;
letter-spacing: 0%;
text-align: right;
color: #a7acbe;
}
.search-filter {
.filter-item {
/* width: 81px; */
height: 40px;
border-radius: 12px;
border-width: 0.3px;
padding-top: 8px;
padding-right: 12px;
padding-bottom: 8px;
padding-left: 12px;
gap: 4px;
background-color: #fff;
border: 0.3px solid #e0e0e0;
box-shadow: 0px 1px 4px 0px #0000000d;
color: #8a92a8;
font-family: IRANSansX;
font-weight: 400;
font-size: 13px;
line-height: 20px;
letter-spacing: 0%;
text-align: right;
&.active {
background-image: linear-gradient(
102.02deg,
#4be8ae 7.38%,
#00a762 91.78%
);
border-color: #4be8ae;
color: #fff;
box-shadow: none;
}
}
}
&.search-page {
.haditha-search-root {
&::before {
content: none;
}
.my-trailing-button {
width: 40px;
height: 40px;
span.iconify {
width: 25px;
height: 25px;
}
}
.haditha-search-input {
height: 56px;
}
}
}
}
.popover-root-content {
width: 20.5em;
height: 17.75em;
/* gap: 8px; */
/* border-radius: 16px; */
border-width: 0.3px;
/* padding-top: 16px; */
/* padding-right: 8px; */
/* padding-bottom: 16px; */
/* padding-left: 8px; */
background: #ffffff;
border: 0.3px solid #e0e0e0;
box-shadow: 0px 8px 20px 0px #0000001a;
.synonymItem {
.title {
font-family: IRANSansX;
font-weight: 400;
font-size: 14px;
line-height: 100%;
letter-spacing: 0%;
text-align: center;
vertical-align: middle;
color: #8a92a8;
}
/* border-bottom: 1px solid #d9d9d9; */
.promotion-item {
width: 52.599998474121094;
height: 36;
gap: 4px;
border-radius: 8px;
border-width: 0.3px;
padding-top: 11px;
padding-right: 12px;
padding-bottom: 11px;
padding-left: 12px;
border: 0.3px solid #d9d9d9;
background: #f0f1f4;
font-family: IRANSansX;
font-weight: 400;
font-size: 12px;
line-height: 100%;
letter-spacing: 0%;
text-align: right;
color: #626b84;
&.active {
background: linear-gradient(320.71deg, #b9fde0 6.56%, #e4f9f0 69.69%);
border: 0.3px solid #29d985;
color: #626b84;
}
}
.add-button {
width: 48;
height: 36;
gap: 4px;
border-radius: 8px;
border-width: 1px;
padding-right: 11px;
padding-left: 11px;
background: #ffffff;
border: 1px solid #f0f1f4;
color: #8a92a8;
}
}
}
</style>