Skip to content

Commit

Permalink
feat: Apps-3068 SEO friendly SectionPagination (#659)
Browse files Browse the repository at this point in the history
* feat: href instead of click event for seo optimization

* chore: cleanup

* fix: still emit click event

* chore: cleanup comments

* feat: add queryParams to sectionPagination callback

---------

Co-authored-by: Jess Divers <[email protected]>
  • Loading branch information
farosFreed and Jess Divers authored Dec 3, 2024
1 parent 8eac2fb commit 2b9eb98
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 24 deletions.
68 changes: 50 additions & 18 deletions src/lib-components/SectionPagination.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue'
import type { Ref } from 'vue'
import { useWindowSize } from '@vueuse/core'
import SvgIconArrowRight from 'ucla-library-design-tokens/assets/svgs/icon-arrow-right.svg'
import { useRoute } from 'vue-router'
import { useTheme } from '@/composables/useTheme'
// COMPONENTS
import SmartLink from '@/lib-components/SmartLink.vue'
// PROPS & DATA
const { nextTo, previousTo, pages, initialCurrentPage } = defineProps({
const { nextTo, previousTo, pages, initialCurrentPage, generateLinkCallback } = defineProps({
nextTo: {
type: String,
required: false,
Expand All @@ -26,8 +27,18 @@ const { nextTo, previousTo, pages, initialCurrentPage } = defineProps({
type: Number,
required: false,
},
// callback function to generate link for each page
// if not provided, will generate a link based on FTVA Elastic Search pattern
generateLinkCallback: {
type: Function,
required: false
}
})
const emit = defineEmits(['changePage']) // let parent component know when page changes
const emit = defineEmits(['changePage'])
// Router and Route
const route = useRoute()
const parsedQuery = computed(() => ({ ...route.query }))
const theme = useTheme()
const maxPages = ref(10) // default # of buttons that will fit in container, gets recalculated onMount & resize
const leftPages = ref([33]) // an array of numbers representing the page buttons that will appear ( we start with a single '33' so we can measure the width of a button to calc maxPages)
Expand All @@ -40,11 +51,28 @@ function handlePageChange(item: number) {
if (currPage.value !== item) {
currPage.value = item
generateLeftPages()
emit('changePage', item)
emit('changePage', item) // let parent component know when page changes
}
}
}
function generateLink(pageNumber: number) {
let queryParams = new URLSearchParams({ ...parsedQuery.value } as any)
if (generateLinkCallback) {
// if there are queryParams in route & generateLinkCallback prop provided
if (queryParams)
return generateLinkCallback(pageNumber, queryParams)
// else if generateLinkCallback prop provided
return generateLinkCallback(pageNumber)
}
// else use default logic
else {
queryParams = new URLSearchParams({ ...parsedQuery.value, page: pageNumber.toString() })
return `${route.path}?${queryParams.toString()}`
}
}
function generateLeftPages() {
if (pages && maxPages) {
let start = 1
Expand Down Expand Up @@ -156,33 +184,37 @@ onMounted(() => {
Previous
</div>
</SmartLink>
<SmartLink v-else-if="isNotFirstPage" class="previous" @click="handlePageChange(parsedPrevTo)">
<SmartLink v-else-if="isNotFirstPage" class="previous" :to="generateLink(parsedPrevTo)" @click="handlePageChange(parsedPrevTo)">
<SvgIconArrowRight class="previous-svg" />
<div class="underline-hover">
Previous
</div>
</SmartLink>
<div v-if="initialCurrentPage && pages" class="pagination-numbers-container">
<div class="pagination-numbers">
<span v-if="currPage > maxPages" class="page-list-first"><button
:class="`pButton${1 === currPage ? ' ' + 'pButton-selected' : ''}`"
@click="handlePageChange(1)"
>{{ 1 }}</button>
<span v-if="currPage > maxPages" class="page-list-first">
<SmartLink
:class="`pButton${1 === currPage ? ' ' + 'pButton-selected' : ''}`" :active="currPage === 1"
:to="generateLink(1)"
@click="handlePageChange(1)"
>{{ 1 }}</SmartLink>
</span>
<span v-if="currPage > maxPages" class="page-list-truncate">...</span>
<button
v-for="item in leftPages"
:key="item"
:class="`pButton${item === currPage ? ' ' + 'pButton-selected' : ''}`"
<SmartLink
v-for="item in leftPages" :key="item"
:class="`pButton${item === currPage ? ' ' + 'pButton-selected' : ''}`" :active="currPage === item"
:to="generateLink(item)"
@click="handlePageChange(item)"
>
{{ item }}
</button>
</SmartLink>
<span v-if="leftPages.length < pages && leftPages.indexOf(pages) === -1" class="page-list-truncate">...</span>
<span v-if="leftPages.length < pages && leftPages.indexOf(pages) === -1" class="page-list-right"><button
:class="`pButton${pages === currPage ? ' ' + 'pButton-selected' : ''}`"
@click="handlePageChange(pages)"
>{{ pages }}</button></span>
<span v-if="leftPages.length < pages && leftPages.indexOf(pages) === -1" class="page-list-right">
<SmartLink
:class="`pButton${pages === currPage ? ' ' + 'pButton-selected' : ''}`"
:active="currPage === pages" :to="generateLink(pages)" @click="handlePageChange(pages)"
>{{ pages }}</SmartLink>
</span>
</div>
</div>
<!-- if legacy attribute nextTo is supplied, use that for Next button instead of handlePageChange -->
Expand All @@ -192,7 +224,7 @@ onMounted(() => {
</div>
<SvgIconArrowRight class="next-svg" />
</SmartLink>
<SmartLink v-else-if="isNotLastPage" class="next" @click="handlePageChange(parsedNextTo)">
<SmartLink v-else-if="isNotLastPage" class="next" :to="generateLink(parsedNextTo)" @click="handlePageChange(parsedNextTo)">
<div class="underline-hover">
Next
</div>
Expand Down
24 changes: 18 additions & 6 deletions src/stories/SectionPagination.stories.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { computed, ref } from 'vue'
import SectionPagination from '@/lib-components/SectionPagination'
import router from '@/router'

/**
* A component to provide pagination for a list of items. It can be used in 2 ways:
Expand All @@ -10,11 +11,9 @@ import SectionPagination from '@/lib-components/SectionPagination'
* Props:
* - nextTo: A string representing the URL to the next page
* - previousTo: A string representing the URL to the previous page
*
* Props added 2024-10-29:
*
* - pages: A number representing the total number of pages we need to show all content
* - initialCurrentPage: A number representing the page we are starting on
* - pages: A number representing the total number of pages we need to show all content (added 2024-10-29)
* - initialCurrentPage: A number representing the page we are starting on (added 2024-10-29)
* - generateLinkCallback: A function that generates the link for the page number. It receives the page number as a parameter (added 2024-11-26)
*/
export default {
title: 'SECTION / Pagination',
Expand Down Expand Up @@ -42,10 +41,23 @@ export function LastPage() {
}
}

// this story uses the generateLinkCallback prop
// to generate the links in the library-website-nuxt format instead of the default format
export function WithPagesAndCurrentPage() {
// mock a library site page where someone has searched 'new' like this:
// https://www.library.ucla.edu/search-site?q=new&from=10'
router.push({ path: 'search-site', query: { q: 'new', from: 10 } })
return {
setup() {
// sample callback to generate the link
const sampleCallback = (pageNumber, queryParams) => {
return `/search-site?${queryParams}`
}

return { sampleCallback }
},
components: { SectionPagination },
template: '<section-pagination :pages="23" :initialCurrentPage="4" />',
template: '<section-pagination :pages="23" :initialCurrentPage="4" :generateLinkCallback="sampleCallback"/>',
}
}

Expand Down

1 comment on commit 2b9eb98

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.