feat(front): [WIP] search modal with accordion UI as filter
This commit is contained in:
parent
f1d6b11686
commit
c8fa78ef28
|
@ -1,30 +1,31 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type components } from '~/generated/types.ts'
|
import { type components } from '~/generated/types.ts'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch, computed } from 'vue'
|
||||||
import { refDebounced } from '@vueuse/core'
|
import { refDebounced } from '@vueuse/core'
|
||||||
|
|
||||||
import useErrorHandler from '~/composables/useErrorHandler'
|
import useErrorHandler from '~/composables/useErrorHandler'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useModal } from '~/ui/composables/useModal.ts'
|
import { useModal } from '~/ui/composables/useModal.ts'
|
||||||
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
|
|
||||||
|
|
||||||
import Modal from '~/components/ui/Modal.vue'
|
import Modal from '~/components/ui/Modal.vue'
|
||||||
import Button from '~/components/ui/Button.vue'
|
import Button from '~/components/ui/Button.vue'
|
||||||
import Layout from '~/components/ui/Layout.vue'
|
import Layout from '~/components/ui/Layout.vue'
|
||||||
import Spacer from '~/components/ui/Spacer.vue'
|
import Spacer from '~/components/ui/Spacer.vue'
|
||||||
import Input from '~/components/ui/Input.vue'
|
import Input from '~/components/ui/Input.vue'
|
||||||
|
import Section from '~/components/ui/Section.vue'
|
||||||
|
import ArtistCard from '~/components/artist/Card.vue'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const { isOpen, toggle, value: query } = useModal('search', { on: () => '', isOn: (value) => value !== undefined && value !== '' })
|
const { isOpen, value: query } = useModal('search', { on: () => '', isOn: (value) => value !== undefined && value !== '' })
|
||||||
onKeyboardShortcut('u', () => toggle())
|
|
||||||
|
|
||||||
const startRadio = () => {
|
const startRadio = () => {
|
||||||
// TODO: Start the radio
|
// TODO: Start the radio
|
||||||
console.log('start radio')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Search
|
||||||
|
|
||||||
const queryDebounced = refDebounced(query, 500)
|
const queryDebounced = refDebounced(query, 500)
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
@ -52,7 +53,107 @@ const search = async () => {
|
||||||
|
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(queryDebounced, search, { immediate: true })
|
watch(queryDebounced, search, { immediate: true })
|
||||||
|
|
||||||
|
// Filter
|
||||||
|
|
||||||
|
type QueryType = 'artists' | 'albums' | 'tracks' | 'playlists' | 'tags' | 'radios' | 'podcasts' | 'series' | 'rss'
|
||||||
|
|
||||||
|
type SearchType = {
|
||||||
|
id: QueryType
|
||||||
|
label: string
|
||||||
|
includeChannels?: boolean
|
||||||
|
contentCategory?: string
|
||||||
|
endpoint?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const types = computed(() => [
|
||||||
|
{
|
||||||
|
id: 'artists',
|
||||||
|
label: t('views.Search.label.artists'),
|
||||||
|
includeChannels: true,
|
||||||
|
contentCategory: 'music'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'albums',
|
||||||
|
label: t('views.Search.label.albums'),
|
||||||
|
includeChannels: true,
|
||||||
|
contentCategory: 'music'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tracks',
|
||||||
|
label: t('views.Search.label.tracks')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'playlists',
|
||||||
|
label: t('views.Search.label.playlists')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'radios',
|
||||||
|
label: t('views.Search.label.radios'),
|
||||||
|
endpoint: 'radios/radios'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tags',
|
||||||
|
label: t('views.Search.label.tags')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'podcasts',
|
||||||
|
label: t('views.Search.label.podcasts'),
|
||||||
|
endpoint: '/artists',
|
||||||
|
contentCategory: 'podcast',
|
||||||
|
includeChannels: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'series',
|
||||||
|
label: t('views.Search.label.series'),
|
||||||
|
endpoint: '/albums',
|
||||||
|
includeChannels: true,
|
||||||
|
contentCategory: 'podcast'
|
||||||
|
}
|
||||||
|
] as const satisfies SearchType[])
|
||||||
|
|
||||||
|
// Display
|
||||||
|
|
||||||
|
const implementedType = (id: QueryType | undefined) =>
|
||||||
|
id && id !== 'playlists' && id !== 'radios' && id !== 'podcasts' && id !== 'series' && id !== 'rss'
|
||||||
|
? id
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
watch(results, () => {
|
||||||
|
if (!results.value || results.value === null) return;
|
||||||
|
|
||||||
|
// If currently open section has no items, or no section is open, then switch to the first section with any items
|
||||||
|
|
||||||
|
const noOpenSection = openSection.value.length === 0
|
||||||
|
|
||||||
|
const currentImplementedSection = implementedType(openSection.value.at(0))
|
||||||
|
|
||||||
|
const currentImplementedSectionIsEmpty = () => currentImplementedSection && results.value?.[currentImplementedSection].length === 0
|
||||||
|
|
||||||
|
if (noOpenSection || currentImplementedSectionIsEmpty()) {
|
||||||
|
const firstTypeWithResults = types.value.find(({ id }) => {
|
||||||
|
const idIsImplemented = implementedType(id)
|
||||||
|
return idIsImplemented && results.value?.[idIsImplemented].length && results.value?.[idIsImplemented].length > 0
|
||||||
|
})
|
||||||
|
if (firstTypeWithResults)
|
||||||
|
openSection.value.unshift(firstTypeWithResults.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Show one section at a time (Accordion behaviour; clicking an open section navigates to the previous section)
|
||||||
|
|
||||||
|
const openSection = ref<QueryType[]>([])
|
||||||
|
|
||||||
|
const toggle = (id: QueryType): void => {
|
||||||
|
if (id === openSection.value.at(0)) {
|
||||||
|
openSection.value.shift()
|
||||||
|
} else {
|
||||||
|
openSection.value.unshift(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -75,11 +176,26 @@ watch(queryDebounced, search, { immediate: true })
|
||||||
{{ t('components.audio.podcast.Modal.button.startRadio') }}
|
{{ t('components.audio.podcast.Modal.button.startRadio') }}
|
||||||
</Button>
|
</Button>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
<Section
|
||||||
|
v-for="type in types"
|
||||||
|
:key="type.id"
|
||||||
|
:action="{ text: type.label, onClick: () => { toggle(type.id) } }"
|
||||||
|
tiny-items
|
||||||
|
align-left
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(result, index) in (results && type.id !== 'playlists' && type.id !== 'radios' && type.id !== 'podcasts' && type.id !== 'series' ? results[type.id] : [])"
|
||||||
|
:key="type.id+index"
|
||||||
|
>
|
||||||
|
{{ result }}
|
||||||
|
|
||||||
|
<ArtistCard
|
||||||
|
v-if="type.id === 'artists'"
|
||||||
|
:artist="result"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- TODO: Implement all the other cards here -->
|
||||||
|
</template>
|
||||||
|
</Section>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style module>
|
|
||||||
.description {
|
|
||||||
font-size: 0.875em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
Loading…
Reference in New Issue