Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Add desktop content switcher to the home page #613

Merged
merged 4 commits into from
Jan 19, 2022
Merged
Show file tree
Hide file tree
Changes from 2 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
86 changes: 86 additions & 0 deletions src/components/VContentSwitcher/VContentSwitcherButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<template>
<VButton
class="flex flex-row font-semibold px-3 py-2 text-sr md:text-base"
:class="{ 'w-12': isIconButton }"
:variant="buttonVariant"
size="disabled"
:aria-label="buttonLabel"
v-bind="a11yProps"
@click="$emit('click')"
>
<VIcon :icon-path="icon" />
<span v-show="showLabel" :class="{ 'ms-2': showLabel }">{{
buttonLabel
}}</span>
<VIcon
v-show="isMinScreenMd"
class="text-dark-charcoal-40"
:class="{ 'ms-2': isMinScreenMd }"
:icon-path="caretDownIcon"
/>
</VButton>
</template>
<script>
import { computed, inject, useContext } from '@nuxtjs/composition-api'
import useContentType from '~/composables/use-content-type'
import caretDownIcon from '~/assets/icons/caret-down.svg'

import VButton from '~/components/VButton.vue'
import VIcon from '~/components/VIcon/VIcon.vue'
import { isMinScreen } from '@/composables/use-media-query'
import { ALL_MEDIA, AUDIO, IMAGE } from '@/constants/media'

export default {
name: 'VContentSwitcherButton',
components: { VButton, VIcon },
props: {
a11yProps: {
type: Object,
required: true,
},
activeItem: {
type: String,
default: ALL_MEDIA,
validator: (val) => [ALL_MEDIA, IMAGE, AUDIO].includes(val),
},
},
setup(props) {
const { i18n } = useContext()
const isHeaderScrolled = inject('isHeaderScrolled', null)
const isMinScreenMd = isMinScreen('md', { shouldPassInSSR: true })

const { icons, activeType: activeItem } = useContentType()
const isIconButton = computed(
() => isHeaderScrolled?.value && !isMinScreenMd.value
)
const buttonVariant = computed(() => {
return isMinScreenMd.value && !isHeaderScrolled?.value
? 'tertiary'
: 'action-menu'
})
const buttonLabel = computed(() => {
const labelKey = {
image: 'search-type.image',
audio: 'search-type.audio',
all: 'search-type.all',
video: 'search-type.video',
}[props.activeItem]
Comment on lines +61 to +66
Copy link
Contributor

Choose a reason for hiding this comment

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

I always liked this approach to mapping variables to a different result (using an object inline). 🙂

return i18n.t(labelKey)
})
const showLabel = computed(
() => isMinScreenMd.value || !isHeaderScrolled.value
)

return {
buttonVariant,
isIconButton,
buttonLabel,
caretDownIcon,
showLabel,
isHeaderScrolled,
isMinScreenMd,
icon: computed(() => icons[activeItem.value]),
}
},
}
</script>
82 changes: 82 additions & 0 deletions src/components/VContentSwitcher/VContentSwitcherPopover.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<template>
<VPopover
ref="contentMenuPopover"
class="flex mx-4 items-stretch"
:label="$t('search-type.label')"
>
<template #trigger="{ a11yProps }">
<VContentSwitcherButton
:a11y-props="a11yProps"
:active-item="activeItem"
/>
</template>
<VItemGroup
direction="vertical"
:bordered="false"
type="radiogroup"
class="z-10"
>
<VItem
v-for="(item, idx) in content.types"
:key="idx"
:selected="item === activeItem"
:is-first="idx === 0"
@click.native="handleClick(item)"
>
<VIcon :icon-path="content.icons[item]" class="me-2 ms-4 my-4" />
<span class="pe-20 py-4 font-semibold">{{
$t(`search-type.${item}`)
}}</span>
</VItem>
</VItemGroup>
</VPopover>
</template>

<script>
import { ref } from '@nuxtjs/composition-api'
import useContentType from '~/composables/use-content-type'
import checkIcon from '~/assets/icons/checkmark.svg'

