Rewrite photoGallery component to TS
This commit is contained in:
parent
f4e65eb58e
commit
af0794dacf
|
@ -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;
|
|
@ -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 = []
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
|
@ -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"
|
|
@ -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"
|
|
@ -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. */
|
||||
|
|
Loading…
Reference in New Issue