hadith_ui/components/haditha/AutoComplation.vue
2025-04-12 07:04:15 +03:30

934 lines
24 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 { useStorage } from "@vueuse/core";
import * as z from "zod";
import routeGlobal from "~/middleware/route.global";
// import type { FormSubmitEvent } from "@nuxt/ui";
export type Synonym = {
title: string;
enable: boolean;
enableForm: boolean;
subTitles: [
{
title: string;
active: boolean;
}
];
};
// #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", "");
// when comming from main page search.
const route = useRoute();
if (route.query.q) {
searchTerm.value = route.query.q;
route.query.q = undefined
}
const open = ref(false);
const typeDropdownOpen = ref(false);
const loading = ref(false);
const httpService = useNuxtApp()["$http"];
const search_type = ref("normal");
const type_key = ref("hadith");
const typeModelValue = ref("normal");
const typeModelValueFa = ref("");
// 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: "جستجو در همه",
value: "normal",
class: "",
onSelect(e: Event) {
console.info(e);
search_type.value = "normal";
state.type.label = e.explicitOriginalTarget?.innerText;
sendQuery();
state.type.items[0].class = "active";
},
},
{
label: "فقط در متن عربی حدیث",
value: "arabic",
valueFa: "عربی",
class: "",
onSelect(e: Event) {
search_type.value = "arabic";
state.type.label = e.explicitOriginalTarget?.innerText;
console.info(e);
state.type.items[0].class = "active";
sendQuery();
},
},
{
label: "فقط در ترجمه ها",
value: "translations",
valueFa: "ترجمه",
onSelect(e: Event) {
search_type.value = "translations";
state.type.label = e.explicitOriginalTarget?.innerText;
console.info(e);
state.type.items[0].class = "active";
sendQuery();
},
},
{
label: "فقط در شروح",
value: "descriptions",
valueFa: "شروح",
onSelect(e: Event) {
search_type.value = "descriptions";
state.type.label = e.explicitOriginalTarget?.innerText;
state.type.items[0].class = "active";
console.info(e);
sendQuery();
},
},
],
},
synonym: {
value: "synonym",
label: "مترادف",
icon: "i-haditha-chevron-down",
items: [],
},
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
// #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 (payload = {}) => {
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");
// اگر نوع انتخاب شود.
const isTypeSelected =
typeModelValue.value == "arabic" ||
typeModelValue.value == "translations" ||
typeModelValue.value == "descriptions";
url = url.replace(
"@q=none",
searchTerm.value.length
? `q=${isTypeSelected ? "#" + typeModelValueFa.value + " " : ""}${
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, payload)
.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);
})
.catch((err) => {
console.info(err);
})
.finally(() => {
loading.value = false;
});
};
// ------------------- form -------------------
const schema = z.object({
name: z.string().min(1, "این فیلد ضروری است"),
});
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
const synonymIsSwitchedOn = computed(() => {
return state.synonym.items.filter((i) => i.enable).length;
});
const onClearSynonymClear = () => {
search_type.value = "normal";
state.synonym.items.forEach((element) => {
element.enable = false;
});
};
async function openSynonymPopup(type: string) {
search_type.value = type;
console.info("openSynonymPopup");
getSynonyms().then(() => {
isSynonymPopupOpen.value = true;
});
}
const onTypeSelectChanged = (value: string) => {
console.info(value);
if (value == "translations") typeModelValueFa.value = "ترجمه";
else if (value == "arabic") typeModelValueFa.value = "عربی";
else if (value == "descriptions") typeModelValueFa.value = "شروح";
else typeModelValueFa.value = "همه";
sendQuery();
};
const getSynonyms = async () => {
let url = repoUrl() + hadithaApi.search.synonym;
const payload = {
query: searchTerm.value,
};
// fetch search list from backend(ssr)
return await httpService.postRequest(url, payload).then((res) => {
state.synonym.items = [];
Object.entries(res.data).forEach((item, index) => {
const synonyms = {} as Synonym;
synonyms.title = item[0];
synonyms.enable = false;
synonyms.enableForm = false;
const subTitlesStrList = item[1].value.split(",");
const subTitlesObjList = subTitlesStrList
.filter((i) => i)
.map((i) => {
return {
title: i,
active: false,
};
});
synonyms.subTitles = subTitlesObjList;
state.synonym.items.push(synonyms);
});
});
};
const prepareSynonym = () => {
const enabledSwitches = state.synonym.items.filter((i) => i.enable);
const res = {};
console.info(enabledSwitches);
enabledSwitches.forEach((item) => {
console.info(item);
if (item.subTitles.length)
res[item.title] = item.subTitles
.filter((i) => i.active)
.map((i) => i.title)
.join(",");
});
console.info(res);
return {
synonym: res,
};
};
const onUpdateSwitch = () => {
sendQuery(prepareSynonym());
};
const onUpdateSubTitle = (subTitle) => {
subTitle.active = !subTitle.active;
sendQuery(prepareSynonym());
};
const onAddNewTitle = (subTitles) => {
subTitles.push({
active: true,
title: Formstate.name,
});
Formstate.name = "";
sendQuery(prepareSynonym());
};
// #endregion methods
onMounted(() => {
if (searchTerm.value.length) sendQuery();
});
</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">
<!-- #region معنایی -->
<!-- @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="setType('normal')"
name="i-haditha-close-bg-circle"
size="20px"
>
</UIcon>
</UButton>
<!-- #endregion -->
<!-- #region مترادف -->
<UPopover
:content="{
align: 'start',
side: 'bottom',
sideOffset: 8,
}"
:ui="{
content: 'popover-root-content',
}"
v-model:open="isSynonymPopupOpen"
>
<UButton
@click="openSynonymPopup('synonym')"
:class="{ active: search_type == 'synonym' }"
class="filter-item"
type="button"
trailingIcon="i-haditha-dropdown-chevron-down"
>
{{ state.synonym.label }}
<UIcon
v-if="synonymIsSwitchedOn"
@click.self="onClearSynonymClear"
name="i-haditha-close-bg-circle"
size="20px"
>
</UIcon>
</UButton>
<template #content>
<!-- synonym item -->
<template v-for="(syn, itemIndex) in state.synonym.items">
<div class="synonymItem px-2 py-4">
<div class="flex justify-between items-center p-3 mb-2">
<span class="title"> {{ syn.title }} </span>
<USwitch
dir="ltr"
v-model="syn.enable"
@update:modelValue="
onUpdateSwitch(syn.subTitles, itemIndex)
"
/>
</div>
<div class="flex items-center px-2 flex-wrap">
<UButton
v-for="(sub, subIndex) in syn.subTitles"
:disabled="!syn.enable"
:key="subIndex"
type="button"
@click="onUpdateSubTitle(sub, itemIndex, subIndex)"
class="me-2.5 mb-3.5 promotion-item"
:class="{ active: sub.active }"
>
{{ sub.title }}
<!-- <UIcon name="i-haditha-close-bg-circle" size="12px"> </UIcon> -->
</UButton>
<UButton
v-if="syn.enable && !syn.enableForm"
type="button"
@click="syn.enableForm = true"
class="me-2.5 mb-3.5 add-button"
:class="{ active: syn.enable }"
icon="i-haditha-add"
>
</UButton>
<UForm
v-if="syn.enable && syn.enableForm"
:schema="schema"
:state="state"
class="w-25 me-2.5 mb-3.5"
>
<UFormField name="name" size="md">
<UInput
v-model="Formstate.name"
placeholder="بنویسید ..."
@keyup.enter="onAddNewTitle(syn.subTitles)"
>
<template v-if="syn.enableForm" #trailing>
<UButton
color=""
variant=""
size="sm"
icon="i-lucide-x"
aria-label="Clear input"
@click="syn.enableForm = false"
/>
</template>
</UInput>
</UFormField>
</UForm>
</div>
</div>
<USeparator class="px-2" color="neutral" type="solid" size="xs" />
</template>
</template>
</UPopover>
<!-- #endregion -->
<!-- #region ترجمه -->
<!-- <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> -->
<!-- #endregion -->
<!-- #region عین عبارت -->
<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>
<!-- #endregion -->
</div>
<!-- #region نوع -->
<div>
<USelect
:items="state.type.items"
:content="{
align: 'start',
side: 'bottom',
sideOffset: 8,
}"
:ui="{
content: 'w-48',
}"
class="filter-item"
:class="{ active: state.type.value == 'hadith' }"
v-model="typeModelValue"
trailing-icon=""
value-key="value"
@update:modelValue="onTypeSelectChanged"
>
<!-- <UButton
class="filter-item"
:label="state.type.label"
trailingIcon="i-haditha-dropdown-chevron-down"
:class="{
active:
search_type == 'arabic' ||
search_type == 'translations' ||
search_type == 'descriptions',
}"
>
{{ state.type.label }}
<UIcon
v-if="
search_type == 'arabic' ||
search_type == 'translations' ||
search_type == 'descriptions'
"
@click.self="closeTypeDropdown"
name="i-haditha-close-bg-circle"
size="20px"
>
</UIcon>
</UButton>-->
<!-- <template #item> item </template> -->
<!-- <template #item-label> item label </template> -->
</USelect>
</div>
<!-- #endregion -->
</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);
max-width: 626px;
width:100%;
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 {
overflow-y: auto;
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;
}
&[disabled="true"] {
filter: grayscale(0.7);
}
}
.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>