import VPopover from '~/components/VPopover/VPopover.vue'
import VContentSwitcherButton from '~/components/VContentSwitcher/VContentSwitcherButton.vue'

export default {
name: 'VContentSwitcherPopover',
components: {
VContentSwitcherButton,
VPopover,
},
props: {
activeItem: {
type: String,
required: true,
},
},
setup(props, { emit }) {
const content = useContentType()

const contentMenuPopover = ref(null)

/**
* Only the contentMenuPopover needs to be closed programmatically
*/
const closeMenu = () => {
if (contentMenuPopover.value) {
contentMenuPopover.value.close()
}
}

const handleClick = (item) => {
emit('select', item)
}

return {
content,
checkIcon,
handleClick,
contentMenuPopover,
closeMenu,
}
},
}
</script>
34 changes: 34 additions & 0 deletions src/composables/use-content-type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { computed, ref, useContext } from '@nuxtjs/composition-api'
import { supportedContentTypes } from '~/constants/media'
import { SEARCH } from '~/constants/store-modules'
import { UPDATE_QUERY } from '~/constants/action-types'
import allIcon from '~/assets/icons/all-content.svg'
import audioIcon from '~/assets/icons/audio-content.svg'
import imageIcon from '~/assets/icons/image-content.svg'

const icons = {
all: allIcon,
audio: audioIcon,
image: imageIcon,
}
export default function useContentType() {
const { store } = useContext()

const contentTypes = [...supportedContentTypes]

const activeType = computed(() => store.state.search.searchType)
const previousContentType = ref(activeType.value)
const setActiveType = async (contentType) => {
if (previousContentType.value === contentType) return
await store.dispatch(`${SEARCH}/${UPDATE_QUERY}`, {
searchType: contentType,
})
previousContentType.value = contentType
}
return {
setActiveType,
activeType,
types: contentTypes,
icons,
}
}
20 changes: 12 additions & 8 deletions src/constants/media.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ export const ALL_MEDIA = 'all'

/** @typedef {typeof AUDIO | typeof IMAGE | typeof VIDEO | typeof ALL_MEDIA} MediaType */

// Media types
/** @type {MediaType[]} */
export const mediaTypes = [AUDIO, IMAGE]
// Media types which support custom filters
/** @type {MediaType[]} */
export const supportedMediaTypes = [AUDIO, IMAGE, VIDEO]
/** @type {MediaType[]} */
export const allMediaTypes = [ALL_MEDIA, IMAGE, AUDIO, VIDEO]
/**
* Media types that the API supports.
* These types also support custom filters.
* @type {MediaType[]}
*/
export const supportedMediaTypes = [AUDIO, IMAGE]

/**
* The types of content that users can search. `All` is also an option here.
* @type {MediaType[]}
*/
export const supportedContentTypes = [ALL_MEDIA, AUDIO, IMAGE]
8 changes: 8 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -690,5 +690,13 @@
"interpunct": "•",
"modal": {
"close": "Close"
},
"search-type": {
"image": "Images",
"audio": "Audio",
"all": "All content",
"video": "Videos",
"label": "Type of content to search",
"heading": "Content types"
}
}
35 changes: 30 additions & 5 deletions src/locales/po-files/openverse.pot
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Openverse \n"
"Report-Msgid-Bugs-To: https://github.com/wordpress/openverse/issues \n"
"POT-Creation-Date: 2022-01-11T19:17:21+00:00\n"
"POT-Creation-Date: 2022-01-17T11:38:38+00:00\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
Expand All @@ -18,6 +18,31 @@ msgctxt "interpunct"
msgid "•"
msgstr ""

msgctxt "search-type.image"
msgid "Images"
msgstr ""

msgctxt "search-type.audio"
msgid "Audio"
msgstr ""

msgctxt "search-type.all"
msgid "All content"
msgstr ""

msgctxt "search-type.video"
msgid "Videos"
msgstr ""

