Skip to content

Add additional search view pages to the Nuxt app #3140

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator

Choose a reason for hiding this comment

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

I might be misreading this, but doesn't this set searchStore.strategy to either default or a boolean value, due to this last check? Is that intended?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This computed is written very shortly so it's not easy to understand. I added a bit more verbosity.

This is actually setting searchStarted based on whether the strategy is default or not (so, one of the collections). If the strategy is default, it checks the searchTerm, otherwise - the collectionParams.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ahhhhh, okay I see that now! Thanks!

})

/**
* 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