1
Fork 0

Rewrite SharePage to typescript

This commit is contained in:
viktorstrate 2021-04-13 15:23:06 +02:00
parent 9c939fd06c
commit 34a411be9d
No known key found for this signature in database
GPG Key ID: 3F855605109C1E8A
12 changed files with 151 additions and 132 deletions

View File

@ -1,4 +1,3 @@
import PropTypes from 'prop-types'
import React from 'react'
import Layout from '../../Layout'
import AlbumGallery from '../../components/albumGallery/AlbumGallery'
@ -73,7 +72,13 @@ const AlbumSharePageWrapper = styled.div`
height: 100%;
`
const AlbumSharePage = ({ albumID, token, password }) => {
type AlbumSharePageProps = {
albumID: string
token: string
password: string | null
}
const AlbumSharePage = ({ albumID, token, password }: AlbumSharePageProps) => {
const { t } = useTranslation()
const { data, loading, error } = useQuery(SHARE_ALBUM_QUERY, {
variables: {
@ -86,11 +91,11 @@ const AlbumSharePage = ({ albumID, token, password }) => {
})
if (error) {
return error.message
return <div>{error.message}</div>
}
if (loading) {
return t('general.loading.default', 'Loading...')
return <div>{t('general.loading.default', 'Loading...')}</div>
}
const album = data.album
@ -111,10 +116,4 @@ const AlbumSharePage = ({ albumID, token, password }) => {
)
}
AlbumSharePage.propTypes = {
albumID: PropTypes.string.isRequired,
token: PropTypes.string.isRequired,
password: PropTypes.string,
}
export default AlbumSharePage

View File

@ -1,58 +0,0 @@
import React, { useContext, useEffect } from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import Layout from '../../Layout'
import {
ProtectedImage,
ProtectedVideo,
} from '../../components/photoGallery/ProtectedMedia'
import { SidebarContext } from '../../components/sidebar/Sidebar'
import MediaSidebar from '../../components/sidebar/MediaSidebar'
const DisplayPhoto = styled(ProtectedImage)`
width: 100%;
max-height: calc(80vh);
object-fit: contain;
`
const DisplayVideo = styled(ProtectedVideo)`
width: 100%;
max-height: calc(80vh);
`
const MediaView = ({ media }) => {
const { updateSidebar } = useContext(SidebarContext)
useEffect(() => {
updateSidebar(<MediaSidebar media={media} hidePreview />)
}, [media])
if (media.type == 'photo') {
return <DisplayPhoto src={media.highRes.url} />
}
if (media.type == 'video') {
return <DisplayVideo media={media} />
}
throw new Error(`Unsupported media type: ${media.type}`)
}
MediaView.propTypes = {
media: PropTypes.object.isRequired,
}
const MediaSharePage = ({ media }) => (
<Layout>
<div data-testid="MediaSharePage">
<h1>{media.title}</h1>
<MediaView media={media} />
</div>
</Layout>
)
MediaSharePage.propTypes = {
media: PropTypes.object.isRequired,
}
export default MediaSharePage

View File

@ -0,0 +1,61 @@
import React, { useContext, useEffect } from 'react'
import styled from 'styled-components'
import Layout from '../../Layout'
import {
ProtectedImage,
ProtectedVideo,
} from '../../components/photoGallery/ProtectedMedia'
import { SidebarContext } from '../../components/sidebar/Sidebar'
import MediaSidebar from '../../components/sidebar/MediaSidebar'
import { useTranslation } from 'react-i18next'
import { SharePageToken_shareToken_media } from './__generated__/SharePageToken'
import { MediaType } from '../../../__generated__/globalTypes'
const DisplayPhoto = styled(ProtectedImage)`
width: 100%;
max-height: calc(80vh);
object-fit: contain;
`
const DisplayVideo = styled(ProtectedVideo)`
width: 100%;
max-height: calc(80vh);
`
type MediaViewProps = {
media: SharePageToken_shareToken_media
}
const MediaView = ({ media }: MediaViewProps) => {
const { updateSidebar } = useContext(SidebarContext)
useEffect(() => {
updateSidebar(<MediaSidebar media={media} hidePreview />)
}, [media])
switch (media.type) {
case MediaType.Photo:
return <DisplayPhoto src={media.highRes?.url} />
case MediaType.Video:
return <DisplayVideo media={media} />
}
}
type MediaSharePageType = {
media: SharePageToken_shareToken_media
}
const MediaSharePage = ({ media }: MediaSharePageType) => {
const { t } = useTranslation()
return (
<Layout title={t('share_page.media.title', 'Shared media')}>
<div data-testid="MediaSharePage">
<h1>{media.title}</h1>
<MediaView media={media} />
</div>
</Layout>
)
}
export default MediaSharePage

View File

@ -82,7 +82,7 @@ describe('load correct share page, based on graphql query', () => {
media: {
id: '1',
title: 'shared_image.jpg',
type: 'photo',
type: 'Photo',
highRes: {
url: 'https://example.com/shared_image.jpg',
},

View File

@ -1,8 +1,7 @@
import PropTypes from 'prop-types'
import React, { useState } from 'react'
import { useQuery, gql } from '@apollo/client'
import { Route, Switch } from 'react-router-dom'
import RouterProps from 'react-router-prop-types'
import { match as MatchType, Route, Switch } from 'react-router-dom'
import { Form, Header, Icon, Input, Message } from 'semantic-ui-react'
import styled from 'styled-components'
import {
@ -71,7 +70,7 @@ export const VALIDATE_TOKEN_PASSWORD_QUERY = gql`
}
`
const AuthorizedTokenRoute = ({ match }) => {
const AuthorizedTokenRoute = ({ match }: MatchProps<TokenRouteMatch>) => {
const { t } = useTranslation()
const token = match.params.token
@ -84,11 +83,11 @@ const AuthorizedTokenRoute = ({ match }) => {
},
})
if (error) return error.message
if (loading) return 'Loading...'
if (error) return <div>{error.message}</div>
if (loading) return <div>{t('general.loading.default', 'Loading...')}</div>
if (data.shareToken.album) {
const SharedSubAlbumPage = ({ match }) => {
const SharedSubAlbumPage = ({ match }: MatchProps<SubalbumRouteMatch>) => {
return (
<AlbumSharePage
albumID={match.params.subAlbum}
@ -136,10 +135,15 @@ const MessageContainer = styled.div`
margin: 100px auto 0;
`
type ProtectedTokenEnterPasswordProps = {
refetchWithPassword(password: string): void
loading: boolean
}
const ProtectedTokenEnterPassword = ({
refetchWithPassword,
loading = false,
}) => {
}: ProtectedTokenEnterPasswordProps) => {
const { t } = useTranslation()
const [passwordValue, setPasswordValue] = useState('')
@ -178,7 +182,9 @@ const ProtectedTokenEnterPassword = ({
<Input
loading={loading}
disabled={loading}
onKeyUp={event => event.key == 'Enter' && onSubmit()}
onKeyUp={(event: KeyboardEvent) =>
event.key == 'Enter' && onSubmit()
}
onChange={e => setPasswordValue(e.target.value)}
placeholder={t('login_page.field.password', 'Password')}
type="password"
@ -191,12 +197,19 @@ const ProtectedTokenEnterPassword = ({
)
}
ProtectedTokenEnterPassword.propTypes = {
refetchWithPassword: PropTypes.func.isRequired,
loading: PropTypes.bool,
interface TokenRouteMatch {
token: string
}
const TokenRoute = ({ match }) => {
interface SubalbumRouteMatch extends TokenRouteMatch {
subAlbum: string
}
interface MatchProps<Route> {
match: MatchType<Route>
}
const TokenRoute = ({ match }: MatchProps<TokenRouteMatch>) => {
const { t } = useTranslation()
const token = match.params.token
@ -227,13 +240,12 @@ const TokenRoute = ({ match }) => {
)
}
return error.message
return <div>{error.message}</div>
}
if (data && data.shareTokenValidatePassword == false) {
return (
<ProtectedTokenEnterPassword
match={match}
refetchWithPassword={password => {
saveSharePassword(token, password)
refetch({ token, password })
@ -243,22 +255,18 @@ const TokenRoute = ({ match }) => {
)
}
if (loading) return t('general.loading.default', 'Loading...')
if (loading) return <div>{t('general.loading.default', 'Loading...')}</div>
return <AuthorizedTokenRoute match={match} />
}
TokenRoute.propTypes = {
match: PropTypes.object.isRequired,
}
const SharePage = ({ match }) => {
const SharePage = ({ match }: { match: MatchType }) => {
const { t } = useTranslation()
return (
<Switch>
<Route path={`${match.url}/:token`}>
{({ match }) => {
{({ match }: { match: MatchType<TokenRouteMatch> }) => {
return <TokenRoute match={match} />
}}
</Route>
@ -267,8 +275,4 @@ const SharePage = ({ match }) => {
)
}
SharePage.propTypes = {
...RouterProps,
}
export default SharePage

View File

@ -1,9 +1,21 @@
import React, { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import AlbumTitle from '../AlbumTitle'
import PhotoGallery from '../photoGallery/PhotoGallery'
import AlbumBoxes from './AlbumBoxes'
import AlbumFilter from '../AlbumFilter'
import { albumQuery_album } from '../../Pages/AlbumPage/__generated__/albumQuery'
type AlbumGalleryProps = {
album: albumQuery_album
loading?: boolean
customAlbumLink?(albumID: string): string
showFilter?: boolean
setOnlyFavorites?(favorites: boolean): void
setOrdering?(ordering: { orderBy: string }): void
ordering?: { orderBy: string }
onlyFavorites?: boolean
onFavorite?(): void
}
const AlbumGallery = React.forwardRef(
(
@ -17,18 +29,23 @@ const AlbumGallery = React.forwardRef(
ordering,
onlyFavorites = false,
onFavorite,
},
ref
}: AlbumGalleryProps,
ref: React.ForwardedRef<HTMLDivElement>
) => {
const [imageState, setImageState] = useState({
type ImageStateType = {
activeImage: number
presenting: boolean
}
const [imageState, setImageState] = useState<ImageStateType>({
activeImage: -1,
presenting: false,
})
const setPresenting = presenting =>
const setPresenting = (presenting: boolean) =>
setImageState(state => ({ ...state, presenting }))
const setPresentingWithHistory = presenting => {
const setPresentingWithHistory = (presenting: boolean) => {
setPresenting(presenting)
if (presenting) {
history.pushState({ imageState }, '')
@ -37,12 +54,12 @@ const AlbumGallery = React.forwardRef(
}
}
const updateHistory = imageState => {
const updateHistory = (imageState: ImageStateType) => {
history.replaceState({ imageState }, '')
return imageState
}
const setActiveImage = activeImage => {
const setActiveImage = (activeImage: number) => {
setImageState(state => updateHistory({ ...state, activeImage }))
}
@ -59,9 +76,10 @@ const AlbumGallery = React.forwardRef(
}
useEffect(() => {
const updateImageState = event => {
const updateImageState = (event: PopStateEvent) => {
setImageState(event.state.imageState)
}
window.addEventListener('popstate', updateImageState)
return () => {
@ -129,16 +147,4 @@ const AlbumGallery = React.forwardRef(
}
)
AlbumGallery.propTypes = {
album: PropTypes.object,
loading: PropTypes.bool,
customAlbumLink: PropTypes.func,
showFilter: PropTypes.bool,
setOnlyFavorites: PropTypes.func,
onlyFavorites: PropTypes.bool,
onFavorite: PropTypes.func,
setOrdering: PropTypes.func,
ordering: PropTypes.object,
}
export default AlbumGallery

View File

@ -119,7 +119,7 @@ type MediaThumbnailProps = {
index: number
active: boolean
setPresenting(presenting: boolean): void
onFavorite(): void
onFavorite?(): void
}
export const MediaThumbnail = ({

View File

@ -48,7 +48,7 @@ type PhotoGalleryProps = {
setPresenting(presenting: boolean): void
nextImage(): void
previousImage(): void
onFavorite(): void
onFavorite?(): void
}
const PhotoGallery = ({

View File

@ -71,7 +71,7 @@ export interface ProtectedVideoProps_Media {
thumbnail: null | {
url: string
}
videoWeb: {
videoWeb: null | {
url: string
}
}
@ -80,14 +80,21 @@ export interface ProtectedVideoProps {
media: ProtectedVideoProps_Media
}
export const ProtectedVideo = ({ media, ...props }: ProtectedVideoProps) => (
<video
{...props}
controls
key={media.id}
crossOrigin="use-credentials"
poster={getProtectedUrl(media.thumbnail?.url)}
>
<source src={getProtectedUrl(media.videoWeb.url)} type="video/mp4" />
</video>
)
export const ProtectedVideo = ({ media, ...props }: ProtectedVideoProps) => {
if (media.videoWeb === null) {
console.error('ProetctedVideo called with media.videoWeb = null')
return null
}
return (
<video
{...props}
controls
key={media.id}
crossOrigin="use-credentials"
poster={getProtectedUrl(media.thumbnail?.url)}
>
<source src={getProtectedUrl(media.videoWeb.url)} type="video/mp4" />
</video>
)
}

View File

@ -27,7 +27,7 @@ const StyledVideo = styled(ProtectedVideo)`
export interface PresentMediaProps_Media extends ProtectedVideoProps_Media {
type: MediaType
highRes?: {
highRes: null | {
url: string
}
}

View File

@ -34,6 +34,6 @@ export function debounce<T extends (...args: any[]) => any>(
return debounced
}
export function isNil(value: any) {
export function isNil(value: any): value is undefined | null {
return value === undefined || value === null
}

View File

@ -23,7 +23,7 @@
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true /* Do not emit outputs. */,
"noEmit": true /* Do not emit outputs. */,
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */