Connecting apis

This commit is contained in:
mustafa-rezae 2025-04-12 07:04:15 +03:30
parent 363d1269f2
commit 4745294ffc
45 changed files with 1277 additions and 676 deletions

View File

@ -2,5 +2,25 @@ export default {
search: {
list: "repo/monir/search/@index_key/@search_type/@type_key/@listkey/@field_collapsed/@offset/@limit/@q=none",
show: "repo/public/get/byid/@index_key/@id",
synonym: "synonym/get/words",
prevNextHadith: "monir/next/@index_key/@vol_id/@parag_order/@step",
},
favorite: {
add: "favorite/add/{{data_type}}/{{ref_key}}",
delete: "favorite/delete/{{data_type}}/{{id}}", //id = portal_meet_22569
deleteByRefid: "favorite/delete/{{data_type}}/{{index_key}}/{{ref_id}}", //id = portal_meet_22569
// getListSearch: "favorite/list/{{data_type}}/{{offset}}/{{limit}}", //offset=0 , limit=10
// counts: "favorite/counts/@data_type", // get
setFavoritesCat: "favorite/tags/@data_type/set/doc/@id",
getCategories: "favorite/tags/@data_type/get",
setCategories: "favorite/tags/@data_type/set",
getCounts: "favorite/tags/@data_type/counts",
getList: "favorite/list/@data_type/@offset/@limit/@filter",
},
library: {
list: "monir/book/volume/@field_collapsed/@offset/@limit/@q",
show: "@appname/book/page/@page_start/@page_end/@vol_id",
},
};

View File

