diff --git a/components/haditha/CardList.vue b/components/haditha/CardList.vue index b63e54c..11fade0 100644 --- a/components/haditha/CardList.vue +++ b/components/haditha/CardList.vue @@ -30,141 +30,89 @@ const goToLibraryShow = (item) => { </script> <template> - <div class="library-list-contianer"> - <div class="page-header flex items-center"> - <span class="title">کتابخانه</span> + <UCard + v-if="props.list?.length" + v-for="(item, index) in props.list" + :key="index" + variant="solid" + :ui="{ + root: 'ring ring-[white] divide-y divide-[var(--ui-border)] rounded-0 shadow-none bg-transparent library-list-item', + header: 'header', + body: 'sm:p-0 p-0 bg-transparent', + footer: 'footer', + }" + > + <!-- <template #header></template> --> + + <ULink + v-if="item?._source?.id" + :to="{ + name: 'hadithaLibraryShow', + params: { + id: item?._source?.id, + slug: item?._source?.title, + }, + query: { + page_first: item._source.page_first, + page_last: item._source.page_last, + page_count: item._source.page_count, + }, + }" + color="neutral" + variant="outline" + :ui="{ + leadingIcon: 'text-(--ui-primary)', + }" + > <img fit="auto" quality="80" placeholder - src="/img/haditha/haditha-title.svg" + src="/img/haditha/sample-bgi.svg" /> - </div> + <p class="title">{{ item?._source?.title }}</p> + <p class="version"> + {{ item?._source?.vol_title + item?._source?.vol_num }} + </p> + </ULink> - <div class="library-list grid grid-cols-5 gap-x-28 gap-y-12"> - <UCard - v-if="props.list.length" - v-for="(item, index) in props.list" - :key="index" - variant="solid" - :ui="{ - root: 'ring ring-[white] divide-y divide-[var(--ui-border)] rounded-0 shadow-none bg-transparent library-list-item', - header: 'header', - body: 'sm:p-0 p-0 bg-transparent', - footer: 'footer', - }" - > - <!-- <template #header></template> --> + <!-- <template #footer> </template> --> + </UCard> - <ULink - v-if="item?._source?.id" - :to="{ - name: 'hadithaLibraryShow', - params: { - id: item?._source?.id, - slug: item?._source?.title, - }, - query: { - page_first: item._source.page_first, - page_last: item._source.page_last, - page_count: item._source.page_count, - }, - }" - color="neutral" - variant="outline" - :ui="{ - leadingIcon: 'text-(--ui-primary)', - }" - > - <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> - - <!-- <template #footer> </template> --> - </UCard> - - <no-data - class="h-full w-full flex flex-col justify-center items-center" - v-else - > - <img fit="auto" quality="80" placeholder :src="props.noDataIcon" /> - <p class="no-data-text">{{ props.noDataText }}</p> - </no-data> - </div> - </div> + <no-data + class="h-full w-full flex flex-col justify-center items-center" + v-else + > + <img fit="auto" quality="80" placeholder :src="props.noDataIcon" /> + <p class="no-data-text">{{ props.noDataText }}</p> + </no-data> </template> <style scoped> -.library-list-contianer { - margin-top: 10em; - max-width: 1200px; - width: 100%; - margin: 0 1em; - margin-right: auto; - margin-left: auto; +.library-list-item { + width: 140; + height: 200; + border-radius: 8px; - .page-header { - margin-bottom: 2em; - .title { - margin-left: 0.4em; - font-family: IRANSansX; - font-weight: 300; - font-size: 24px; - line-height: 36px; - letter-spacing: 0%; - text-align: center; - - color: var(--ui-color-two); - } - } - - .library-list { - /* padding: 1em 1.3em; */ - height: calc(100dvh - 13.5em); - overflow-y: auto; - - .library-list-item { - width: 140; - height: 200; - border-radius: 8px; - - .title { - margin-top: 0.7em; - font-family: IRANSansX; - font-weight: 400; - font-size: 13px; - line-height: 19.5px; - letter-spacing: 0%; - text-align: right; - color: #444444; - } - - .version { - font-family: IRANSansX; - font-weight: 400; - font-size: 10px; - line-height: 15px; - letter-spacing: 0%; - text-align: right; - color: #444444; - } - } - } - .no-data-text { + .title { + margin-top: 0.7em; font-family: IRANSansX; - font-weight: 300; - font-size: 16px; - line-height: 24px; + font-weight: 400; + font-size: 13px; + line-height: 19.5px; letter-spacing: 0%; - text-align: center; + text-align: right; + color: #444444; + } + + .version { + font-family: IRANSansX; + font-weight: 400; + font-size: 10px; + line-height: 15px; + letter-spacing: 0%; + text-align: right; + color: #444444; } } </style> diff --git a/pages/haditha/library/index.vue b/pages/haditha/library/index.vue index dae0e9c..7e8e15b 100644 --- a/pages/haditha/library/index.vue +++ b/pages/haditha/library/index.vue @@ -1,5 +1,8 @@ -<script setup> +<script setup lang="ts"> import hadithaApi from "@haditha/apis/hadithaApi"; +import headLinks from "@haditha/json/haditha/headLinks"; +import headMetas from "@haditha/json/haditha/headMetas"; +import { useInfiniteScroll } from "@vueuse/core"; definePageMeta({ layout: false, @@ -10,153 +13,99 @@ useHead({ title: `${import.meta.env.VITE_HADITH_PAGE_TITLE} | کتابخانه`, meta: [ { name: "description", content: "کاوش با هوش مصنوعی در احادیث اسلامی" }, - { - name: "msapplication-TileImage", - content: "/img/haditha/fav-icons/ms-icon-144x144.png", - }, - { name: "theme-color", content: "#ffffff" }, + ...headMetas, ], bodyAttrs: { class: import.meta.env.VITE_HADITH_SYSTEM, }, - link: [ - { - rel: "icon", - type: "image/x-icon", - href: "/img/haditha/fav-icons/favicon.ico", - }, - { rel: "manifest", href: "/img/haditha/fav-icons/manifest.json" }, - // rel: icon - { - rel: "icon", - type: "image/png", - sizes: "16x16", - href: "/img/haditha/fav-icons/favicon-16x16.png", - }, - { - rel: "icon", - type: "image/png", - sizes: "32x32", - href: "/img/haditha/fav-icons/favicon-32x32.png", - }, - { - rel: "icon", - type: "image/png", - sizes: "96x96", - href: "/img/haditha/fav-icons/favicon-96x96.png", - }, - { - rel: "icon", - sizes: "192x192", - type: "image/png", - href: "/img/haditha/fav-icons/android-icon-192x192.png", - }, - // rel: apple - { - rel: "apple-touch-icon", - sizes: "57x57", - href: "/img/haditha/fav-icons/apple-icon-57x57.png", - }, - { - rel: "apple-touch-icon", - sizes: "60x60", - href: "/img/haditha/fav-icons/android-icon-60x60.png", - }, - { - rel: "apple-touch-icon", - sizes: "72x72", - href: "/img/haditha/fav-icons/android-icon-72x72.png", - }, - { - rel: "apple-touch-icon", - sizes: "76x76", - href: "/img/haditha/fav-icons/android-icon-76x76.png", - }, - { - rel: "apple-touch-icon", - sizes: "114x114", - href: "/img/haditha/fav-icons/android-icon-114x114.png", - }, - { - rel: "apple-touch-icon", - sizes: "120x120", - href: "/img/haditha/fav-icons/android-icon-120x120.png", - }, - { - rel: "apple-touch-icon", - sizes: "144x144", - href: "/img/haditha/fav-icons/android-icon-144x144.png", - }, - { - rel: "apple-touch-icon", - sizes: "152x152", - href: "/img/haditha/fav-icons/android-icon-152x152.png", - }, - { - rel: "apple-touch-icon", - sizes: "180x180", - href: "/img/haditha/fav-icons/android-icon-180x180.png", - }, - ], + link: headLinks, }); // #region refs -const loading = ref(false); +const el = useTemplateRef<HTMLElement>("el"); const httpService = useNuxtApp()["$http"]; - +const route = useRoute(); +const page = ref(Number(route.query.page) || 1); // #endregion refs // #region reactive const state = reactive({ - list: [], - counts: [], - totalCounts: [], - libraryList: new Array(20).fill(0), + pagination: { + offset: 0, + limit: 10, + page: 1, + pages: 1, + }, }); -// #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", "normal"); - url = url.replace("@offset", 0); - url = url.replace("@limit", 20); + url = url.replace("@offset", state.pagination.offset); + url = url.replace("@limit", state.pagination.limit); 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(); -}); + return await httpService.postRequest(url); +}; + +// Server-side initial load +const { data: initialItems } = await useAsyncData( + "libraryList", + () => getLibraryList(), + { + transform: (data) => data.hits.hits, + getCachedData: (key) => { + return useNuxtApp().payload.data[key] || useNuxtApp().static.data[key]; + }, + watch: [page], + } +); + +// Client-side state +const loadedItems = ref([]); +const loading = ref(false); +const hasMore = ref(true); +const loader = ref(null); +const totalPages = ref(10); // Set based on your API response + +// Client-side infinite scroll +useInfiniteScroll( + el, + async () => { + if (!hasMore.value || loading.value) return; + + loading.value = true; + try { + // const nextPage = page.value + 1; + await getLibraryList().then((res) => { + const hits = res.hits.hits; + + if (hits.length) { + loadedItems.value.push(...hits); + state.pagination.offset += state.pagination.limit; + } else { + hasMore.value = false; + } + }); + } finally { + loading.value = false; + } + }, + { distance: 100 } +); // #endregion methods // components declaration -const HadithaLayout = defineAsyncComponent(() => - import("@haditha/layouts/HadithaLayout.vue") +const HadithaLayout = defineAsyncComponent( + () => import("@haditha/layouts/HadithaLayout.vue") ); -const NavigationMenu = defineAsyncComponent(() => - import("@haditha/components/haditha/NavigationMenu.vue") +const NavigationMenu = defineAsyncComponent( + () => import("@haditha/components/haditha/NavigationMenu.vue") ); -const CardList = defineAsyncComponent(() => - import("@haditha/components/haditha/CardList.vue") +const CardList = defineAsyncComponent( + () => import("@haditha/components/haditha/CardList.vue") ); </script> @@ -165,11 +114,30 @@ const CardList = defineAsyncComponent(() => <div class="search-box-container h-full flex flex-col justify-center"> <navigation-menu></navigation-menu> - <card-list - no-data-text="هنوز چیزی ذخیره نکردهاید!" - no-data-icon="/img/haditha/no-data.png" - :list="state.libraryList" - ></card-list> + <div class="library-list-contianer"> + <div class="page-header flex items-center"> + <span class="title">کتابخانه</span> + <img fit="auto" quality="80" src="/img/haditha/haditha-title.svg" /> + </div> + + <div ref="el" class="library-list grid grid-cols-5 gap-x-28 gap-y-12"> + <!-- Client-side loaded content --> + <card-list + v-if="loadedItems.length" + no-data-text="هنوز چیزی ذخیره نکردهاید!" + no-data-icon="/img/haditha/no-data.png" + :list="loadedItems" + ></card-list> + + <!-- Server-rendered initial content --> + <card-list + v-else + no-data-text="هنوز چیزی ذخیره نکردهاید!" + no-data-icon="/img/haditha/no-data.png" + :list="initialItems" + ></card-list> + </div> + </div> </div> </HadithaLayout> </template> @@ -178,5 +146,43 @@ const CardList = defineAsyncComponent(() => .search-box-container { padding-top: 8.3em; background: #f7fffd; + + .library-list-contianer { + margin-top: 10em; + max-width: 1200px; + width: 100%; + margin: 0 1em; + margin-right: auto; + margin-left: auto; + + .page-header { + margin-bottom: 2em; + .title { + margin-left: 0.4em; + font-family: IRANSansX; + font-weight: 300; + font-size: 24px; + line-height: 36px; + letter-spacing: 0%; + text-align: center; + + color: var(--ui-color-two); + } + } + + .library-list { + /* padding: 1em 1.3em; */ + height: calc(100dvh - 13.5em); + overflow-y: auto; + } + .no-data-text { + font-family: IRANSansX; + font-weight: 300; + font-size: 16px; + line-height: 24px; + letter-spacing: 0%; + text-align: center; + } + } } </style>