1
Fork 0

Rewrite photoGallery component to TS

This commit is contained in:
viktorstrate 2021-04-12 23:36:29 +02:00
parent f4e65eb58e
commit af0794dacf
No known key found for this signature in database
GPG Key ID: 3F855605109C1E8A
11 changed files with 169 additions and 139 deletions

View File

@ -1,9 +1,9 @@
import React, { useCallback, useState } from 'react'
import { useMutation, gql } from '@apollo/client'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import { Icon } from 'semantic-ui-react'
import { ProtectedImage } from './ProtectedMedia'
import { MediaType } from '../../../__generated__/globalTypes'
const markFavoriteMutation = gql`
mutation markMediaFavorite($mediaId: ID!, $favorite: Boolean!) {
@ -24,7 +24,7 @@ const MediaContainer = styled.div`
overflow: hidden;
`
const StyledPhoto = styled(ProtectedImage)`
const StyledPhoto = styled(ProtectedImage)<{ loaded: boolean }>`
height: 200px;
min-width: 100%;
position: relative;
@ -34,35 +34,30 @@ const StyledPhoto = styled(ProtectedImage)`
transition: opacity 300ms;
`
const LazyPhoto = photoProps => {
type LazyPhotoProps = {
src?: string
}
const LazyPhoto = (photoProps: LazyPhotoProps) => {
const [loaded, setLoaded] = useState(false)
const onLoad = useCallback(e => {
!e.target.dataset.src && setLoaded(true)
}, [])
return (
<StyledPhoto
{...photoProps}
lazyLoading
loaded={loaded ? 1 : 0}
onLoad={onLoad}
/>
<StyledPhoto {...photoProps} lazyLoading loaded={loaded} onLoad={onLoad} />
)
}
LazyPhoto.propTypes = {
src: PropTypes.string,
}
const PhotoOverlay = styled.div`
const PhotoOverlay = styled.div<{ active: boolean }>`
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
${props =>
props.active &&
${({ active }) =>
active &&
`
border: 4px solid rgba(65, 131, 196, 0.6);
@ -109,6 +104,24 @@ const VideoThumbnailIcon = styled(Icon)`
top: calc(50% - 13px);
`
type MediaThumbnailProps = {
media: {
id: string
type: MediaType
favorite?: boolean
thumbnail: null | {
url: string
width: number
height: number
}
}
onSelectImage(index: number): void
index: number
active: boolean
setPresenting(presenting: boolean): void
onFavorite(): void
}
export const MediaThumbnail = ({
media,
onSelectImage,
@ -116,16 +129,16 @@ export const MediaThumbnail = ({
active,
setPresenting,
onFavorite,
}) => {
}: MediaThumbnailProps) => {
const [markFavorite] = useMutation(markFavoriteMutation)
let heartIcon = null
if (typeof media.favorite == 'boolean') {
if (media.favorite !== undefined) {
heartIcon = (
<FavoriteIcon
favorite={media.favorite.toString()}
name={media.favorite ? 'heart' : 'heart outline'}
onClick={event => {
onClick={(event: MouseEvent) => {
event.stopPropagation()
const favorite = !media.favorite
markFavorite({
@ -148,7 +161,7 @@ export const MediaThumbnail = ({
}
let videoIcon = null
if (media.type == 'video') {
if (media.type == MediaType.Video) {
videoIcon = <VideoThumbnailIcon name="play" size="big" />
}
@ -163,11 +176,11 @@ export const MediaThumbnail = ({
<MediaContainer
key={media.id}
style={{
cursor: onSelectImage ? 'pointer' : null,
cursor: 'pointer',
minWidth: `clamp(124px, ${minWidth}px, 100% - 8px)`,
}}
onClick={() => {
onSelectImage && onSelectImage(index)
onSelectImage(index)
}}
>
<div
@ -176,7 +189,7 @@ export const MediaThumbnail = ({
height: `200px`,
}}
>
<LazyPhoto src={media.thumbnail && media.thumbnail.url} />
<LazyPhoto src={media.thumbnail?.url} />
</div>
<PhotoOverlay active={active}>
{videoIcon}
@ -192,15 +205,6 @@ export const MediaThumbnail = ({
)
}
MediaThumbnail.propTypes = {
media: PropTypes.object.isRequired,
onSelectImage: PropTypes.func,
index: PropTypes.number.isRequired,
active: PropTypes.bool.isRequired,
setPresenting: PropTypes.func.isRequired,
onFavorite: PropTypes.func,
}
export const PhotoThumbnail = styled.div`
flex-grow: 1;
height: 200px;

View File

@ -6,6 +6,7 @@ import PresentView from './presentView/PresentView'
import { SidebarContext, UpdateSidebarFn } from '../sidebar/Sidebar'
import MediaSidebar from '../sidebar/MediaSidebar'
import { useTranslation } from 'react-i18next'
import { PresentMediaProps_Media } from './presentView/PresentMedia'
const Gallery = styled.div`
display: flex;
@ -30,19 +31,21 @@ const ClearWrap = styled.div`
clear: both;
`
interface PhotoGalleryProps_Media extends PresentMediaProps_Media {
thumbnail: null | {
url: string
width: number
height: number
}
}
type PhotoGalleryProps = {
loading: boolean
media: {
id: string
title: string
thumbnail?: {
url: string
}
}[]
media: PhotoGalleryProps_Media[]
activeIndex: number
presenting: boolean
onSelectImage(index: number): void
setPresenting(callback: (presenting: boolean) => void): void
setPresenting(presenting: boolean): void
nextImage(): void
previousImage(): void
onFavorite(): void
@ -62,7 +65,15 @@ const PhotoGallery = ({
const { t } = useTranslation()
const { updateSidebar } = useContext(SidebarContext)
const activeImage = (media && activeIndex != -1 && media[activeIndex]) || {}
if (
media === undefined ||
activeIndex === -1 ||
media[activeIndex] === undefined
) {
return null
}
const activeImage = media[activeIndex]
const getPhotoElements = (updateSidebar: UpdateSidebarFn) => {
let photoElements = []

View File

@ -19,10 +19,12 @@ const getProtectedUrl = (url?: string) => {
}
export interface ProtectedImageProps
extends DetailedHTMLProps<
ImgHTMLAttributes<HTMLImageElement>,
HTMLImageElement
extends Omit<
DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>,
'src'
> {
src?: string
key?: string
lazyLoading?: boolean
}
@ -34,6 +36,7 @@ export interface ProtectedImageProps
*/
export const ProtectedImage = ({
src,
key,
lazyLoading,
...props
}: ProtectedImageProps) => {
@ -47,16 +50,17 @@ export const ProtectedImage = ({
lazyLoadProps.loading = 'lazy'
}
const imgSrc: string =
lazyLoading && !isNativeLazyLoadSupported
? placeholder
: getProtectedUrl(src) || placeholder
return (
<img
key={src}
key={key}
{...props}
{...lazyLoadProps}
src={
lazyLoading && !isNativeLazyLoadSupported
? placeholder
: getProtectedUrl(src)
}
src={imgSrc}
crossOrigin="use-credentials"
/>
)

View File

@ -1,53 +0,0 @@
import PropTypes from 'prop-types'
import React from 'react'
import styled from 'styled-components'
import { ProtectedImage, ProtectedVideo } from '../ProtectedMedia'
const StyledPhoto = styled(ProtectedImage)`
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
object-fit: contain;
object-position: center;
`
const StyledVideo = styled(ProtectedVideo)`
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
`
const PresentMedia = ({ media, imageLoaded, ...otherProps }) => {
if (media.type == 'photo') {
return (
<div {...otherProps}>
<StyledPhoto src={media.thumbnail?.url} />
<StyledPhoto
style={{ display: 'none' }}
src={media.highRes?.url}
onLoad={e => {
e.target.style.display = 'initial'
imageLoaded && imageLoaded()
}}
/>
</div>
)
}
if (media.type == 'video') {
return <StyledVideo media={media} />
}
throw new Error(`Unknown media type '${media.type}'`)
}
PresentMedia.propTypes = {
media: PropTypes.object.isRequired,
imageLoaded: PropTypes.func,
}
export default PresentMedia

View File

@ -0,0 +1,66 @@
import React from 'react'
import styled from 'styled-components'
import { MediaType } from '../../../../__generated__/globalTypes'
import {
ProtectedImage,
ProtectedVideo,
ProtectedVideoProps_Media,
} from '../ProtectedMedia'
const StyledPhoto = styled(ProtectedImage)`
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
object-fit: contain;
object-position: center;
`
const StyledVideo = styled(ProtectedVideo)`
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
`
export interface PresentMediaProps_Media extends ProtectedVideoProps_Media {
type: MediaType
highRes?: {
url: string
}
}
type PresentMediaProps = {
media: PresentMediaProps_Media
imageLoaded?(): void
}
const PresentMedia = ({
media,
imageLoaded,
...otherProps
}: PresentMediaProps) => {
switch (media.type) {
case MediaType.Photo:
return (
<div {...otherProps}>
<StyledPhoto src={media.thumbnail?.url} />
<StyledPhoto
style={{ display: 'none' }}
src={media.highRes?.url}
onLoad={e => {
const elem = e.target as HTMLImageElement
elem.style.display = 'initial'
imageLoaded && imageLoaded()
}}
/>
</div>
)
case MediaType.Video:
return <StyledVideo media={media} />
}
}
export default PresentMedia

View File

@ -1,7 +1,6 @@
import React, { useState, useRef, useEffect } from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import { debounce } from '../../../helpers/utils'
import { debounce, DebouncedFn } from '../../../helpers/utils'
import ExitIcon from './icons/Exit'
import NextIcon from './icons/Next'
@ -50,7 +49,7 @@ const ExitButton = styled(OverlayButton)`
top: 28px;
`
const NavigationButton = styled(OverlayButton)`
const NavigationButton = styled(OverlayButton)<{ float: 'left' | 'right' }>`
height: 80%;
width: 20%;
top: 10%;
@ -64,14 +63,21 @@ const NavigationButton = styled(OverlayButton)`
}
`
type PresentNavigationOverlayProps = {
children?: React.ReactChild
nextImage(): void
previousImage(): void
setPresenting(presenting: boolean): void
}
const PresentNavigationOverlay = ({
children,
nextImage,
previousImage,
setPresenting,
}) => {
}: PresentNavigationOverlayProps) => {
const [hide, setHide] = useState(true)
const onMouseMove = useRef(null)
const onMouseMove = useRef<null | DebouncedFn<() => void>>(null)
useEffect(() => {
onMouseMove.current = debounce(
@ -83,33 +89,33 @@ const PresentNavigationOverlay = ({
)
return () => {
onMouseMove.current.cancel()
onMouseMove.current?.cancel()
}
}, [])
return (
<StyledOverlayContainer
onMouseMove={() => {
onMouseMove.current()
onMouseMove.current && onMouseMove.current()
}}
>
{children}
<NavigationButton
className={hide && 'hide'}
className={hide ? 'hide' : undefined}
float="left"
onClick={() => previousImage()}
>
<PrevIcon />
</NavigationButton>
<NavigationButton
className={hide && 'hide'}
className={hide ? 'hide' : undefined}
float="right"
onClick={() => nextImage()}
>
<NextIcon />
</NavigationButton>
<ExitButton
className={hide && 'hide'}
className={hide ? 'hide' : undefined}
onClick={() => setPresenting(false)}
>
<ExitIcon />
@ -118,11 +124,4 @@ const PresentNavigationOverlay = ({
)
}
PresentNavigationOverlay.propTypes = {
children: PropTypes.element,
nextImage: PropTypes.func.isRequired,
previousImage: PropTypes.func.isRequired,
setPresenting: PropTypes.func.isRequired,
}
export default PresentNavigationOverlay

View File

@ -1,8 +1,7 @@
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import styled, { createGlobalStyle } from 'styled-components'
import PresentNavigationOverlay from './PresentNavigationOverlay'
import PresentMedia from './PresentMedia'
import PresentMedia, { PresentMediaProps_Media } from './PresentMedia'
const StyledContainer = styled.div`
position: fixed;
@ -21,6 +20,15 @@ const PreventScroll = createGlobalStyle`
}
`
type PresentViewProps = {
media: PresentMediaProps_Media
className?: string
imageLoaded?(): void
nextImage(): void
previousImage(): void
setPresenting(presenting: boolean): void
}
const PresentView = ({
className,
media,
@ -28,16 +36,16 @@ const PresentView = ({
nextImage,
previousImage,
setPresenting,
}) => {
}: PresentViewProps) => {
useEffect(() => {
const keyDownEvent = e => {
const keyDownEvent = (e: KeyboardEvent) => {
if (e.key == 'ArrowRight') {
nextImage && nextImage()
nextImage()
e.stopPropagation()
}
if (e.key == 'ArrowLeft') {
nextImage && previousImage()
previousImage()
e.stopPropagation()
}
@ -66,13 +74,4 @@ const PresentView = ({
)
}
PresentView.propTypes = {
media: PropTypes.object.isRequired,
imageLoaded: PropTypes.func,
className: PropTypes.string,
nextImage: PropTypes.func.isRequired,
previousImage: PropTypes.func.isRequired,
setPresenting: PropTypes.func.isRequired,
}
export default PresentView

View File

@ -1,6 +1,6 @@
import * as React from 'react'
function SvgExit(props) {
function SvgExit(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 36 36"

View File

@ -1,6 +1,6 @@
import * as React from 'react'
function SvgNext(props) {
function SvgNext(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 28 52"

View File

@ -1,6 +1,6 @@
import * as React from 'react'
function SvgPrevious(props) {
function SvgPrevious(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 28 52"

View File

@ -41,7 +41,7 @@
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */