This repository has been archived by the owner on Feb 22, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ImageGrid component and use it for RelatedImages (#281)
* 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
1 parent
1c361e4
commit 62d20fd
Showing
18 changed files
with
715 additions
and
314 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Oops, something went wrong.