Skip to content

Commit

Permalink
Add additional search view pages to the Nuxt app (#3140)
Browse files Browse the repository at this point in the history
* Add and update unit tests

* Add collections to the search&media stores & service

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Add collection page

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Add POC media fetching and collection header

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Make page matching more strict and set up page

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Add a test to validate-collection-params

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Do not show "0 results found" before fetch finished

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Use getCollectionPath from search store

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Simplify pages

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Fix load more

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Reset search state

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Set back to results path in single-result middleware

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Fix paddings

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Use Results type

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Remove page query param

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Refactor creatorHref

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Add requested changes

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Fix server rendering

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Add e2e tests

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Update bottom margin of the collection header

Signed-off-by: Olga Bulat <obulat@gmail.com>

* Fix e2e tests

Signed-off-by: Olga Bulat <obulat@gmail.com>

---------

Signed-off-by: Olga Bulat <obulat@gmail.com>
  • Loading branch information
obulat authored Dec 5, 2023
1 parent b5270e2 commit 6102985
Show file tree
Hide file tree
Showing 29 changed files with 3,533 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export default defineComponent({
},
},
setup(props) {
const mediaStore = useMediaStore()
const providerStore = useProviderStore()
const uiStore = useUiStore()
Expand Down Expand Up @@ -114,7 +115,10 @@ export default defineComponent({
const { getI18nCollectionResultCountLabel } = useI18nResultsCount()
const resultsLabel = computed(() => {
const resultsCount = useMediaStore().results[props.mediaType].count
if (mediaStore.resultCount === 0 && mediaStore.fetchState.isFetching) {
return ""
}
const resultsCount = mediaStore.results[props.mediaType].count
if (props.collectionParams.collection === "creator") {
return getI18nCollectionResultCountLabel(
resultsCount,
Expand Down
106 changes: 106 additions & 0 deletions frontend/src/components/VCollectionPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<template>
<div class="p-6 pt-0 lg:p-10 lg:pt-2">
<VCollectionHeader
v-if="collectionParams"
:collection-params="collectionParams"
:creator-url="creatorUrl"
:media-type="mediaType"
:class="mediaType === 'image' ? 'mb-4' : 'mb-2'"
/>
<VAudioCollection
v-if="results.type === 'audio'"
:collection-label="collectionLabel"
:fetch-state="fetchState"
kind="collection"
:results="results.items"
/>
<VImageGrid
v-if="results.type === 'image'"
:image-grid-label="collectionLabel"
:fetch-state="fetchState"
kind="collection"
:results="results.items"
/>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from "vue"
import { useMediaStore } from "~/stores/media"
import { useSearchStore } from "~/stores/search"
import type { SupportedMediaType } from "~/constants/media"
import { Results } from "~/types/result"
import { useI18n } from "~/composables/use-i18n"
import VCollectionHeader from "~/components/VCollectionHeader/VCollectionHeader.vue"
import VAudioCollection from "~/components/VSearchResultsGrid/VAudioCollection.vue"
import VImageGrid from "~/components/VSearchResultsGrid/VImageGrid.vue"
export default defineComponent({
name: "VCollectionPage",
components: { VAudioCollection, VImageGrid, VCollectionHeader },
props: {
mediaType: {
type: String as PropType<SupportedMediaType>,
required: true,
},
},
setup(props) {
const i18n = useI18n()
const mediaStore = useMediaStore()
const fetchState = computed(() => mediaStore.fetchState)
const results = computed<Results>(() => {
return {
type: props.mediaType,
items: mediaStore.resultItems[props.mediaType],
} as Results
})
const creatorUrl = computed(() => {
const media = results.value.items
return media.length > 0 ? media[0].creator_url : undefined
})
const searchStore = useSearchStore()
const collectionParams = computed(() => searchStore.collectionParams)
const collectionLabel = computed(() => {
const collection = collectionParams.value?.collection
switch (collection) {
case "tag":
return i18n
.t(`collection.ariaLabel.tag.${props.mediaType}`, {
tag: collectionParams.value?.tag,
})
.toString()
case "source":
return i18n
.t(`collection.ariaLabel.source.${props.mediaType}`, {
source: collectionParams.value?.source,
})
.toString()
case "creator":
return i18n
.t(`collection.ariaLabel.creator.${props.mediaType}`, {
creator: collectionParams.value?.creator,
source: collectionParams.value?.source,
})
.toString()
default:
return ""
}
})
return {
fetchState,
results,
creatorUrl,
collectionParams,
collectionLabel,
}
},
})
</script>
21 changes: 14 additions & 7 deletions frontend/src/components/VLoadMore.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,26 @@ export default defineComponent({
storeToRefs(mediaStore)
const { searchTerm } = storeToRefs(searchStore)
const searchStarted = computed(() => {
return searchStore.strategy === "default"
? searchTerm.value !== ""
: searchStore.collectionParams !== null
})
/**
* Whether we should show the "Load more" button.
* If the user has entered a search term, there is at least 1 page of results,
* there has been no fetching error, and there are more results to fetch,
* we show the button.
*/
const canLoadMore = computed(
() =>
searchTerm.value !== "" &&
!fetchState.value.fetchingError &&
!fetchState.value.isFinished &&
resultCount.value > 0
)
const canLoadMore = computed(() => {
return Boolean(
searchStarted.value &&
!fetchState.value.fetchingError &&
!fetchState.value.isFinished &&
resultCount.value > 0
)
})
const reachResultEndEventSent = ref(false)
/**
Expand Down
45 changes: 21 additions & 24 deletions frontend/src/components/VMediaInfo/VByLine/VByLine.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
import { useElementSize, useScroll, watchDebounced } from "@vueuse/core"
import { useI18n } from "~/composables/use-i18n"
import { useSearchStore } from "~/stores/search"
import type { SupportedMediaType } from "~/constants/media"
import VSourceCreatorButton from "~/components/VMediaInfo/VByLine/VSourceCreatorButton.vue"
Expand Down Expand Up @@ -95,7 +96,9 @@ export default defineComponent({
const buttonsRef = ref<HTMLElement | null>(null)
const showCreator = computed(() => {
return props.creator && props.creator.toLowerCase() !== "unidentified"
return Boolean(
props.creator && props.creator.toLowerCase() !== "unidentified"
)
})
const i18n = useI18n()
Expand Down Expand Up @@ -213,33 +216,27 @@ export default defineComponent({
{ debounce: 100 }
)
// TODO: implement this function in the search store.
const getCollectionPath = ({
type,
source,
creator,
}: {
type: SupportedMediaType
source: string
creator?: string
}) => {
let path = `/${type}/source/${source}/`
if (creator) path += `creator/${encodeURIComponent(creator)}/`
return path
}
const searchStore = useSearchStore()
const creatorHref = computed(() => {
return showCreator.value
? getCollectionPath({
type: props.mediaType,
source: props.sourceSlug,
creator: props.creator,
})
: undefined
if (!props.creator) return undefined
return searchStore.getCollectionPath({
type: props.mediaType,
collectionParams: {
collection: "creator",
source: props.sourceSlug,
creator: props.creator,
},
})
})
const sourceHref = computed(() => {
return getCollectionPath({
return searchStore.getCollectionPath({
type: props.mediaType,
source: props.sourceSlug,
collectionParams: {
collection: "source",
source: props.sourceSlug,
},
})
})
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/data/api-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export interface ApiService {
client: AxiosInstance
query<T = unknown>(
resource: string,
slug: string,
params: Record<string, string>
): Promise<AxiosResponse<T>>
get<T = unknown>(
Expand Down Expand Up @@ -138,19 +139,22 @@ export const createApiService = ({

/**
* @param resource - The endpoint of the resource
* @param slug - the optional additional endpoint, used for collections.
* @param params - Url parameter object
* @returns response The API response object
*/
query<T = unknown>(
resource: string,
params: Record<string, string>
slug: string = "",
params: Record<string, string> = {}
): Promise<AxiosResponse<T>> {
return client.get(`${getResourceSlug(resource)}`, { params })
return client.get(`${getResourceSlug(resource)}${slug}`, { params })
},

/**
* @param resource - The endpoint of the resource
* @param slug - The sub-endpoint of the resource
* @param params - Url query parameter object
* @returns Response The API response object
*/
get<T = unknown>(
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/data/media-service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { decodeMediaData } from "~/utils/decode-media-data"
import type { PaginatedSearchQuery } from "~/types/search"
import type {
PaginatedCollectionQuery,
PaginatedSearchQuery,
} from "~/types/search"
import type { ApiService } from "~/data/api-service"
import type { DetailFromMediaType, Media } from "~/types/media"
import { AUDIO, type SupportedMediaType } from "~/constants/media"
Expand Down Expand Up @@ -45,9 +48,11 @@ class MediaService<T extends Media> {
/**
* Search for media items by keyword.
* @param params - API search query parameters
* @param slug - optional slug to get a collection
*/
async search(
params: PaginatedSearchQuery
params: PaginatedSearchQuery | PaginatedCollectionQuery,
slug: string = ""
): Promise<MediaResult<Record<string, Media>>> {
// Add the `peaks` param to all audio searches automatically
if (this.mediaType === AUDIO) {
Expand All @@ -56,6 +61,7 @@ class MediaService<T extends Media> {

const res = await this.apiService.query<MediaResult<T[]>>(
this.mediaType,
slug,
params as unknown as Record<string, string>
)
return this.transformResults(res.data)
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/locales/scripts/en.json5
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,20 @@
source: "Open source site",
creator: "Open creator page",
},
ariaLabel: {
creator: {
audio: "Audio files by {creator} in {source}",
image: "Images by {creator} in {source}",
},
source: {
audio: "Audio files from {source}",
image: "Images from {source}",
},
tag: {
audio: "Audio files with the tag {tag}",
image: "Images with the tag {tag}",
},
},
resultCountLabel: {
creator: {
audio: {
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/middleware/collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useFeatureFlagStore } from "~/stores/feature-flag"

import type { Middleware } from "@nuxt/types"

export const collectionMiddleware: Middleware = async ({
$pinia,
error: nuxtError,
}) => {
if (!useFeatureFlagStore($pinia).isOn("additional_search_views")) {
nuxtError({
statusCode: 404,
message: "Additional search views are not enabled",
})
}
}
18 changes: 12 additions & 6 deletions frontend/src/middleware/single-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { AUDIO, IMAGE } from "~/constants/media"

import type { Middleware } from "@nuxt/types"

const isSearchPath = (path: string) => path.includes("/search/")
const isSearchOrCollectionPath = (path: string) =>
isSearchPath(path) || path.includes("/source/") || path.includes("/tag/")

export const singleResultMiddleware: Middleware = async ({
route,
from,
Expand All @@ -31,16 +35,18 @@ export const singleResultMiddleware: Middleware = async ({
// Client-side rendering
singleResultStore.setMediaById(mediaType, route.params.id)

if (from && from.path.includes("/search/")) {
if (from && isSearchOrCollectionPath(from.path)) {
const searchStore = useSearchStore($pinia)
searchStore.setBackToSearchPath(from.fullPath)

const searchTerm = Array.isArray(route.query.q)
? route.query.q[0]
: route.query.q
if (isSearchPath(from.path)) {
const searchTerm = Array.isArray(route.query.q)
? route.query.q[0]
: route.query.q

if (searchTerm) {
searchStore.setSearchTerm(searchTerm)
if (searchTerm) {
searchStore.setSearchTerm(searchTerm)
}
}
}
}
Expand Down
Loading

0 comments on commit 6102985

Please sign in to comment.