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

Commit

Permalink
Add ImageGrid component and use it for RelatedImages (#281)
Browse files Browse the repository at this point in the history
* Extract ImageGrid component from SearchGridManualLoad

* Use ImageGrid for RelatedImages

* Remove RelatedImages fetching from PhotoDetailPage

* Replace Vuex related store with a composable and use in audio

* Replace related store with useRelated in images

* Apply changes from code review

* Add type annotations to RelatedImages

* Apply suggestions from code review

Co-authored-by: Krystle Salazar <krystle.salazar@automattic.com>
Co-authored-by: Zack Krida <zackkrida@pm.me>

* Update i18n files

* Use testing-library in ImageGrid and RelatedImages tests

* Use static props for testing

* Add comment about using testing-library with Nuxt

Co-authored-by: Krystle Salazar <krystle.salazar@automattic.com>
Co-authored-by: Zack Krida <zackkrida@pm.me>
  • Loading branch information
3 people authored Oct 8, 2021
1 parent 1c361e4 commit 62d20fd
Show file tree
Hide file tree
Showing 18 changed files with 715 additions and 314 deletions.
1 change: 1 addition & 0 deletions nuxt.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export default {
'~/components/ContentReport',
'~/components/Filters',
'~/components/ImageDetails',
'~/components/ImageGrid',
'~/components/MediaInfo',
'~/components/MetaSearch',
'~/components/MediaTag',
Expand Down
52 changes: 43 additions & 9 deletions src/components/AudioDetails/Related.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,56 @@
<h4 class="b-header mb-6">
{{ $t('audio-details.related-audios') }}
</h4>
<AudioTrack
v-for="audio in relatedAudios"
:key="audio.id"
:audio="audio"
:is-compact="true"
layout="row"
class="mb-12"
/>
<template v-if="!$fetchState.error">
<AudioTrack
v-for="audio in audios"
:key="audio.id"
:audio="audio"
layout="row"
size="m"
class="mb-12"
/>
<LoadingIcon v-show="$fetchState.pending" />
</template>
<p v-show="!!$fetchState.error">
{{ $t('media-details.related-error') }}
</p>
</aside>
</template>

<script>
import { ref } from '@nuxtjs/composition-api'
import { AUDIO } from '~/constants/media'
import useRelated from '~/composables/use-related'
export default {
name: 'RelatedAudios',
props: {
relatedAudios: {},
audioId: {
type: String,
required: true,
},
service: {},
},
/**
* Fetches related audios on `audioId` change
* @param {object} props
* @param {string} props.audioId
* @param {any} props.service
* @return {{ audios: Ref<AudioDetail[]> }}
*/
setup(props) {
const mainAudioId = ref(props.audioId)
const relatedOptions = {
mediaType: AUDIO,
mediaId: mainAudioId,
}
// Using service prop to be able to mock when testing
if (props.service) {
relatedOptions.service = props.service
}
const { media: audios } = useRelated(relatedOptions)
return { audios }
},
}
</script>
57 changes: 57 additions & 0 deletions src/components/ImageDetails/RelatedImages.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<template>
<aside
:aria-label="$t('photo-details.aria.related')"
class="p-4 my-6 photo_related-images"
>
<h3 class="b-header">
{{ $t('photo-details.related-images') }}
</h3>
<ImageGrid
:images="images"
:can-load-more="false"
:is-fetching="$fetchState.pending"
:fetching-error="$fetchState.error"
:error-message-text="null"
/>
</aside>
</template>

<script>
import { ref } from '@nuxtjs/composition-api'
import useRelated from '~/composables/use-related'
import { IMAGE } from '~/constants/media'
import ImageGrid from '~/components/ImageGrid/ImageGrid'
export default {
name: 'RelatedImages',
components: { ImageGrid },
props: {
imageId: {
type: String,
required: true,
},
service: {},
},
/**
* Fetches related images on `imageId` change
* @param {object} props
* @param {string} props.imageId
* @param {any} props.service
* @return {{ images: Ref<ImageDetail[]> }}
*/
setup(props) {
const mainImageId = ref(props.imageId)
const relatedOptions = {
mediaType: IMAGE,
mediaId: mainImageId,
}
// Using service prop to be able to mock when testing
if (props.service) {
relatedOptions.service = props.service
}
const { media: images } = useRelated(relatedOptions)
return { images }
},
}
</script>
223 changes: 223 additions & 0 deletions src/components/ImageGrid/ImageCell.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
<template>
<div
:aria-label="image.title"
class="search-grid_item-container"
:style="`width: ${containerAspect * widthBasis}px;
flex-grow: ${containerAspect * widthBasis}`"
>
<figure class="search-grid_item">
<i :style="`padding-bottom:${iPadding}%`" />
<NuxtLink
:to="localePath('/photos/' + image.id)"
class="search-grid_image-ctr"
:style="`width: ${imageWidth}%; top: ${imageTop}%; left:${imageLeft}%;`"
@click="onGotoDetailPage($event, image)"
>
<img
ref="img"
loading="lazy"
:class="{
'search-grid_image': true,
'search-grid_image__fill': !shouldContainImage,
}"
:alt="image.title"
:src="getImageUrl(image)"
@load="getImgDimension"
@error="onImageLoadError($event, image)"
/>
</NuxtLink>
<figcaption class="overlay overlay__top p-2">
<LicenseIcons :license="image.license" />
</figcaption>
<figcaption class="overlay overlay__bottom py-2 px-4">
<span class="caption font-semibold">{{ image.title }}</span>
</figcaption>
</figure>
</div>
</template>

<script>
import getProviderLogo from '~/utils/get-provider-logo'
const errorImage = require('~/assets/image_not_available_placeholder.png')
const minAspect = 3 / 4
const maxAspect = 16 / 9
const panaromaAspect = 21 / 9
const minRowWidth = 450
const toAbsolutePath = (url, prefix = 'https://') => {
if (url.indexOf('http://') >= 0 || url.indexOf('https://') >= 0) {
return url
}
return `${prefix}${url}`
}
export default {
name: 'ImageCell',
props: ['image', 'shouldContainImage'],
data() {
return {
widthBasis: minRowWidth / maxAspect,
imgHeight: this.image.height || 100,
imgWidth: this.image.width || 100,
}
},
computed: {
imageAspect() {
return this.imgWidth / this.imgHeight
},
containerAspect() {
if (this.imageAspect > maxAspect) return maxAspect
if (this.imageAspect < minAspect) return minAspect
return this.imageAspect
},
iPadding() {
if (this.imageAspect < minAspect) return (1 / minAspect) * 100
if (this.imageAspect > maxAspect) return (1 / maxAspect) * 100
return (1 / this.imageAspect) * 100
},
imageWidth() {
if (this.imageAspect < maxAspect) return 100
return (this.imageAspect / maxAspect) * 100
},
imageTop() {
if (this.imageAspect > minAspect) return 0
return (
((minAspect - this.imageAspect) /
(this.imageAspect * minAspect * minAspect)) *
-50
)
},
imageLeft() {
if (this.imageAspect < maxAspect) return 0
return ((this.imageAspect - maxAspect) / maxAspect) * -50
},
},
methods: {
getImageUrl(image) {
if (!image) {
return ''
}
const url = image.thumbnail || image.url
if (this.imageAspect > panaromaAspect) return toAbsolutePath(url)
return toAbsolutePath(url)
},
getImageForeignUrl(image) {
return toAbsolutePath(image.foreign_landing_url)
},
getProviderLogo(providerName) {
return getProviderLogo(providerName)
},
onGotoDetailPage(event, image) {
if (!event.metaKey && !event.ctrlKey) {
event.preventDefault()
const detailRoute = this.localeRoute({
name: 'photo-detail-page',
params: { id: image.id, location: window.scrollY },
})
this.$router.push(detailRoute)
}
},
onImageLoadError(event, image) {
const element = event.target
if (element.src !== image.url) {
element.src = image.url
} else {
element.src = errorImage
}
},
getImgDimension(e) {
this.imgHeight = e.target.naturalHeight
this.imgWidth = e.target.naturalWidth
},
},
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
.search-grid_image-ctr {
background: #ebece4;
display: block;
width: 100%;
}

.search-grid_item-container {
margin: 10px;
}

.search-grid_item {
position: relative;
width: 100%;
overflow: hidden;

i {
display: block;
}

a {
position: absolute;
vertical-align: bottom;
img {
width: 100%;
}
}

&:hover .overlay {
opacity: 1;
bottom: 0;
}
}

.overlay {
position: absolute;
opacity: 0;
transition: all 0.4s ease;
color: #fff;
display: block;
top: -100%;

&__top {
top: 0;
width: 100%;
height: 2rem;
}

&__bottom {
background-color: #000;
bottom: -100%;
top: auto;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
max-width: 100%;
}

// Show on touch devices
@media (hover: none) {
position: absolute;
opacity: 1;
bottom: 0;
}
}

.search-grid_item {
width: 100%;
position: relative;
display: block;
float: left;
flex: 0 0 auto;
flex-grow: 1;
cursor: pointer;
}

.search-grid_image {
margin: auto;
display: block;
}

.search-grid_image__fill {
width: 100%;
}
</style>
Loading

0 comments on commit 62d20fd

Please sign in to comment.