Rewrite gallery to use reducer
This commit is contained in:
parent
6693cb589c
commit
a069eff0b2
|
@ -16,7 +16,7 @@ docker-compose.yml
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/ui/coverage
|
||||||
|
|
||||||
# building
|
# building
|
||||||
.cache/
|
.cache/
|
||||||
|
|
|
@ -43,7 +43,7 @@ module.exports = {
|
||||||
// parser: 'babel-eslint',
|
// parser: 'babel-eslint',
|
||||||
overrides: [
|
overrides: [
|
||||||
Object.assign(require('eslint-plugin-jest').configs.recommended, {
|
Object.assign(require('eslint-plugin-jest').configs.recommended, {
|
||||||
files: ['**/*.test.js'],
|
files: ['**/*.test.js', '**/*.test.ts', '**/*.test.tsx'],
|
||||||
env: { jest: true },
|
env: { jest: true },
|
||||||
plugins: ['jest', 'jest-dom'],
|
plugins: ['jest', 'jest-dom'],
|
||||||
rules: Object.assign(
|
rules: Object.assign(
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
"@testing-library/jest-dom": "^5.11.10",
|
"@testing-library/jest-dom": "^5.11.10",
|
||||||
"@testing-library/react": "^11.2.6",
|
"@testing-library/react": "^11.2.6",
|
||||||
"@types/geojson": "^7946.0.7",
|
"@types/geojson": "^7946.0.7",
|
||||||
"@types/jest": "^26.0.22",
|
"@types/jest": "^26.0.23",
|
||||||
"@types/mapbox-gl": "^2.1.1",
|
"@types/mapbox-gl": "^2.1.1",
|
||||||
"@types/react": "^17.0.3",
|
"@types/react": "^17.0.3",
|
||||||
"@types/react-dom": "^17.0.3",
|
"@types/react-dom": "^17.0.3",
|
||||||
|
@ -2789,9 +2789,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/jest": {
|
"node_modules/@types/jest": {
|
||||||
"version": "26.0.22",
|
"version": "26.0.23",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.22.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.23.tgz",
|
||||||
"integrity": "sha512-eeWwWjlqxvBxc4oQdkueW5OF/gtfSceKk4OnOAGlUSwS/liBRtZppbJuz1YkgbrbfGOoeBHun9fOvXnjNwrSOw==",
|
"integrity": "sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jest-diff": "^26.0.0",
|
"jest-diff": "^26.0.0",
|
||||||
|
@ -16438,9 +16438,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/jest": {
|
"@types/jest": {
|
||||||
"version": "26.0.22",
|
"version": "26.0.23",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.22.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.23.tgz",
|
||||||
"integrity": "sha512-eeWwWjlqxvBxc4oQdkueW5OF/gtfSceKk4OnOAGlUSwS/liBRtZppbJuz1YkgbrbfGOoeBHun9fOvXnjNwrSOw==",
|
"integrity": "sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"jest-diff": "^26.0.0",
|
"jest-diff": "^26.0.0",
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
"@testing-library/jest-dom": "^5.11.10",
|
"@testing-library/jest-dom": "^5.11.10",
|
||||||
"@testing-library/react": "^11.2.6",
|
"@testing-library/react": "^11.2.6",
|
||||||
"@types/geojson": "^7946.0.7",
|
"@types/geojson": "^7946.0.7",
|
||||||
"@types/jest": "^26.0.22",
|
"@types/jest": "^26.0.23",
|
||||||
"@types/mapbox-gl": "^2.1.1",
|
"@types/mapbox-gl": "^2.1.1",
|
||||||
"@types/react": "^17.0.3",
|
"@types/react": "^17.0.3",
|
||||||
"@types/react-dom": "^17.0.3",
|
"@types/react-dom": "^17.0.3",
|
||||||
|
@ -97,6 +97,10 @@
|
||||||
"arrowParens": "avoid"
|
"arrowParens": "avoid"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
"testMatch": [
|
||||||
|
"**/__tests__/**/*.+(ts|tsx|js)",
|
||||||
|
"**/?(*.)+(spec|test).+(ts|tsx|js)"
|
||||||
|
],
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"^.+\\.css$"
|
"^.+\\.css$"
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useReducer } from 'react'
|
||||||
import AlbumTitle from '../AlbumTitle'
|
import AlbumTitle from '../AlbumTitle'
|
||||||
import PhotoGallery from '../photoGallery/PhotoGallery'
|
import PhotoGallery from '../photoGallery/PhotoGallery'
|
||||||
import AlbumBoxes from './AlbumBoxes'
|
import AlbumBoxes from './AlbumBoxes'
|
||||||
import AlbumFilter from '../AlbumFilter'
|
import AlbumFilter from '../AlbumFilter'
|
||||||
import { albumQuery_album } from '../../Pages/AlbumPage/__generated__/albumQuery'
|
import { albumQuery_album } from '../../Pages/AlbumPage/__generated__/albumQuery'
|
||||||
import { OrderDirection } from '../../../__generated__/globalTypes'
|
import { OrderDirection } from '../../../__generated__/globalTypes'
|
||||||
|
import { photoGalleryReducer } from '../photoGallery/photoGalleryReducer'
|
||||||
|
|
||||||
type AlbumGalleryProps = {
|
type AlbumGalleryProps = {
|
||||||
album?: albumQuery_album
|
album?: albumQuery_album
|
||||||
|
@ -29,71 +30,49 @@ const AlbumGallery = React.forwardRef(
|
||||||
setOrdering,
|
setOrdering,
|
||||||
ordering,
|
ordering,
|
||||||
onlyFavorites = false,
|
onlyFavorites = false,
|
||||||
onFavorite,
|
|
||||||
}: AlbumGalleryProps,
|
}: AlbumGalleryProps,
|
||||||
ref: React.ForwardedRef<HTMLDivElement>
|
ref: React.ForwardedRef<HTMLDivElement>
|
||||||
) => {
|
) => {
|
||||||
type ImageStateType = {
|
// const [imageState, setImageState] = useState<ImageStateType>({
|
||||||
activeImage: number
|
// activeImage: -1,
|
||||||
presenting: boolean
|
// presenting: false,
|
||||||
}
|
// })
|
||||||
|
|
||||||
const [imageState, setImageState] = useState<ImageStateType>({
|
const [mediaState, dispatchMedia] = useReducer(photoGalleryReducer, {
|
||||||
activeImage: -1,
|
|
||||||
presenting: false,
|
presenting: false,
|
||||||
|
activeIndex: -1,
|
||||||
|
media: album?.media || [],
|
||||||
})
|
})
|
||||||
|
|
||||||
const setPresenting = (presenting: boolean) =>
|
// const setPresentingWithHistory = (presenting: boolean) => {
|
||||||
setImageState(state => ({ ...state, presenting }))
|
// setPresenting(presenting)
|
||||||
|
// if (presenting) {
|
||||||
|
// history.pushState({ imageState }, '')
|
||||||
|
// } else {
|
||||||
|
// history.back()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
const setPresentingWithHistory = (presenting: boolean) => {
|
// const updateHistory = (imageState: ImageStateType) => {
|
||||||
setPresenting(presenting)
|
// history.replaceState({ imageState }, '')
|
||||||
if (presenting) {
|
// return imageState
|
||||||
history.pushState({ imageState }, '')
|
// }
|
||||||
} else {
|
|
||||||
history.back()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateHistory = (imageState: ImageStateType) => {
|
// useEffect(() => {
|
||||||
history.replaceState({ imageState }, '')
|
// const updateImageState = (event: PopStateEvent) => {
|
||||||
return imageState
|
// setImageState(event.state.imageState)
|
||||||
}
|
// }
|
||||||
|
|
||||||
const setActiveImage = (activeImage: number) => {
|
// window.addEventListener('popstate', updateImageState)
|
||||||
setImageState(state => updateHistory({ ...state, activeImage }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextImage = () => {
|
// return () => {
|
||||||
if (album === undefined) return
|
// window.removeEventListener('popstate', updateImageState)
|
||||||
setActiveImage((imageState.activeImage + 1) % album.media.length)
|
// }
|
||||||
}
|
// }, [imageState])
|
||||||
|
|
||||||
const previousImage = () => {
|
// useEffect(() => {
|
||||||
if (album === undefined) return
|
// setActiveImage(-1)
|
||||||
|
// }, [album])
|
||||||
if (imageState.activeImage <= 0) {
|
|
||||||
setActiveImage(album.media.length - 1)
|
|
||||||
} else {
|
|
||||||
setActiveImage(imageState.activeImage - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const updateImageState = (event: PopStateEvent) => {
|
|
||||||
setImageState(event.state.imageState)
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('popstate', updateImageState)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('popstate', updateImageState)
|
|
||||||
}
|
|
||||||
}, [imageState])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setActiveImage(-1)
|
|
||||||
}, [album])
|
|
||||||
|
|
||||||
let subAlbumElement = null
|
let subAlbumElement = null
|
||||||
|
|
||||||
|
@ -135,16 +114,8 @@ const AlbumGallery = React.forwardRef(
|
||||||
}
|
}
|
||||||
<PhotoGallery
|
<PhotoGallery
|
||||||
loading={loading}
|
loading={loading}
|
||||||
media={album?.media || []}
|
mediaState={mediaState}
|
||||||
activeIndex={imageState.activeImage}
|
dispatchMedia={dispatchMedia}
|
||||||
presenting={imageState.presenting}
|
|
||||||
onSelectImage={index => {
|
|
||||||
setActiveImage(index)
|
|
||||||
}}
|
|
||||||
onFavorite={onFavorite}
|
|
||||||
setPresenting={setPresentingWithHistory}
|
|
||||||
nextImage={nextImage}
|
|
||||||
previousImage={previousImage}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,22 +1,8 @@
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { useMutation, gql } from '@apollo/client'
|
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { Icon } from 'semantic-ui-react'
|
import { Icon } from 'semantic-ui-react'
|
||||||
import { ProtectedImage } from './ProtectedMedia'
|
import { ProtectedImage } from './ProtectedMedia'
|
||||||
import { MediaType } from '../../../__generated__/globalTypes'
|
import { MediaType } from '../../../__generated__/globalTypes'
|
||||||
import {
|
|
||||||
markMediaFavorite,
|
|
||||||
markMediaFavoriteVariables,
|
|
||||||
} from './__generated__/markMediaFavorite'
|
|
||||||
|
|
||||||
const markFavoriteMutation = gql`
|
|
||||||
mutation markMediaFavorite($mediaId: ID!, $favorite: Boolean!) {
|
|
||||||
favoriteMedia(mediaId: $mediaId, favorite: $favorite) {
|
|
||||||
id
|
|
||||||
favorite
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const MediaContainer = styled.div`
|
const MediaContainer = styled.div`
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -119,26 +105,19 @@ type MediaThumbnailProps = {
|
||||||
height: number
|
height: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onSelectImage(index: number): void
|
|
||||||
index: number
|
|
||||||
active: boolean
|
active: boolean
|
||||||
setPresenting(presenting: boolean): void
|
selectImage(): void
|
||||||
onFavorite?(): void
|
clickPresent(): void
|
||||||
|
clickFavorite(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MediaThumbnail = ({
|
export const MediaThumbnail = ({
|
||||||
media,
|
media,
|
||||||
onSelectImage,
|
|
||||||
index,
|
|
||||||
active,
|
active,
|
||||||
setPresenting,
|
selectImage,
|
||||||
onFavorite,
|
clickPresent,
|
||||||
|
clickFavorite,
|
||||||
}: MediaThumbnailProps) => {
|
}: MediaThumbnailProps) => {
|
||||||
const [markFavorite] = useMutation<
|
|
||||||
markMediaFavorite,
|
|
||||||
markMediaFavoriteVariables
|
|
||||||
>(markFavoriteMutation)
|
|
||||||
|
|
||||||
let heartIcon = null
|
let heartIcon = null
|
||||||
if (media.favorite !== undefined) {
|
if (media.favorite !== undefined) {
|
||||||
heartIcon = (
|
heartIcon = (
|
||||||
|
@ -147,21 +126,7 @@ export const MediaThumbnail = ({
|
||||||
name={media.favorite ? 'heart' : 'heart outline'}
|
name={media.favorite ? 'heart' : 'heart outline'}
|
||||||
onClick={(event: MouseEvent) => {
|
onClick={(event: MouseEvent) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
const favorite = !media.favorite
|
clickFavorite()
|
||||||
markFavorite({
|
|
||||||
variables: {
|
|
||||||
mediaId: media.id,
|
|
||||||
favorite: favorite,
|
|
||||||
},
|
|
||||||
optimisticResponse: {
|
|
||||||
favoriteMedia: {
|
|
||||||
id: media.id,
|
|
||||||
favorite: favorite,
|
|
||||||
__typename: 'Media',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
onFavorite && onFavorite()
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -187,7 +152,7 @@ export const MediaThumbnail = ({
|
||||||
minWidth: `clamp(124px, ${minWidth}px, 100% - 8px)`,
|
minWidth: `clamp(124px, ${minWidth}px, 100% - 8px)`,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSelectImage(index)
|
selectImage()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -203,7 +168,7 @@ export const MediaThumbnail = ({
|
||||||
<HoverIcon
|
<HoverIcon
|
||||||
name="expand"
|
name="expand"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPresenting(true)
|
clickPresent()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{heartIcon}
|
{heartIcon}
|
||||||
|
|
|
@ -4,10 +4,28 @@ import { Loader } from 'semantic-ui-react'
|
||||||
import { MediaThumbnail, PhotoThumbnail } from './MediaThumbnail'
|
import { MediaThumbnail, PhotoThumbnail } from './MediaThumbnail'
|
||||||
import PresentView from './presentView/PresentView'
|
import PresentView from './presentView/PresentView'
|
||||||
import { SidebarContext } from '../sidebar/Sidebar'
|
import { SidebarContext } from '../sidebar/Sidebar'
|
||||||
import MediaSidebar from '../sidebar/MediaSidebar'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { PresentMediaProps_Media } from './presentView/PresentMedia'
|
import { PresentMediaProps_Media } from './presentView/PresentMedia'
|
||||||
import { sidebarPhoto_media_thumbnail } from '../sidebar/__generated__/sidebarPhoto'
|
import { sidebarPhoto_media_thumbnail } from '../sidebar/__generated__/sidebarPhoto'
|
||||||
|
import {
|
||||||
|
PhotoGalleryAction,
|
||||||
|
PhotoGalleryState,
|
||||||
|
selectImageAction,
|
||||||
|
} from './photoGalleryReducer'
|
||||||
|
import { gql, useMutation } from '@apollo/client'
|
||||||
|
import {
|
||||||
|
markMediaFavorite,
|
||||||
|
markMediaFavoriteVariables,
|
||||||
|
} from './__generated__/markMediaFavorite'
|
||||||
|
|
||||||
|
const markFavoriteMutation = gql`
|
||||||
|
mutation markMediaFavorite($mediaId: ID!, $favorite: Boolean!) {
|
||||||
|
favoriteMedia(mediaId: $mediaId, favorite: $favorite) {
|
||||||
|
id
|
||||||
|
favorite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const Gallery = styled.div`
|
const Gallery = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -32,37 +50,32 @@ const ClearWrap = styled.div`
|
||||||
clear: both;
|
clear: both;
|
||||||
`
|
`
|
||||||
|
|
||||||
interface PhotoGalleryProps_Media extends PresentMediaProps_Media {
|
export interface PhotoGalleryProps_Media extends PresentMediaProps_Media {
|
||||||
thumbnail: sidebarPhoto_media_thumbnail | null
|
thumbnail: sidebarPhoto_media_thumbnail | null
|
||||||
|
favorite?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type PhotoGalleryProps = {
|
type PhotoGalleryProps = {
|
||||||
loading: boolean
|
loading: boolean
|
||||||
media: PhotoGalleryProps_Media[]
|
mediaState: PhotoGalleryState
|
||||||
activeIndex: number
|
dispatchMedia: React.Dispatch<PhotoGalleryAction>
|
||||||
presenting: boolean
|
|
||||||
onSelectImage(index: number): void
|
|
||||||
setPresenting(presenting: boolean): void
|
|
||||||
nextImage(): void
|
|
||||||
previousImage(): void
|
|
||||||
onFavorite?(): void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PhotoGallery = ({
|
const PhotoGallery = ({
|
||||||
activeIndex = -1,
|
mediaState,
|
||||||
media,
|
|
||||||
loading,
|
loading,
|
||||||
onSelectImage,
|
dispatchMedia,
|
||||||
presenting,
|
|
||||||
setPresenting,
|
|
||||||
nextImage,
|
|
||||||
previousImage,
|
|
||||||
onFavorite,
|
|
||||||
}: PhotoGalleryProps) => {
|
}: PhotoGalleryProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { updateSidebar } = useContext(SidebarContext)
|
const { updateSidebar } = useContext(SidebarContext)
|
||||||
|
|
||||||
const activeImage: PhotoGalleryProps_Media | undefined = media[activeIndex]
|
const [markFavorite] = useMutation<
|
||||||
|
markMediaFavorite,
|
||||||
|
markMediaFavoriteVariables
|
||||||
|
>(markFavoriteMutation)
|
||||||
|
|
||||||
|
const { media, activeIndex, presenting } = mediaState
|
||||||
|
|
||||||
let photoElements = []
|
let photoElements = []
|
||||||
if (media) {
|
if (media) {
|
||||||
|
@ -73,14 +86,36 @@ const PhotoGallery = ({
|
||||||
<MediaThumbnail
|
<MediaThumbnail
|
||||||
key={media.id}
|
key={media.id}
|
||||||
media={media}
|
media={media}
|
||||||
onSelectImage={index => {
|
|
||||||
updateSidebar(<MediaSidebar media={media} />)
|
|
||||||
onSelectImage(index)
|
|
||||||
}}
|
|
||||||
onFavorite={onFavorite}
|
|
||||||
setPresenting={setPresenting}
|
|
||||||
index={index}
|
|
||||||
active={active}
|
active={active}
|
||||||
|
selectImage={() => {
|
||||||
|
selectImageAction({
|
||||||
|
index,
|
||||||
|
mediaState,
|
||||||
|
dispatchMedia,
|
||||||
|
updateSidebar,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
clickFavorite={() => {
|
||||||
|
markFavorite({
|
||||||
|
variables: {
|
||||||
|
mediaId: media.id,
|
||||||
|
favorite: !media.favorite,
|
||||||
|
},
|
||||||
|
optimisticResponse: {
|
||||||
|
favoriteMedia: {
|
||||||
|
id: media.id,
|
||||||
|
favorite: !media.favorite,
|
||||||
|
__typename: 'Media',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
clickPresent={() => {
|
||||||
|
dispatchMedia({
|
||||||
|
type: 'setPresenting',
|
||||||
|
presenting: true,
|
||||||
|
})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -100,10 +135,7 @@ const PhotoGallery = ({
|
||||||
<PhotoFiller />
|
<PhotoFiller />
|
||||||
</Gallery>
|
</Gallery>
|
||||||
{presenting && (
|
{presenting && (
|
||||||
<PresentView
|
<PresentView mediaState={mediaState} dispatchMedia={dispatchMedia} />
|
||||||
media={activeImage}
|
|
||||||
{...{ nextImage, previousImage, setPresenting }}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</ClearWrap>
|
</ClearWrap>
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { photoGalleryReducer, PhotoGalleryState } from './photoGalleryReducer'
|
||||||
|
import { MediaType } from '../../../__generated__/globalTypes'
|
||||||
|
|
||||||
|
describe('photo gallery reducer', () => {
|
||||||
|
const defaultState: PhotoGalleryState = {
|
||||||
|
presenting: false,
|
||||||
|
activeIndex: 0,
|
||||||
|
media: [
|
||||||
|
{
|
||||||
|
__typename: 'Media',
|
||||||
|
id: '1',
|
||||||
|
highRes: null,
|
||||||
|
thumbnail: null,
|
||||||
|
type: MediaType.Photo,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Media',
|
||||||
|
id: '2',
|
||||||
|
highRes: null,
|
||||||
|
thumbnail: null,
|
||||||
|
type: MediaType.Photo,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Media',
|
||||||
|
id: '3',
|
||||||
|
highRes: null,
|
||||||
|
thumbnail: null,
|
||||||
|
type: MediaType.Photo,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
test('next image', () => {
|
||||||
|
expect(photoGalleryReducer(defaultState, { type: 'nextImage' })).toEqual({
|
||||||
|
...defaultState,
|
||||||
|
activeIndex: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
photoGalleryReducer(
|
||||||
|
{ ...defaultState, activeIndex: 2 },
|
||||||
|
{ type: 'nextImage' }
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
...defaultState,
|
||||||
|
activeIndex: 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('previous image', () => {
|
||||||
|
expect(
|
||||||
|
photoGalleryReducer(defaultState, { type: 'previousImage' })
|
||||||
|
).toEqual({
|
||||||
|
...defaultState,
|
||||||
|
activeIndex: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
photoGalleryReducer(
|
||||||
|
{ ...defaultState, activeIndex: 2 },
|
||||||
|
{ type: 'previousImage' }
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
...defaultState,
|
||||||
|
activeIndex: 1,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('select image', () => {
|
||||||
|
expect(
|
||||||
|
photoGalleryReducer(defaultState, { type: 'selectImage', index: 1 })
|
||||||
|
).toEqual({
|
||||||
|
...defaultState,
|
||||||
|
activeIndex: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
photoGalleryReducer(defaultState, { type: 'selectImage', index: 100 })
|
||||||
|
).toEqual({
|
||||||
|
...defaultState,
|
||||||
|
activeIndex: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
photoGalleryReducer(defaultState, { type: 'selectImage', index: -5 })
|
||||||
|
).toEqual({
|
||||||
|
...defaultState,
|
||||||
|
activeIndex: 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,74 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { UpdateSidebarFn } from '../sidebar/Sidebar'
|
||||||
|
import { PhotoGalleryProps_Media } from './PhotoGallery'
|
||||||
|
import MediaSidebar from '../sidebar/MediaSidebar'
|
||||||
|
|
||||||
|
export type PhotoGalleryState = {
|
||||||
|
presenting: boolean
|
||||||
|
activeIndex: number
|
||||||
|
media: PhotoGalleryProps_Media[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PhotoGalleryAction =
|
||||||
|
| { type: 'nextImage' }
|
||||||
|
| { type: 'previousImage' }
|
||||||
|
| { type: 'setPresenting'; presenting: boolean }
|
||||||
|
| { type: 'selectImage'; index: number }
|
||||||
|
|
||||||
|
export function photoGalleryReducer(
|
||||||
|
state: PhotoGalleryState,
|
||||||
|
action: PhotoGalleryAction
|
||||||
|
): PhotoGalleryState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'nextImage':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
activeIndex: (state.activeIndex + 1) % state.media.length,
|
||||||
|
}
|
||||||
|
case 'previousImage':
|
||||||
|
if (state.activeIndex <= 0) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
activeIndex: state.media.length - 1,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
activeIndex: state.activeIndex - 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'setPresenting':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
presenting: action.presenting,
|
||||||
|
}
|
||||||
|
case 'selectImage':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
activeIndex: Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(state.media.length - 1, action.index)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectImageAction = ({
|
||||||
|
index,
|
||||||
|
mediaState,
|
||||||
|
dispatchMedia,
|
||||||
|
updateSidebar,
|
||||||
|
}: {
|
||||||
|
index: number
|
||||||
|
mediaState: PhotoGalleryState
|
||||||
|
dispatchMedia: React.Dispatch<PhotoGalleryAction>
|
||||||
|
updateSidebar: UpdateSidebarFn
|
||||||
|
}) => {
|
||||||
|
updateSidebar(
|
||||||
|
<MediaSidebar media={mediaState.media[mediaState.activeIndex]} />
|
||||||
|
)
|
||||||
|
dispatchMedia({
|
||||||
|
type: 'selectImage',
|
||||||
|
index,
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState, useRef, useEffect } from 'react'
|
import React, { useState, useRef, useEffect } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { debounce, DebouncedFn } from '../../../helpers/utils'
|
import { debounce, DebouncedFn } from '../../../helpers/utils'
|
||||||
|
import { PhotoGalleryAction } from '../photoGalleryReducer'
|
||||||
|
|
||||||
import ExitIcon from './icons/Exit'
|
import ExitIcon from './icons/Exit'
|
||||||
import NextIcon from './icons/Next'
|
import NextIcon from './icons/Next'
|
||||||
|
@ -65,16 +66,12 @@ const NavigationButton = styled(OverlayButton)<{ float: 'left' | 'right' }>`
|
||||||
|
|
||||||
type PresentNavigationOverlayProps = {
|
type PresentNavigationOverlayProps = {
|
||||||
children?: React.ReactChild
|
children?: React.ReactChild
|
||||||
nextImage(): void
|
dispatchMedia: React.Dispatch<PhotoGalleryAction>
|
||||||
previousImage(): void
|
|
||||||
setPresenting(presenting: boolean): void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PresentNavigationOverlay = ({
|
const PresentNavigationOverlay = ({
|
||||||
children,
|
children,
|
||||||
nextImage,
|
dispatchMedia,
|
||||||
previousImage,
|
|
||||||
setPresenting,
|
|
||||||
}: PresentNavigationOverlayProps) => {
|
}: PresentNavigationOverlayProps) => {
|
||||||
const [hide, setHide] = useState(true)
|
const [hide, setHide] = useState(true)
|
||||||
const onMouseMove = useRef<null | DebouncedFn<() => void>>(null)
|
const onMouseMove = useRef<null | DebouncedFn<() => void>>(null)
|
||||||
|
@ -103,20 +100,22 @@ const PresentNavigationOverlay = ({
|
||||||
<NavigationButton
|
<NavigationButton
|
||||||
className={hide ? 'hide' : undefined}
|
className={hide ? 'hide' : undefined}
|
||||||
float="left"
|
float="left"
|
||||||
onClick={() => previousImage()}
|
onClick={() => dispatchMedia({ type: 'previousImage' })}
|
||||||
>
|
>
|
||||||
<PrevIcon />
|
<PrevIcon />
|
||||||
</NavigationButton>
|
</NavigationButton>
|
||||||
<NavigationButton
|
<NavigationButton
|
||||||
className={hide ? 'hide' : undefined}
|
className={hide ? 'hide' : undefined}
|
||||||
float="right"
|
float="right"
|
||||||
onClick={() => nextImage()}
|
onClick={() => dispatchMedia({ type: 'nextImage' })}
|
||||||
>
|
>
|
||||||
<NextIcon />
|
<NextIcon />
|
||||||
</NavigationButton>
|
</NavigationButton>
|
||||||
<ExitButton
|
<ExitButton
|
||||||
className={hide ? 'hide' : undefined}
|
className={hide ? 'hide' : undefined}
|
||||||
onClick={() => setPresenting(false)}
|
onClick={() =>
|
||||||
|
dispatchMedia({ type: 'setPresenting', presenting: false })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<ExitIcon />
|
<ExitIcon />
|
||||||
</ExitButton>
|
</ExitButton>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import styled, { createGlobalStyle } from 'styled-components'
|
import styled, { createGlobalStyle } from 'styled-components'
|
||||||
import PresentNavigationOverlay from './PresentNavigationOverlay'
|
import PresentNavigationOverlay from './PresentNavigationOverlay'
|
||||||
import PresentMedia, { PresentMediaProps_Media } from './PresentMedia'
|
import PresentMedia from './PresentMedia'
|
||||||
|
import { PhotoGalleryAction, PhotoGalleryState } from '../photoGalleryReducer'
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -21,37 +22,33 @@ const PreventScroll = createGlobalStyle`
|
||||||
`
|
`
|
||||||
|
|
||||||
type PresentViewProps = {
|
type PresentViewProps = {
|
||||||
media: PresentMediaProps_Media
|
|
||||||
className?: string
|
className?: string
|
||||||
imageLoaded?(): void
|
imageLoaded?(): void
|
||||||
nextImage(): void
|
mediaState: PhotoGalleryState
|
||||||
previousImage(): void
|
dispatchMedia: React.Dispatch<PhotoGalleryAction>
|
||||||
setPresenting(presenting: boolean): void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PresentView = ({
|
const PresentView = ({
|
||||||
className,
|
className,
|
||||||
media,
|
|
||||||
imageLoaded,
|
imageLoaded,
|
||||||
nextImage,
|
mediaState,
|
||||||
previousImage,
|
dispatchMedia,
|
||||||
setPresenting,
|
|
||||||
}: PresentViewProps) => {
|
}: PresentViewProps) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const keyDownEvent = (e: KeyboardEvent) => {
|
const keyDownEvent = (e: KeyboardEvent) => {
|
||||||
if (e.key == 'ArrowRight') {
|
if (e.key == 'ArrowRight') {
|
||||||
nextImage()
|
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
dispatchMedia({ type: 'nextImage' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key == 'ArrowLeft') {
|
if (e.key == 'ArrowLeft') {
|
||||||
previousImage()
|
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
dispatchMedia({ type: 'previousImage' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key == 'Escape') {
|
if (e.key == 'Escape') {
|
||||||
setPresenting(false)
|
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
dispatchMedia({ type: 'setPresenting', presenting: false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,10 +62,11 @@ const PresentView = ({
|
||||||
return (
|
return (
|
||||||
<StyledContainer {...className}>
|
<StyledContainer {...className}>
|
||||||
<PreventScroll />
|
<PreventScroll />
|
||||||
<PresentNavigationOverlay
|
<PresentNavigationOverlay dispatchMedia={dispatchMedia}>
|
||||||
{...{ nextImage, previousImage, setPresenting }}
|
<PresentMedia
|
||||||
>
|
media={mediaState.media[mediaState.activeIndex]}
|
||||||
<PresentMedia media={media} imageLoaded={imageLoaded} />
|
imageLoaded={imageLoaded}
|
||||||
|
/>
|
||||||
</PresentNavigationOverlay>
|
</PresentNavigationOverlay>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
)
|
)
|
||||||
|
|
|
@ -52,9 +52,10 @@
|
||||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
"typeRoots": [
|
"typeRoots": [
|
||||||
|
"node_modules/@types",
|
||||||
"./src/@types/"
|
"./src/@types/"
|
||||||
] /* List of folders to include type definitions from. */,
|
] /* List of folders to include type definitions from. */,
|
||||||
// "types": [], /* Type declaration files to be included in compilation. */
|
"types": ["jest"], /* Type declaration files to be included in compilation. */
|
||||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
||||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
|
|
Loading…
Reference in New Issue