#: src/components/VContentSwitcher/VContentSwitcherPopover.vue:5
msgctxt "search-type.label"
msgid "Type of content to search"
msgstr ""

msgctxt "search-type.heading"
msgid "Content types"
msgstr ""

#: src/components/VModal/VModalContent.vue:25
msgctxt "modal.close"
msgid "Close"
Expand Down Expand Up @@ -183,7 +208,7 @@ msgid "Loading..."
msgstr ""

#. Do not translate words between ### ###.
#: src/components/AudioThumbnail/AudioThumbnail.vue:33
#: src/components/VAudioThumbnail/VAudioThumbnail.vue:142
msgctxt "audio-thumbnail.alt"
msgid "Cover art for \"###title###\" by ###creator###"
msgstr ""
Expand All @@ -194,7 +219,7 @@ msgid "Audio seek bar"
msgstr ""

#. Do not translate words between ### ###.
#: src/components/VAudioTrack/VWaveform.vue:514
#: src/components/VAudioTrack/VWaveform.vue:516
msgctxt "waveform.current-time"
msgid "###time### seconds"
msgstr ""
Expand Down Expand Up @@ -1128,13 +1153,13 @@ msgid "An error occurred"
msgstr ""

#. Do not translate words between ### ###.
#: src/pages/image/_id.vue:54
#: src/pages/image/_id.vue:53
msgctxt "error.image-not-found"
msgid "Couldn't find image with id ###id###"
msgstr ""

#. Do not translate words between ### ###.
#: src/pages/audio/_id.vue:70
#: src/pages/audio/_id.vue:69
msgctxt "error.media-not-found"
msgid "Couldn't find ###mediaType### with id ###id###"
msgstr ""
Expand Down
34 changes: 32 additions & 2 deletions src/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@
<OpenverseLogoText class="w-80" />
</h1>
<h2>Browse through over 600 million items to reuse</h2>
<VSearchBar :placeholder="featuredSearch.term" />
<VSearchBar :placeholder="featuredSearch.term">
<VContentSwitcherPopover
v-if="isMounted && isMinScreenMd"
ref="contentSwitcher"
class="mx-3"
:active-item="contentType"
@select="setContentType"
/>
</VSearchBar>
<p>
All Openverse content is under a creative Comons license or is in the
All Openverse content is under a creative Commons license or is in the
public domain.
</p>
</header>
Expand Down Expand Up @@ -44,16 +52,24 @@
</template>

<script>
import { ALL_MEDIA } from '~/constants/media'
import { ref, onMounted } from '@nuxtjs/composition-api'
import { isMinScreen } from '~/composables/use-media-query'

import OpenverseLogoText from '~/assets/icons/openverse-logo-text.svg?inline'
import VContentSwitcherPopover from '~/components/VContentSwitcher/VContentSwitcherPopover.vue'

const HomePage = {
name: 'home-page',
layout: 'blank',
components: {
OpenverseLogoText,
VContentSwitcherPopover,
},
setup() {
const contentSwitcher = ref(null)
const isMinScreenMd = isMinScreen('md', { shouldPassInSSR: true })

const makeImageArray = (prefix = '') =>
new Array(7).fill({}).map((_, index) => ({
src: `${prefix}-${index + 1}.jpg`,
Expand All @@ -75,6 +91,14 @@ const HomePage = {
},
]
const featuredSearch = ref(featuredSearches[1])

const isMounted = ref(false)
const contentType = ref(ALL_MEDIA)
const setContentType = (type) => {
contentType.value = type
contentSwitcher.value?.closeMenu()
}

onMounted(() => {
setInterval(() => {
let activeIndex = featuredSearches.indexOf(featuredSearch.value)
Expand All @@ -83,10 +107,16 @@ const HomePage = {
activeIndex < featuredSearches.length - 1 ? activeIndex + 1 : 0
]
}, 6000)
isMounted.value = true
})

return {
featuredSearch,
contentSwitcher,
contentType,
setContentType,
isMounted,
isMinScreenMd,
}
},
}
Expand Down