@ -0,0 +1,9 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.7668 1.71576H7.22222C6.10322 1.71849 5.03079 2.1869 4.23953 3.01851C3.44827 3.85013 3.0026 4.97725 3 6.15333L3 20.3516C3 22.4209 4.41105 23.2946 6.13932 22.2945L11.4773 19.179C11.7955 19.0144 12.1454 18.9289 12.5 18.9289C12.8546 18.9289 13.2045 19.0144 13.5227 19.179L18.8607 22.2945C20.5889 23.3061 22 22.4324 22 20.3516V6.15333C21.9931 4.97666 21.5445 3.85043 20.7518 3.01946C19.9592 2.1885 18.8864 1.71998 17.7668 1.71576Z" fill="url(#paint0_linear_67_5429)"/>
<defs>
<linearGradient id="paint0_linear_67_5429" x1="18.5723" y1="34.3787" x2="21.3731" y2="3.44045" gradientUnits="userSpaceOnUse">
<stop stop-color="#4BE8AE"/>
<stop offset="1" stop-color="#00A762"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 809 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 490 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 783 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -3,8 +3,20 @@ import type { InputMenuItem } from "@nuxt/ui";
import hadithaApi from "../../apis/hadithaApi";
import { useStorage } from "@vueuse/core";
import * as z from "zod";
import type { FormSubmitEvent } from "@nuxt/ui";
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: {
@ -26,15 +38,22 @@ const userSearchHistory = useStorage(
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 synonymOne = ref(false);
const synonymTwo = ref(false);
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
@ -60,27 +79,57 @@ const state = reactive({
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();
},
},
],
@ -89,24 +138,7 @@ const state = reactive({
value: "synonym",
label: "مترادف",
icon: "i-haditha-chevron-down",
items: [
{
label: "جستجو در همه",
slot: "arabic",
},
{
label: "فقط در متن عربی حدیث",
slot: "arabic",
},
{
label: "فقط در ترجمه ها",
slot: "arabic",
},
{
label: "فقط در شروح",
slot: "arabic",
},
],
items: [],
},
type_key: {
label: "ترجمه",
@ -130,9 +162,6 @@ const state = reactive({
// );
// #endregion emits
onMounted(() => {
if (searchTerm.value.length) sendQuery();
});
// #region methods
const clearSimilar = () => {
@ -162,7 +191,7 @@ const setKey = (type: string) => {
sendQuery();
};
const sendQuery = async () => {
const sendQuery = async (payload = {}) => {
if (loading.value) return;
loading.value = true;
@ -175,9 +204,20 @@ const sendQuery = async () => {
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=${searchTerm.value}` : "q=none"
searchTerm.value.length
? `q=${isTypeSelected ? "#" + typeModelValueFa.value + " " : ""}${
searchTerm.value
}`
: "q=none"
);
// const baseURL =
@ -185,7 +225,7 @@ const sendQuery = async () => {
// fetch search list from backend(ssr)
return await httpService
.postRequest(url)
.postRequest(url, payload)
.then((res) => {
// pass res and search query to the parent.
emit("response-ready", {
@ -211,35 +251,127 @@ const sendQuery = async () => {
});
};
// ------------------- form -------------------
type Schema = z.output<typeof schema>;
const toast = useToast();
const schema = z.object({
name: z.string(),
name: z.string().min(1, "این فیلد ضروری است"),
});
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);
}
// 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");
sendQuery().then(() => {
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>
@ -305,7 +437,7 @@ async function openSynonymPopup(type: string) {
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')"
@ -318,14 +450,15 @@ async function openSynonymPopup(type: string) {
<UIcon
v-if="search_type == 'vector'"
@click.self="search_type = 'normal'"
@click.self="setType('normal')"
name="i-haditha-close-bg-circle"
size="20px"
>
</UIcon>
</UButton>
<!-- مترادف -->
<!-- #endregion -->
<!-- #region مترادف -->
<UPopover
:content="{
align: 'start',
@ -339,119 +472,94 @@ async function openSynonymPopup(type: string) {
>
<UButton
@click="openSynonymPopup('synonym')"
:class="{ active: search_type == 'synonym' }"
class="filter-item"
:label="state.synonym.label"
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 -->
<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>
<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>
<USeparator class="px-2" color="neutral" type="solid" size="xs" />
<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>
<!-- synonym item -->
<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="synonymTwo" />
</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" />
<USeparator class="px-2" color="neutral" type="solid" size="xs" />
</template>
</template>
</UPopover>
<!-- ترجمه -->
<UButton
<!-- #endregion -->
<!-- #region ترجمه -->
<!-- <UButton
@click.self="setKey('hadith_fa')"
:class="{ active: type_key == 'hadith_fa' }"
type="button"
@ -466,8 +574,9 @@ async function openSynonymPopup(type: string) {
size="20px"
>
</UIcon>
</UButton>
<!-- عین عبارت -->
</UButton> -->
<!-- #endregion -->
<!-- #region عین عبارت -->
<UButton
@click.self="setType('phrase')"
:class="{ active: search_type == 'phrase' }"
@ -482,11 +591,12 @@ async function openSynonymPopup(type: string) {
>
</UIcon>
</UButton>
<!-- #endregion -->
</div>
<!-- نوع -->
<!-- #region نوع -->
<div>
<UDropdownMenu
<USelect
:items="state.type.items"
:content="{
align: 'start',
@ -496,28 +606,44 @@ async function openSynonymPopup(type: string) {
: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
<!-- <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="state.type.value != 'normal'"
@click.self="state.type.value = 'normal'"
<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>
</UIcon>
</UButton>-->
<!-- <template #item> item </template> -->
<!-- <template #item-label> item label </template> -->
</UDropdownMenu>
</USelect>
</div>
<!-- #endregion -->
</div>
</div>
</template>
@ -542,7 +668,8 @@ async function openSynonymPopup(type: string) {
backdrop-filter: blur(60px);
background: linear-gradient(137.41deg, #ffffff -42.82%, #e5e0ff 87.9%);
filter: blur(60px);
width: 626px;
max-width: 626px;
width:100%;
height: 68px;
z-index: 0;
}
@ -730,6 +857,7 @@ async function openSynonymPopup(type: string) {
}
}
.popover-root-content {
overflow-y: auto;
width: 20.5em;
height: 17.75em;
/* gap: 8px; */
@ -782,6 +910,10 @@ async function openSynonymPopup(type: string) {
border: 0.3px solid #29d985;
color: #626b84;
}
&[disabled="true"] {
filter: grayscale(0.7);
}
}
.add-button {
width: 48;

View File

@ -18,7 +18,7 @@ const props = defineProps({
<div class="library-list-contianer">
<div class="page-header flex items-center">
<span class="title">کتابخانه</span>
<NuxtImg
<img
fit="auto"
quality="80"
placeholder
@ -41,30 +41,35 @@ const props = defineProps({
>
<!-- <template #header></template> -->
<ULink
:to="{
name:'hadithaLibraryShow',
params:{
id:1,
slug:'اصول کافی-جلد 1'
}
}"
color="neutral"
variant="outline"
:ui="{
leadingIcon: 'text-(--ui-primary)',
}"
@click=""
>
<NuxtImg
fit="auto"
quality="80"
placeholder
src="/img/haditha/sample-bgi.svg"
/>
<p class="title">اصول کافی</p>
<p class="version">جلد 1</p>
</ULink>
<client-only>
<ULink
:to="{
name: 'hadithaLibraryShow',
params: {
id: 1,
slug: item?._source?.title,
},
}"
color="neutral"
variant="outline"
:ui="{
leadingIcon: 'text-(--ui-primary)',
}"
@click=""
>
<img
fit="auto"
quality="80"
placeholder
src="/img/haditha/sample-bgi.svg"
/>
<p class="title">{{ item?._source?.title }}</p>
<p class="version">
{{ item?._source?.vol_title + item?._source?.vol_num }}
</p>
</ULink>
</client-only>
<!-- <template #footer> </template> -->
</UCard>
@ -72,7 +77,7 @@ const props = defineProps({
class="h-full w-full flex flex-col justify-center items-center"
v-else
>
<NuxtImg fit="auto" quality="80" placeholder :src="props.noDataIcon" />
<img fit="auto" quality="80" placeholder :src="props.noDataIcon" />
<p class="no-data-text">{{ props.noDataText }}</p>
</no-data>
</div>

View File

@ -59,13 +59,17 @@ const items = ref([
{
label: "خروج از حساب",
icon: "i-haditha-logout",
to: "/haditha/logout",
type: "button" as const,
onSelect(e: Event) {
e.preventDefault();
logout();
},
},
],
},
]);
const leftItem = ref([
const leftItem = computed(() => [
{
label: "نشان شده ها",
icon: "i-haditha-bookmark",
@ -94,7 +98,11 @@ const leftItem = ref([
{
label: "خروج از حساب",
icon: "i-haditha-logout",
to: "/haditha/logout",
type: "button" as const,
onSelect(e: Event) {
e.preventDefault();
logout();
},
},
],
},
@ -186,22 +194,22 @@ onMounted(() => {
.fixed {
z-index: 999;
.my-navbar {
max-width: 1200px;
height: 68px;
border-radius: 16px;
max-width: 75em; //1200px
height: 4.25em; // 68px;
border-radius: 1em;
border-width: 0.3px;
// justify-content: space-between;
padding-top: 4px;
padding-right: 16px;
padding-bottom: 4px;
padding-left: 16px;
padding-top: 0.25em;
padding-right: 1em;
padding-bottom: 0.25em;
padding-left: 1em;
background-color: #fff;
border: 0.3px solid #e0e0e0;
box-shadow: 0px 4px 15px 0px #0000001a;
@media screen and (max-width: 991.99px) {
height: 76px;
height: 5em; // 76px;
}
nav > div {
@ -226,23 +234,23 @@ onMounted(() => {
&::before {
background-color: color-mix(in oklab, #00a762 50%, transparent);
box-shadow: 0px 4px 10px 0px #00745933;
border-radius: 12px;
border-radius: 0.75em; //12px;
}
}
// max-width: 112px;
height: 48px;
height: 3.5em;
gap: 4px;
border-radius: 12px;
padding-top: 6px;
padding-right: 16px;
padding-bottom: 6px;
padding-left: 16px;
border-radius: 0.75em;
padding-top: 0.37em; /*6px*/
padding-right: 1.2em;
padding-bottom: 0.37em; /*6px*/
padding-left: 1.2em;
font-family: IRANSansX;
font-weight: 400;
font-size: 14px;
line-height: 21px;
font-size: 0.87rem; /*14px*/
line-height: 1.3rem; /*6px*/
letter-spacing: 0%;
text-align: center;
@ -260,7 +268,7 @@ onMounted(() => {
}
@media screen and (max-width: 991.99px) {
height: 60px;
height: 5em;
}
}
@ -285,10 +293,10 @@ onMounted(() => {
gap: 4px;
border-radius: 12px;
padding-top: 14px;
padding-right: 16px;
padding-bottom: 14px;
padding-left: 16px;
padding-top: 0.875em; /*14px*/
padding-right: 1em; /*16px*/
padding-bottom: 0.875em; /*14px*/
padding-left: 1em; /*16px*/
background: linear-gradient(102.02deg, #4be8ae 7.38%, #00a762 91.78%);
box-shadow: 0px 4px 10px 0px #00745933;

View File

@ -40,7 +40,7 @@ const state = reactive({
<div class="header flex items-center justify-center mb-8">
<span class="title me-1">تجربه شما با </span>
<NuxtImg
<img
width="49"
height="18"
fit="auto"
@ -78,7 +78,7 @@ const state = reactive({
dot: 'dot size-2',
}"
>
<NuxtImg
<img
width="100"
height="100"
fit="auto"
@ -108,8 +108,8 @@ const state = reactive({
.title {
font-family: IRANSansX;
font-weight: 300;
font-size: 20px;
line-height: 30px;
font-size: 1.25rem; /* 20px;*/
line-height: 1.87rem; /* 30px;*/
letter-spacing: 0%;
text-align: center;
@ -120,8 +120,8 @@ const state = reactive({
.title {
font-family: IRANSansX;
font-weight: 400;
font-size: 16px;
line-height: 24px;
font-size: 1rem;
line-height: 1.5rem; /* 24px;*/
letter-spacing: 0%;
text-align: center;
@ -143,8 +143,8 @@ const state = reactive({
.job {
font-family: IRANSansX;
font-weight: 300;
font-size: 16px;
line-height: 24px;
font-size: 1rem;
line-height: 1.5rem;
letter-spacing: 0%;
text-align: center;
color: #626b84;
@ -155,8 +155,8 @@ const state = reactive({
margin-bottom: 0.3em;
font-family: IRANSansX;
font-weight: 300;
font-size: 16px;
line-height: 24px;
font-size: 1rem;
line-height: 1.5rem;
letter-spacing: 0%;
text-align: justify;
@ -173,7 +173,7 @@ const state = reactive({
width: 40;
height: 56;
padding: 1.2em 0.6em;
border-radius: 12px;
border-radius: 0.75em;
background: #ffffff;
border: 0.3px solid #e0e0e0;
@ -181,6 +181,12 @@ const state = reactive({
color: #545aea;
}
.prev {
right: 0;
}
.next {
left: 0;
}
.content {
.controls {
@ -198,8 +204,7 @@ const state = reactive({
#4be8ae 7.38%,
#00a762 91.78%
);
border:0;
border: 0;
}
}
}

View File

@ -38,10 +38,10 @@ const state = reactive({
<UContainer
class="section-container mx-auto max-w-[var(--ui-container-two)] sm:px-6 lg:px-4"
>
<div class="header flex items-center justify-center mb-8 ">
<div class="header flex items-center justify-center mb-8">
<span class="title me-1">امکانات </span>
<NuxtImg
<img
width="49"
height="18"
fit="auto"
@ -53,7 +53,9 @@ const state = reactive({
</div>
<!-- grid-cols-1 md:grid-cols-2 lg:grid-cols-3 -->
<div class="content flex justify-center flex-wrap items-start p-4 space-y-3">
<div
class="content flex justify-center flex-wrap items-start p-4 space-y-3"
>
<UCard
:ui="{
root: 'ring ring-[white] shadow-none bg-transparent card-item flex flex-col items-center basis-[360px] p-4',
@ -65,12 +67,7 @@ const state = reactive({
:key="index"
>
<template #header>
<NuxtImg
fit="auto"
quality="100"
:src="item.img"
class="me-2"
/>
<img fit="auto" quality="100" :src="item.img" class="me-2" />
</template>
<p class="mb-2 title">{{ item.title }}</p>
@ -91,8 +88,8 @@ const state = reactive({
.title {
font-family: IRANSansX;
font-weight: 300;
font-size: 20px;
line-height: 30px;
font-size: 1.25rem; /* 20px;*/
line-height: 1.87rem; /* 30px;*/
letter-spacing: 0%;
text-align: center;
@ -106,16 +103,16 @@ const state = reactive({
.title {
font-family: IRANSansX;
font-weight: 600;
font-size: 16px;
line-height: 24px;
font-size: 1rem;
line-height: 1.5rem; /* 24px;*/
letter-spacing: 0%;
text-align: center;
}
.description {
font-family: IRANSansX;
font-weight: 300;
font-size: 14px;
line-height: 21px;
font-size: 0.87rem; /* 14px;*/
line-height: 1.31rem; /* 21px;*/
letter-spacing: 0%;
text-align: center;
}

View File

@ -1,9 +1,11 @@
<script setup>
const img = useImage();
const router = useRouter();
import { useStorage } from "@vueuse/core";
const backgroundImageStyle = computed(() => {
// Use $img to generate an optimized image URL
const optimizedImageUrl = img("/img/haditha/background.png", {
const optimizedImageUrl = img("/img/haditha/background.webp", {
quality: 80,
fit: "auto",
});
@ -13,6 +15,19 @@ const backgroundImageStyle = computed(() => {
};
});
const searchPhrase = useStorage("searchPhrase", "");
searchPhrase.value = '';
const handleResponseReady = (payload) => {
router.push({
name: "hadithaSearch",
query: {
q: payload.searchQuery,
},
});
};
const NavigationMenu = defineAsyncComponent(() =>
import("@haditha/components/haditha/NavigationMenu.vue")
);
@ -26,7 +41,12 @@ const AutoComplation = defineAsyncComponent(() =>
<navigation-menu></navigation-menu>
<div class="logo-container flex justify-center flex-col items-center">
<NuxtImg fit="auto" quality="80" placeholder src="/img/haditha/logo.png" />
<img
fit="auto"
quality="80"
placeholder
src="/img/haditha/logo.webp"
/>
<div class="title">
کاوش با
<span class="badge-style me-1"> هوش مصنوعی </span>
@ -35,7 +55,9 @@ const AutoComplation = defineAsyncComponent(() =>
</div>
</div>
<div class="search-box-container flex justify-center">
<auto-complation></auto-complation>
<auto-complation
@response-ready="handleResponseReady($event)"
></auto-complation>
</div>
</section>
</template>

View File

@ -3,8 +3,8 @@ const img = useImage();
const backgroundImageStyle = computed(() => {
// Use $img to generate an optimized image URL
const optimizedImageUrl = img("/img/haditha/section-three-bgi.png", {
quality: 80,
const optimizedImageUrl = img("/img/haditha/section-three-bgi.webp", {
quality: 30,
fit: "auto",
});
@ -17,14 +17,12 @@ const backgroundImageStyle = computed(() => {
<section class="section-three flex" :style="backgroundImageStyle">
<div class="section-container mx-auto">
<div class="header flex items-center mb-2">
<NuxtImg
width="88"
height="34"
<img
fit="auto"
quality="80"
placeholder
src="/img/haditha/section-three-logo.svg"
class="me-2"
title="حدیثا"
src="/img/haditha/section-three-logo.png"
class="me-2 haditha-text"
/>
<span class="title"> چگونه کار میکند؟ </span>
</div>
@ -64,17 +62,21 @@ const backgroundImageStyle = computed(() => {
width: 352.611083984375;
height: 56;
gap: 10px;
padding-top: 4px;
padding-right: 16px;
padding-bottom: 4px;
padding-left: 16px;
border-radius: 16px;
padding-top: 0.25em;
padding-right: 1em;
padding-bottom: 0.25em;
padding-left: 1em;
border-radius: 1em;
.haditha-text {
width: 5.5em; /*"88";*/
height: 2.1em; /*"34";*/
}
.title {
font-family: IRANSansX;
font-weight: 300;
font-size: 32px;
line-height: 48px;
font-size: 2rem; /*32px;*/
line-height: 3rem; /*48px;*/
letter-spacing: 0%;
text-align: center;
@ -87,17 +89,17 @@ const backgroundImageStyle = computed(() => {
height: 184;
gap: 10px;
padding-top: 8px;
padding-right: 16px;
padding-bottom: 8px;
padding-left: 16px;
border-radius: 16px;
padding-top: 0.5em;
padding-right: 1em;
padding-bottom: 0.5em;
padding-left: 1em;
border-radius: 1em;
background: #1b2132cc;
font-family: IRANSansX;
font-weight: 300;
font-size: 16px;
line-height: 24px;
font-size: 1rem;
line-height: 1.5rem; /*24px;*/
letter-spacing: 0%;
text-align: right;
color: #ffffff;

View File

@ -3,7 +3,7 @@
<div class="flex flex-col items-center py-6">
<div>قابلیت ها</div>
<div>
<NuxtImg
<img
width="32"
height="32"
fit="auto"
@ -17,7 +17,7 @@
<div class="flex flex-col lg:flex-row">
<div class="my-card card-one basis-full flex justify-center items-center">
<div class="inner-container">
<div>
<div class="px-4">
<p class="title">جستجوی معنایی</p>
<div class="mb-6">
<p class="description">ارائه نتایج دقیق و معنادار از میان</p>
@ -25,17 +25,17 @@
</div>
</div>
<NuxtImg
<img
fit="auto"
quality="80"
placeholder
src="/img/haditha/card-one.png"
src="/img/haditha/card-one.webp"
/>
</div>
</div>
<div class="my-card card-two basis-full flex justify-center items-center">
<div class="inner-container">
<div class="">
<div class="px-4">
<p class="title">جستجوی مترادفها</p>
<div class="mb-6">
<p class="description">یافتن سریع واژگان مرتبط برای</p>
@ -43,11 +43,11 @@
</div>
</div>
<NuxtImg
<img
fit="auto"
quality="80"
placeholder
src="/img/haditha/card-two.png"
src="/img/haditha/card-two.webp"
/>
</div>
</div>
@ -55,7 +55,7 @@
class="my-card card-three basis-full flex flex-col justify-center items-center"
>
<div class="inner-container">
<div>
<div class="px-4">
<p class="title">مشابهتیابی حدیث</p>
<div class="mb-6">
<p class="description">امکان یافتن احادیث یا مفاهیمی</p>
@ -63,11 +63,11 @@
</div>
</div>
<NuxtImg
<img
fit="auto"
quality="80"
placeholder
src="/img/haditha/card-three.png"
src="/img/haditha/card-three.webp"
/>
</div>
</div>
@ -78,15 +78,16 @@
<style scoped>
.section-two {
font-weight: 200;
font-size: 20px;
line-height: 30px;
font-size: 1.25rem; /*20px;*/
line-height: 1.8rem; /*30px;*/
letter-spacing: 0%;
text-align: center;
.my-card {
background-repeat: no-repeat;
background-size: auto;
height: 660px;
height: 41.25em; /* 660px*/
padding: 4.7em 0;
.inner-container {
max-width: 21.25em;
@ -95,8 +96,8 @@
.title {
font-family: IRANSansX;
font-weight: 700;
font-size: 24px;
line-height: 36px;
font-size: 1.5rem; /*24px*/
line-height: 2.25rem; /* 36px;*/
letter-spacing: 0%;
color: var(--ui-color-two);
text-align: right;
@ -105,23 +106,23 @@
.description {
font-weight: 300;
font-size: 20px;
line-height: 30px;
font-size: 1.25rem; /* 20px;*/
line-height: 1.87rem; /* 30px;*/
letter-spacing: 0%;
text-align: right;
color: #626b84;
}
&.card-one {
background-image: url("/img/haditha/card-one-bgi.png"),
background-image: url("/img/haditha/card-one-bgi.webp"),
linear-gradient(134.17deg, #ffffff -9.81%, #e5e0ff 87.62%);
}
&.card-two {
background-image: url("/img/haditha/card-two-bgi.png"),
background-image: url("/img/haditha/card-two-bgi.webp"),
linear-gradient(329.16deg, #b9fde0 13.45%, #eefff8 63.57%);
}
&.card-three {
background-image: url("/img/haditha/card-three-bgi.png"),
background-image: url("/img/haditha/card-three-bgi.webp"),
linear-gradient(134.17deg, #ffffff -9.81%, #e5e0ff 87.62%);
}
}

View File

@ -1,13 +1,14 @@
<script setup lang="ts">
import * as z from "zod";
import type { FormSubmitEvent } from "@nuxt/ui";
import hadithaApi from "../../../apis/hadithaApi";
import { useAuthStore } from "~/stores/authStore";
import { useStorage } from "@vueuse/core";
definePageMeta({
layout: false,
name: "hadithaLogin",
});
useHead({
title: `${import.meta.env.VITE_HADITH_PAGE_TITLE} | ورود`,
title: `${
import.meta.env.VITE_HADITH_PAGE_TITLE
} | وارد کردن شماره تلفن همراه`,
meta: [
{ name: "description", content: "کاوش با هوش مصنوعی در احادیث اسلامی" },
],
@ -16,6 +17,12 @@ useHead({
},
});
const emits = defineEmits(["next-step"]);
const router = useRouter();
const authStore = useAuthStore();
const loading = ref(false);
const localizedErrors = {
required: "Invalid email address",
// Add more error codes and translations as needed
@ -25,7 +32,7 @@ const schema = z.object({
mobile: z
.string()
.min(1, "این فیلد ضروری است.(exmaple : 09--*******)")
.max(11, "موبایل حداکثر 11 عدد می باشد.(exmaple : 09--*******)")
.max(11, "موبایل حداکثر 11 عدد می باشد.(exmaple : 09--*******)"),
});
type Schema = z.output<typeof schema>;
@ -36,84 +43,80 @@ const state = reactive<Partial<Schema>>({
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({
title: "Success",
description: "The form has been submitted.",
color: "success",
});
console.log(event.data);
}
// const payoload = {
// username: "dev",
// password: "dev123",
// // captcha: "",
// };
// components declaration
const HadithaLayout = defineAsyncComponent(
() => import("@haditha/layouts/HadithaLayout.vue")
);
const NavigationMenu = defineAsyncComponent(
() => import("@haditha/components/haditha/NavigationMenu.vue")
);
authStore
.loginAsGuest()
.then((res) => {
loading.value = false;
// check if search term is not empty
toast.add({
title: "Success",
description: res.data.message,
color: "success",
});
if (!res.data.isNew) emits("next-step");
else
navigateTo({
name: "haditha",
});
})
.catch((err) => {
console.info(err);
})
.finally(() => {
loading.value = false;
});
}
</script>
<template>
<HadithaLayout>
<div class="haditha-login-page h-full">
<div class="page-container pt-20 h-full">
<navigation-menu></navigation-menu>
<p class="title">خوش آمدید</p>
<p class="description">لطفا شماره موبایل خود را وارد کنید</p>
<UContainer
ui="{
base: 'sm:px-6 lg:px-4',
}"
class="page-inner-container sm:px-6 lg:px-4 h-full"
>
<div
class="page-content flex flex-col items-center justify-center h-full"
>
<p class="title">خوش آمدید</p>
<p class="description">لطفا شماره موبایل خود را وارد کنید</p>
<UForm
:schema="schema"
:state="state"
class="space-y-4 w-full"
@submit="onSubmit"
>
<UFormField
dir="ltr"
name="mobile"
class="mb-6"
:ui="{
root: 'root',
wrapper: 'wrapper',
labelWrapper: 'labelWrapper',
label: 'label',
container: 'container',
description: 'description',
error: 'error',
hint: 'hint',
help: 'help',
}"
>
<UInput
class="input-elem w-full"
placeholder="09--*******"
v-model="state.mobile"
type="text"
/>
</UFormField>
<UButton
class="submit-btn w-full flex justify-center"
variant="solid"
type="submit"
>
ورود / ثبت نام
</UButton>
</UForm>
</div>
</UContainer>
</div>
</div>
</HadithaLayout>
<UForm
:schema="schema"
:state="state"
class="space-y-4 w-full"
@submit="onSubmit"
>
<UFormField
dir="ltr"
name="mobile"
class="mb-6"
:ui="{
root: 'root',
wrapper: 'wrapper',
labelWrapper: 'labelWrapper',
label: 'label',
container: 'container',
description: 'description',
error: 'error',
hint: 'hint',
help: 'help',
}"
>
<UInput
class="input-elem w-full"
placeholder="09--*******"
v-model="state.mobile"
type="text"
/>
</UFormField>
<UButton
class="submit-btn w-full flex justify-center"
variant="solid"
type="submit"
>
ورود / ثبت نام
</UButton>
</UForm>
</template>
<style scoped>
@ -123,7 +126,8 @@ const NavigationMenu = defineAsyncComponent(
.page-container {
.page-inner-container {
margin: auto;
width: 512px;
/* max-width: 512px; */
max-width: 300px;
/* height: 125px; */
gap: 24px;
@ -170,7 +174,7 @@ const NavigationMenu = defineAsyncComponent(
.page-inner-container {
.input-elem {
input {
width: 480;
width: 48;
height: 53px;
justify-content: space-between;
border-radius: 12px;
@ -205,6 +209,31 @@ const NavigationMenu = defineAsyncComponent(
border: 0.3px solid #d9d9d9;
box-shadow: 0px 1px 4px 0px #0000000d;
}
.root-pin-input {
direction: ltr;
.base-pin-input {
width: 56px;
height: 53px;
justify-content: space-between;
border-radius: 12px;
border-width: 0.3px;
padding: 16px;
border: 0.3px solid #d9d9d9;
box-shadow: 0px 1px 4px 0px #0000000d;
font-family: IRANSansX;
font-weight: 400;
font-size: 16px;
line-height: 100%;
letter-spacing: 0%;
text-align: center;
color: var(--ui-color-two);
}
}
}
}
}

View File

@ -0,0 +1,248 @@
<script setup lang="ts">
import * as z from "zod";
import type { FormSubmitEvent } from "@nuxt/ui";
import hadithaApi from "../../../apis/hadithaApi";
import { useAuthStore } from "~/stores/authStore";
import { useTemplateRef, ref } from "vue";
useHead({
title: `${
import.meta.env.VITE_HADITH_PAGE_TITLE
} | وارد کردن شماره تلفن همراه`,
meta: [
{ name: "description", content: "کاوش با هوش مصنوعی در احادیث اسلامی" },
],
bodyAttrs: {
class: import.meta.env.VITE_HADITH_SYSTEM,
},
});
const router = useRouter();
const authStore = useAuthStore();
const loading = ref(false);
const pinInputRef = ref(null);
// const localizedErrors = {
// required: "Invalid email address",
// // Add more error codes and translations as needed
// };
const schema = z.object({
verifyCode: z.string().array(), // string[]
});
type Schema = z.output<typeof schema>;
const state = reactive<Partial<Schema>>({
verifyCode: [] as string[],
});
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
const payoload = {
username: "m.rezae",
password: "1234",
// captcha: "",
};
console.info("verify code");
// .loginAsGuest()
authStore
.login(payoload)
.then((res) => {
loading.value = false;
// check if search term is not empty
toast.add({
title: "Success",
description: res.data.message,
color: "success",
});
navigateTo({
name: "haditha",
});
})
.catch((err) => {
console.info(err);
})
.finally(() => {
loading.value = false;
});
}
onMounted(() => {
// console.info(pinInputRef.value);
// if (pinInputRef.value) {
// pinInputRef.value.focus(); // Or whatever focus method your PinInput component provides
// }
});
</script>
<template>
<p class="description">کد پیامک شده را وارد کنید</p>
<UForm
:schema="schema"
:state="state"
class="space-y-4 w-full"
@submit="onSubmit"
>
<UFormField
dir="ltr"
name="mobile"
class="mb-6"
:ui="{
root: 'root',
wrapper: 'wrapper',
labelWrapper: 'labelWrapper',
label: 'label',
container: 'container',
description: 'description',
error: 'error',
hint: 'hint',
help: 'help',
}"
>
<UPinInput
dir="ltr"
ref="pinInputRef"
otp
:length="4"
highlight
color="primary"
variant="outline"
v-model="state.verifyCode"
:ui="{
root: 'root-pin-input ltr w-full flex justify-between',
base: 'base-pin-input',
}"
/>
</UFormField>
<UButton
class="submit-btn w-full flex justify-center"
variant="solid"
type="submit"
>
ثبت کد
</UButton>
</UForm>
</template>
<style scoped>
.haditha-login-page {
background: #f7fffd;
.page-container {
.page-inner-container {
margin: auto;
/* max-width: 512px; */
max-width: 300px;
/* height: 125px; */
gap: 24px;
.title {
margin-bottom: 1.2em;
font-family: IRANSansX;
font-weight: 400;
font-size: 20px;
line-height: 30px;
letter-spacing: 0%;
/* Fallback color */
color: #4d00ff;
/* Gradient background */
background: linear-gradient(268.94deg, #d284ff -0.65%, #4d00ff 104.59%);
/* Clip the background to the text */
background-clip: text;
-webkit-background-clip: text; /* For Safari */
/* Make the text transparent */
color: transparent;
-webkit-text-fill-color: transparent; /* For Safari */
}
.description {
font-family: IRANSansX;
font-weight: 400;
font-size: 14px;
line-height: 21px;
letter-spacing: 0%;
color: #626b84;
margin-bottom: 5em; /* 70px */
}
}
}
}
</style>
<style>
.haditha-login-page {
.page-container {
.page-inner-container {
.input-elem {
input {
width: 48;
height: 53px;
justify-content: space-between;
border-radius: 12px;
border-width: 0.3px;
padding: 16px;
background: #ffffff;
border: 0.3px solid #d9d9d9;
box-shadow: 0px 1px 4px 0px #0000000d;
font-family: IRANSansX;
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0%;
text-align: center;
color: var(--ui-color-two);
}
}
.submit-btn {
width: 480;
height: 48px;
gap: 4px;
padding-top: 9.1px;
padding-right: 32px;
padding-bottom: 9.1px;
padding-left: 32px;
border-radius: 12px;
border-width: 0.3px;
background: linear-gradient(102.02deg, #4be8ae 7.38%, #00a762 91.78%);
border: 0.3px solid #d9d9d9;
box-shadow: 0px 1px 4px 0px #0000000d;
}
.root-pin-input {
direction: ltr;
.base-pin-input {
width: 56px;
height: 53px;
justify-content: space-between;
border-radius: 12px;
border-width: 0.3px;
padding: 16px;
border: 0.3px solid #d9d9d9;
box-shadow: 0px 1px 4px 0px #0000000d;
font-family: IRANSansX;
font-weight: 400;
font-size: 16px;
line-height: 100%;
letter-spacing: 0%;
text-align: center;
color: var(--ui-color-two);
}
}
}
}
}
</style>

View File

@ -62,17 +62,23 @@ function openModal(selectedItem) {
:key="index"
>
<p @click="openModal(item)" class="from-person">
{{ item._source.meta.hadith_masoum ?? "بدون عنوان" }}
{{
item?._source?.meta?.hadith_masoum ??
item?._source?.meta?.hadith_sanad ??
"بدون عنوان"
}}
</p>
<p @click="openModal(item)" class="arabic-text">
{{ item?._source?.content_ar }}
</p>
<p @click="openModal(item)" class="arabic-text">بدون متن عربی</p>
<p
class="persian-text"
v-html="item.highlight['content.fa'] ?? item._source.content"
v-html="item?.highlight?.['content.fa'] ?? item?._source?.content"
></p>
<div class="flex justify-end">
<p class="reference">
{{ item._source.address.vol_title }}، صفحه
{{ item._source.address.page_num }}
{{ item?._source?.address?.vol_title }}، صفحه
{{ item?._source?.address?.page_num }}
</p>
</div>
</div>
@ -81,7 +87,7 @@ function openModal(selectedItem) {
class="h-full w-full flex flex-col justify-center items-center"
v-else
>
<NuxtImg fit="auto" quality="80" placeholder :src="props.noDataIcon" />
<img fit="auto" quality="80" placeholder :src="props.noDataIcon" />
<p class="no-data-text">{{ props.noDataText }}</p>
</no-data>
</div>
@ -113,7 +119,7 @@ function openModal(selectedItem) {
<style scoped>
.search-list-contianer {
max-width: 656px;
max-width: 41em; /*656px*/
width: 100%;
margin: 0 1em;
@ -122,8 +128,8 @@ function openModal(selectedItem) {
font-family: IRANSansX;
font-weight: 400;
font-size: 11px;
line-height: 16.5px;
font-size: 0.68rem; /*11px*/
line-height: 1rem;
letter-spacing: 0%;
text-align: right;
color: #b4c2cf;
@ -145,8 +151,8 @@ function openModal(selectedItem) {
.from-person {
font-family: IRANSansX;
font-weight: 300;
font-size: 12px;
line-height: 18px;
font-size: 0.75rem; /*12px*/
line-height: 1.125rem; /*18px*/
letter-spacing: 0%;
text-align: right;
color: #00a762; /* #4be8ae 7.38% */
@ -162,8 +168,8 @@ function openModal(selectedItem) {
.arabic-text {
font-family: Takrim;
font-weight: 400;
font-size: 18px;
line-height: 32px;
font-size: 1.125rem; /*18px*/
line-height: 2rem; /*23px*/
letter-spacing: 0%;
text-align: right;
color: var(--ui-color-two);
@ -179,8 +185,8 @@ function openModal(selectedItem) {
.persian-text {
font-family: Takrim;
font-weight: 400;
font-size: 16px;
line-height: 22px;
font-size: 1rem; /*16px*/
line-height: 1.375rem; /*22px*/
letter-spacing: 0%;
text-align: right;
color: #626b84;
@ -189,18 +195,18 @@ function openModal(selectedItem) {
.reference {
height: 24px;
gap: 4px;
padding-top: 4px;
padding-right: 8px;
padding-bottom: 4px;
padding-left: 8px;
border-radius: 6px;
padding-top: 0.25em; /*4px*/
padding-right: 0.5em; /*8px*/
padding-bottom: 0.25em; /*4px*/
padding-left: 0.5em; /*8px*/
border-radius: 6px; /*18px*/
border-width: 0.5px;
border: 0.5px solid #d9d9d9;
font-family: IRANSansX;
font-weight: 300;
font-size: 10px;
line-height: 15px;
font-size: 0.625rem; /*10px*/
line-height: 0.9rem; /*15px*/
letter-spacing: 0%;
text-align: right;
color: #8a92a8;
@ -217,8 +223,8 @@ function openModal(selectedItem) {
.no-data-text {
font-family: IRANSansX;
font-weight: 300;
font-size: 16px;
line-height: 24px;
font-size: 1rem;
line-height: 1.5rem; /*24px*/
letter-spacing: 0%;
text-align: center;
}
@ -235,14 +241,14 @@ function openModal(selectedItem) {
box-shadow: 0px 8px 20px 0px #0000001a;
background: #ffffff;
width: 100%;
max-width: 720px;
border-radius: 16px;
max-width: 720px; /*18px*/
border-radius: 16px; /*18px*/
gap: 8px;
border-width: 0.3px;
.modal-body {
border-radius: 16px;
border-radius: 16px; /*18px*/
height: 800px;
height: 800px; /*18px*/
position: relative;
.top-left-bgi {

View File

@ -20,7 +20,7 @@ const closeModal = () => {
<div class="body-header">
<span class="top-left-bgi z-0"></span>
<div class="modal-title flex justify-between">
<NuxtImg
<img
fit="auto"
quality="80"
placeholder

View File

@ -1,4 +1,6 @@
<script setup>
import hadithaApi from "@haditha/apis/hadithaApi";
definePageMeta({
layout: false,
name: "hadithaFavorites",
@ -14,6 +16,72 @@ useHead({
},
});
// #region refs
const loading = ref(false);
const httpService = useNuxtApp()["$http"];
// #endregion refs
// #region reactive
const state = reactive({
// list: new Array(5).fill(0),
list: [],
counts: [],
totalCounts: [],
});
// #endregion reactive
// #region methods
const getCategories = async (dataType = "bookmark") => {
if (loading.value) return;
loading.value = true;
let url = repoUrl() + hadithaApi.favorite.getList;
url = url.replace("@field_collapsed", "book_title");
url = url.replace("@offset", 0);
url = url.replace("@limit", 20);
url = url.replace("@q", "none");
return await httpService
.getRequest(url)
.then((res) => {
state.list = res.data;
getCounts();
})
.catch((err) => {
console.info(err);
loading.value = false;
});
};
const getCounts = async () => {
let url = repoUrl() + hadithaApi.favorite.getCounts;
url = url.replace("@data_type", "bookmark");
await httpService
.getRequest(url)
.then((res) => {
state.totalCounts = res.hits.total.value;
state.counts = res.aggregations.result.buckets;
loading.value = false;
})
.catch((err) => {
console.info(err);
})
.finally(() => {
loading.value = false;
});
};
// #endregion methods
// #region hooks
onMounted(() => {
getCategories();
});
// #endregion methods
const HadithaLayout = defineAsyncComponent(() =>
import("@haditha/layouts/HadithaLayout.vue")
);
@ -24,10 +92,6 @@ const NavigationMenu = defineAsyncComponent(() =>
const SearchList = defineAsyncComponent(() =>
import("@haditha/components/haditha/search-page/SearchList.vue")
);
const state = reactive({
searchList: new Array(5).fill(0),
});
</script>
<template>
@ -41,7 +105,7 @@ const state = reactive({
<search-list
no-data-text="هنوز چیزی ذخیره نکرده‌اید!"
no-data-icon="/img/haditha/save.png"
:list="state.searchList"
:list="state.list"
></search-list>
</div>
</div>

View File

@ -1,11 +1,29 @@
<script setup lang="ts">
// 1. Imports
// 2. Metas
// 3. Props
// 2. Reactive State
// 3. Computed Properties
// 4. Functions / Methods
// 5. Lifecycle Hooks
// 6. Watchers
const route = useRoute();
const router = useRouter();
// #region imports
import hadithaApi from "@haditha/apis/hadithaApi";
// import type { HadithResponseModel } from "@haditha/types/hadithType";
import type { HadithResponseModel } from "~/systems/hadith_ui/types/hadithType";
// #endregion imports
// #region meta
definePageMeta({
layout: false,
name: "hadithaLibraryShow",
});
useHead({
title: `${import.meta.env.VITE_HADITH_PAGE_TITLE} | ${route.params.slug}`,
meta: [
@ -15,6 +33,8 @@ useHead({
class: import.meta.env.VITE_HADITH_SYSTEM,
},
});
// #endregion imports
// #region props
const props = defineProps({
list: {
@ -29,6 +49,9 @@ const props = defineProps({
default: "/img/haditha/no-data.png",
},
});
// #endregion props
// #region refs and reactives
const treeItems = [
{
@ -83,7 +106,52 @@ const treeItems = [
{ title: "فصل پنجم", icon: "vscode-icons:file-type-nuxt" },
];
const isModalOpen = ref(false);
const loading = ref(false);
const httpService = useNuxtApp()["$http"];
const state = reactive({
selectedItem: {} as HadithResponseModel,
});
// #endregion refs and reactives
// #region methods
const fetchData = async () => {
if (loading.value) return;
loading.value = true;
console.info(route.params.id);
let url = repoUrl() + hadithaApi.library.show;
url = url.replace("@appname", "monir");
url = url.replace("@page_start", "0");
url = url.replace("@page_end", "10");
url = url.replace("@vol_id", route.params.id.toString());
// fetch search list from backend(ssr)
const { data, status, error, refresh, clear } =
await useHadithaSearchComposable<HadithResponseModel>(url, {
method: "get",
});
// console.info(data.value);
console.info(status.value);
if (status.value == "success") {
console.info(data.value);
console.info(data.value.hits.hits);
state.selectedItem = <HadithResponseModel>data.value.hits.hits;
}
loading.value = false;
};
fetchData();
const goToTheLibrary = (type: string) => {
router.push({
name: "hadithaLibrary",
});
};
const onOpenList = () => {
console.info("onOpenList");
isModalOpen.value = true;
@ -97,6 +165,8 @@ const onClose = () => {
name: "hadithLibrary",
});
};
// #endregion methods
// components declaration
const HadithaLayout = defineAsyncComponent(
() => import("@haditha/layouts/HadithaLayout.vue")
@ -143,7 +213,7 @@ const UTree = defineAsyncComponent(
</UButton>
<UButton
@click="onClose"
@click="goToTheLibrary"
class="my-trailing-button close p-0 ms-8"
icon="i-lucide:x"
variant=""

View File

@ -1,4 +1,6 @@
<script setup>
import hadithaApi from "@haditha/apis/hadithaApi";
definePageMeta({
layout: false,
name: "hadithaLibrary",
@ -14,9 +16,53 @@ useHead({
},
});
// #region refs
const loading = ref(false);
const httpService = useNuxtApp()["$http"];
// #endregion refs
// #region reactive
const state = reactive({
list: [],
counts: [],
totalCounts: [],
libraryList: new Array(20).fill(0),
});
// #endregion reactive
// #region methods
const getLibraryList = async (dataType = "bookmark") => {
if (loading.value) return;
loading.value = true;
let url = repoUrl() + hadithaApi.library.list;
url = url.replace("@field_collapsed", "book_title");
url = url.replace("@offset", 0);
url = url.replace("@limit", 20);
url = url.replace("@q", "none");
return await httpService
.postRequest(url)
.then((res) => {
state.libraryList = res.hits.hits;
console.info(state.libraryList)
})
.catch((err) => {
console.info(err);
loading.value = false;
})
.finally(() => (loading.value = false));
};
// #endregion methods
// #region hooks
onMounted(() => {
getLibraryList();
});
// #endregion methods
// components declaration
const HadithaLayout = defineAsyncComponent(() =>

View File

@ -1,9 +1,4 @@
<script setup lang="ts">
import * as z from "zod";
import type { FormSubmitEvent } from "@nuxt/ui";
import hadithaApi from "../../apis/hadithaApi";
import { useAuthStore } from "~/stores/authStore";
definePageMeta({
layout: false,
name: "hadithaLogin",
@ -18,60 +13,7 @@ useHead({
},
});
const router = useRouter();
const authStore = useAuthStore();
// const localizedErrors = {
// required: "Invalid email address",
// // Add more error codes and translations as needed
// };
const schema = z.object({
mobile: z
.string()
.min(1, "این فیلد ضروری است.(exmaple : 09--*******)")
.max(11, "موبایل حداکثر 11 عدد می باشد.(exmaple : 09--*******)"),
});
const loading = ref(false);
type Schema = z.output<typeof schema>;
const state = reactive<Partial<Schema>>({
mobile: undefined,
});
const toast = useToast();
async function onSubmit(event: FormSubmitEvent<Schema>) {
console.log(event.data);
const payoload = {
username: "dev",
password: "dev123",
};
authStore
.login(payoload)
.then((res) => {
loading.value = false;
// check if search term is not empty
toast.add({
title: "Success",
description: res.data.message,
color: "success",
});
navigateTo({
name: "haditha",
});
})
.catch((err) => {
console.info(err);
})
.finally(() => {
loading.value = false;
});
}
const stepOne = ref(true);
// components declaration
const HadithaLayout = defineAsyncComponent(
@ -80,6 +22,12 @@ const HadithaLayout = defineAsyncComponent(
const NavigationMenu = defineAsyncComponent(
() => import("@haditha/components/haditha/NavigationMenu.vue")
);
const EnterMobile = defineAsyncComponent(
() => import("@haditha/components/haditha/login/EnterMobile.vue")
);
const EnterVerifyCode = defineAsyncComponent(
() => import("@haditha/components/haditha/login/EnterVerifyCode.vue")
);
</script>
<template>
@ -97,47 +45,11 @@ const NavigationMenu = defineAsyncComponent(
<div
class="page-content flex flex-col items-center justify-center h-full"
>
<p class="title">خوش آمدید</p>
<p class="description">لطفا شماره موبایل خود را وارد کنید</p>
<UForm
:schema="schema"
:state="state"
class="space-y-4 w-full"
@submit="onSubmit"
>
<UFormField
dir="ltr"
name="mobile"
class="mb-6"
:ui="{
root: 'root',
wrapper: 'wrapper',
labelWrapper: 'labelWrapper',
label: 'label',
container: 'container',
description: 'description',
error: 'error',
hint: 'hint',
help: 'help',
}"
>
<UInput
class="input-elem w-full"
placeholder="09--*******"
v-model="state.mobile"
type="text"
/>
</UFormField>
<UButton
class="submit-btn w-full flex justify-center"
variant="solid"
type="submit"
>
ورود / ثبت نام
</UButton>
</UForm>
<enter-mobile
v-if="stepOne"
@next-step="stepOne = !stepOne"
></enter-mobile>
<enter-verify-code v-else></enter-verify-code>
</div>
</UContainer>
</div>
@ -152,7 +64,8 @@ const NavigationMenu = defineAsyncComponent(
.page-container {
.page-inner-container {
margin: auto;
width: 512px;
/* max-width: 512px; */
max-width: 300px;
/* height: 125px; */
gap: 24px;
@ -199,7 +112,7 @@ const NavigationMenu = defineAsyncComponent(
.page-inner-container {
.input-elem {
input {
width: 480;
width: 48;
height: 53px;
justify-content: space-between;
border-radius: 12px;
@ -234,6 +147,31 @@ const NavigationMenu = defineAsyncComponent(
border: 0.3px solid #d9d9d9;
box-shadow: 0px 1px 4px 0px #0000000d;
}
.root-pin-input {
direction: ltr;
.base-pin-input {
width: 56px;
height: 53px;
justify-content: space-between;
border-radius: 12px;
border-width: 0.3px;
padding: 16px;
border: 0.3px solid #d9d9d9;
box-shadow: 0px 1px 4px 0px #0000000d;
font-family: IRANSansX;
font-weight: 400;
font-size: 16px;
line-height: 100%;
letter-spacing: 0%;
text-align: center;
color: var(--ui-color-two);
}
}
}
}
}

View File

@ -33,7 +33,7 @@ const NavigationMenu = defineAsyncComponent(() =>
<div class="page-header pt-38 pb-4 flex justify-between items-center">
<div class="flex items-center">
<h1 class="m-0 title">درباره</h1>
<NuxtImg
<img
fit="auto"
quality="80"
placeholder

View File

@ -64,11 +64,11 @@ const NavigationMenu = defineAsyncComponent(() =>
>
</div>
<div class="location">
<NuxtImg
<img
fit="auto"
quality="80"
placeholder
src="/img/haditha/location.png"
src="/img/haditha/location.webp"
/>
</div>
</div>

View File

@ -52,7 +52,7 @@ onMounted(() => {
<div class="page-header pt-38 pb-4 flex justify-between items-center">
<div class="flex items-center">
<h1 class="m-0 title">درباره</h1>
<NuxtImg
<img
fit="auto"
quality="80"
placeholder

View File

@ -33,7 +33,7 @@ const NavigationMenu = defineAsyncComponent(() =>
<div class="page-header pt-38 pb-4 flex justify-between items-center">
<div class="flex items-center">
<h1 class="m-0 title">قوانین و مقررات</h1>
<NuxtImg
<img
fit="auto"
quality="80"
placeholder

View File

@ -40,13 +40,14 @@ useHead({
// },
// },
// });
// #endregion imports
// #endregion props
// #region refs and reactives
const emit = defineEmits(["close"]);
const route = useRoute();
const router = useRouter();
const loading = ref(false);
const httpService = useNuxtApp()["$http"];
const state = reactive({
selectedItem: {} as HadithResponseShowModel,
@ -58,6 +59,8 @@ const fetchData = async () => {
if (loading.value) return;
loading.value = true;
console.info(route.params.id);
let url = hadithaApi.search.show;
url = url.replace("@index_key", "dhparag");
url = url.replace("@id", route.params.id);
@ -68,12 +71,17 @@ const fetchData = async () => {
method: "get",
});
console.info(data);
if (status.value == "success") {
loading.value = false;
state.selectedItem = <HadithResponseShowModel>data.value;
}
loading.value = false;
};
fetchData();
const goToTheSearch = (type: string) => {
router.push({
name: "hadithaSearch",
@ -85,9 +93,82 @@ const goToTheChatbot = () => {
});
};
const handleFavorite = async (item = {}) => {
console.info("addToFavorites");
addToFavorites();
// // add
// if (!item._source.tbookmark) {
// addToFavorites();
// }
// // delete
// else {
// removeToFavorites();
// }
};
const addToFavorites = async (item = {}) => {
console.info("addToFavorites");
let url = repoUrl() + hadithaApi.favorite.add;
url = url.replace("{{data_type}}", "bookmark");
url = url.replace("{{ref_key}}", "dhparag");
const formData = {
// ref_id: item._id,
ref_id: "pdh388_2113",
title: "عنوان تست_حدیثا بوک مارک",
// title: item._source.title,
};
httpService.postRequest(url, formData).then((res) => {
// this.updateListAnswer(index, "tbookmark", 1);
});
};
const removeFromFavorites = async (item = {}, index = 0) => {
let url = repoUrl() + hadithaApi.favorite.deleteByRefid;
url = url.replace("{{data_type}}", "bookmark");
url = url.replace("{{index_key}}", "dhparag");
url = url.replace("{{ref_id}}", "pdh388_2113");
const formData = {
ref_id: item._id,
title: item._source.title,
};
httpService.postRequest(url, formData).then((res) => {
// this.updateListAnswer(index, "tbookmark", 0);
});
};
const handlePagination = (prevNextIndicator: string) => {
console.info("fsdfsdfsdf");
if (loading.value) return;
loading.value = true;
let url = repoUrl() + hadithaApi.search.prevNextHadith;
url = url.replace("@index_key", "dhparag");
url = url.replace("@vol_id", state.selectedItem._source.address.vol_id);
url = url.replace("@parag_order", state.selectedItem._source.parag_order);
url = url.replace("@step", prevNextIndicator);
httpService
.getRequest(url)
.then((res) => {
state.selectedItem = res.hits.hits?.[0];
})
.finally(() => (loading.value = false));
};
const localCopyTextToClipboard = (text: string) => {
copyTextToClipboard(text);
};
// #endregion methods
fetchData();
// #region hooks
// onMounted(() => {
// console.info("mounted");
// });
// #endregion methods
</script>
<template>
@ -101,7 +182,7 @@ fetchData();
<div class="body-header">
<span class="top-left-bgi z-0"></span>
<div class="modal-title flex justify-between">
<NuxtImg
<img
fit="auto"
quality="80"
placeholder
@ -123,9 +204,17 @@ fetchData();
<div class="bg-container h-full">
<div class="header flex">
<UButton
variant="ghost"
@click="handleFavorite"
:variant="
state.selectedItem?._source?.tbookmark ? 'soft' : 'ghost'
"
color="primary"
class="bookmark-btn"
icon="i-haditha-tag"
:icon="
state.selectedItem?._source?.tbookmark
? 'i-haditha-tag-active'
: 'i-haditha-tag'
"
>
</UButton>
<div class="referene">
@ -146,7 +235,16 @@ fetchData();
}}
</span>
<UButton variant="ghost" class="copy-btn" label="کپی" />
<UButton
@click="
localCopyTextToClipboard(
state.selectedItem?._source?.meta?.hadith_masoum
)
"
variant="ghost"
class="copy-btn"
label="کپی"
/>
</div>
<div class="arabic-text">
<p>بدون متن عربی</p>
@ -158,7 +256,16 @@ fetchData();
<div class="text-persian-section">
<div class="section-header">
<span class="section-title"> ترجمه </span>
<UButton variant="ghost" class="copy-btn" label="کپی" />
<UButton
@click="
localCopyTextToClipboard(
state.selectedItem?._source?.content
)
"
variant="ghost"
class="copy-btn"
label="کپی"
/>
</div>
<p class="from">
@ -178,13 +285,52 @@ fetchData();
<div class="text-description-section">
<div class="section-header">
<span class="section-title"> شرح </span>
<UButton variant="ghost" class="copy-btn" label="کپی" />
<UButton
@click="
localCopyTextToClipboard(
state.selectedItem?._source?.content
)
"
variant="ghost"
class="copy-btn"
label="کپی"
/>
</div>
<p
class="description-item"
v-html="state.selectedItem?._source?.content"
></p>
</div>
<div class="separator"></div>
<div class="text-description-section">
<div class="text-sm mb-1">
<span class=""> کلیدواژگان: </span>
<span
class="me-1 text-gray-400 font-light"
v-for="(aiClass, i) in state.selectedItem?._source
?.ai_keywords"
:key="i"
>
{{ i > 0 ? "," : "" }}
{{ aiClass }}
</span>
</div>
<div class="text-sm">
<span class=""> دسته بندی ها: </span>
<span
class="me-1 text-gray-400 font-light"
v-for="(aiClass, i) in state.selectedItem?._source
?.ai_classes"
:key="i"
>
{{ i > 0 ? "," : "" }}
{{ aiClass.label }}
</span>
</div>
</div>
</div>
</div>
</div>
@ -195,13 +341,14 @@ fetchData();
<div class="mt-5 z-2">
<div class="flex justify-between actions">
<UButton
disabled
class="similar-btn"
icon="i-haditha-search-3"
label="مشابه"
color="neutral"
variant="outline"
@click.prevent="goToTheSearch('similar')"
/>
/>
<!-- @click.prevent="goToTheSearch('similar')" -->
<UButton
class="explore-btn"
trailing-icon="i-haditha-explore"
@ -212,6 +359,7 @@ fetchData();
</div>
<div class="flex justify-between pagination">
<UButton
@click="handlePagination('-1')"
class="prev-haditha"
label="حدیث قبل"
color=""
@ -219,6 +367,7 @@ fetchData();
icon="i-haditha-chevron-right"
/>
<UButton
@click="handlePagination('1')"
class="next-haditha"
label="حدیث بعد"
color=""
@ -263,7 +412,7 @@ fetchData();
padding-bottom: 4px;
padding-left: 12px;
border: 0.5px solid #d9d9d9;
background: #ffffff;
/* background: #ffffff; */
display: flex;
justify-content: center;
align-items: center;
@ -364,7 +513,7 @@ fetchData();
padding-right: 12px;
padding-bottom: 4px;
padding-left: 12px;
background: #ffffff;
/* background: #ffffff; */
border: 0.5px solid #d9d9d9;
font-family: IRANSansX;
@ -374,7 +523,7 @@ fetchData();
letter-spacing: 0%;
text-align: right;
color: #8a92a8;
/* color: #8a92a8; */
}
}
.text-arabic-section {

View File

@ -23,12 +23,12 @@ const state = reactive({
const backgroundImageStyle = computed(() => {
// Use $img to generate an optimized image URL
let optimizedImageUrl = img("/img/haditha/background.png", {
let optimizedImageUrl = img("/img/haditha/background.webp", {
quality: 40,
});
if (searchQuery.value?.length) {
optimizedImageUrl = img("/img/haditha/sub-header-bgi.png", {
optimizedImageUrl = img("/img/haditha/sub-header-bgi.webp", {
quality: 80,
});
@ -86,11 +86,11 @@ const SearchList = defineAsyncComponent(() =>
v-if="searchQuery?.length == 0"
class="flex justify-center flex-col items-center"
>
<NuxtImg
<img
fit="auto"
quality="80"
placeholder
src="/img/haditha/logo.png"
src="/img/haditha/logo.webp"
/>
<div class="title">
کاوش با
@ -103,7 +103,7 @@ const SearchList = defineAsyncComponent(() =>
class="h-full"
v-if="searchQuery?.length && state.searchList?.length == 0"
>
<NuxtImg
<img
fit="auto"
quality="80"
placeholder