1
Fork 0

Further work on typescript migration

This commit is contained in:
viktorstrate 2021-04-12 22:42:23 +02:00
parent c5d2f3dc8b
commit f4e65eb58e
No known key found for this signature in database
GPG Key ID: 3F855605109C1E8A
12 changed files with 146 additions and 74 deletions

22
ui/package-lock.json generated
View File

@ -68,7 +68,8 @@
"husky": "^6.0.0", "husky": "^6.0.0",
"jest": "^26.6.3", "jest": "^26.6.3",
"lint-staged": "^10.5.4", "lint-staged": "^10.5.4",
"prettier": "^2.2.1" "prettier": "^2.2.1",
"tsc-files": "^1.1.2"
} }
}, },
"node_modules/@apollo/client": { "node_modules/@apollo/client": {
@ -13373,6 +13374,18 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A=="
}, },
"node_modules/tsc-files": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/tsc-files/-/tsc-files-1.1.2.tgz",
"integrity": "sha512-biLtl4npoohZ9MBnTFw4NttqYM60RscjzjWxT538UCS8iXaGRZMi+AXj+vEEpDdcjIS2Kx0Acj++1gor5dbbBw==",
"dev": true,
"bin": {
"tsc-files": "lib/index.js"
},
"peerDependencies": {
"typescript": ">=3"
}
},
"node_modules/tslib": { "node_modules/tslib": {
"version": "1.13.0", "version": "1.13.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
@ -24801,6 +24814,13 @@
} }
} }
}, },
"tsc-files": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/tsc-files/-/tsc-files-1.1.2.tgz",
"integrity": "sha512-biLtl4npoohZ9MBnTFw4NttqYM60RscjzjWxT538UCS8iXaGRZMi+AXj+vEEpDdcjIS2Kx0Acj++1gor5dbbBw==",
"dev": true,
"requires": {}
},
"tslib": { "tslib": {
"version": "1.13.0", "version": "1.13.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",

View File

@ -57,7 +57,9 @@
"start": "node --experimental-modules build.mjs watch", "start": "node --experimental-modules build.mjs watch",
"build": "NODE_ENV=production node --experimental-modules build.mjs", "build": "NODE_ENV=production node --experimental-modules build.mjs",
"test": "npm run lint && npm run jest", "test": "npm run lint && npm run jest",
"lint": "eslint ./src --max-warnings 0 --cache --config .eslintrc.js", "lint": "npm run lint:types & npm run lint:eslint",
"lint:eslint": "eslint ./src --max-warnings 0 --cache --config .eslintrc.js",
"lint:types": "tsc --noemit",
"jest": "jest", "jest": "jest",
"genSchemaTypes": "npx apollo client:codegen --target=typescript", "genSchemaTypes": "npx apollo client:codegen --target=typescript",
"prepare": "(cd .. && npx husky install)" "prepare": "(cd .. && npx husky install)"
@ -77,7 +79,8 @@
"husky": "^6.0.0", "husky": "^6.0.0",
"jest": "^26.6.3", "jest": "^26.6.3",
"lint-staged": "^10.5.4", "lint-staged": "^10.5.4",
"prettier": "^2.2.1" "prettier": "^2.2.1",
"tsc-files": "^1.1.2"
}, },
"cache": { "cache": {
"swDest": "service-worker.js" "swDest": "service-worker.js"
@ -99,7 +102,8 @@
} }
}, },
"lint-staged": { "lint-staged": {
"*.{js,json,css,md,graphql}": "prettier --write", "*.{ts,tsx,js,json,css,md,graphql}": "prettier --write",
"*.js": "eslint --cache --fix --max-warnings 0" "*.{js,ts,tsx}": "eslint --cache --fix --max-warnings 0",
"*.{ts,tsx}": "tsc-files --noEmit"
} }
} }

View File

@ -1,5 +1,4 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components' import styled from 'styled-components'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ProtectedImage } from '../photoGallery/ProtectedMedia' import { ProtectedImage } from '../photoGallery/ProtectedMedia'
@ -28,7 +27,7 @@ const Image = styled(ProtectedImage)`
object-position: center; object-position: center;
` `
const Placeholder = styled.div` const Placeholder = styled.div<{ overlap?: boolean; loaded?: boolean }>`
width: 220px; width: 220px;
height: 220px; height: 220px;
border-radius: 4%; border-radius: 4%;
@ -47,14 +46,18 @@ const Placeholder = styled.div`
`} `}
` `
const AlbumBoxImage = ({ src, ...props }) => { interface AlbumBoxImageProps {
src?: string
}
const AlbumBoxImage = ({ src, ...props }: AlbumBoxImageProps) => {
const [loaded, setLoaded] = useState(false) const [loaded, setLoaded] = useState(false)
if (src) { if (src) {
return ( return (
<ImageWrapper> <ImageWrapper>
<Image {...props} onLoad={loaded => setLoaded(loaded)} src={src} /> <Image {...props} onLoad={() => setLoaded(true)} src={src} />
<Placeholder overlap loaded={loaded ? 1 : 0} /> <Placeholder overlap loaded={loaded} />
</ImageWrapper> </ImageWrapper>
) )
} }
@ -62,11 +65,16 @@ const AlbumBoxImage = ({ src, ...props }) => {
return <Placeholder /> return <Placeholder />
} }
AlbumBoxImage.propTypes = { type AlbumBoxProps = {
src: PropTypes.string, album?: {
id: string
title: string
thumbnail?: { thumbnail?: { url: string } }
}
customLink?: string
} }
export const AlbumBox = ({ album, customLink, ...props }) => { export const AlbumBox = ({ album, customLink, ...props }: AlbumBoxProps) => {
if (!album) { if (!album) {
return ( return (
<AlbumBoxLink {...props} to="#"> <AlbumBoxLink {...props} to="#">
@ -75,7 +83,7 @@ export const AlbumBox = ({ album, customLink, ...props }) => {
) )
} }
let thumbnail = album.thumbnail?.thumbnail?.url const thumbnail = album.thumbnail?.thumbnail?.url
return ( return (
<AlbumBoxLink {...props} to={customLink || `/album/${album.id}`}> <AlbumBoxLink {...props} to={customLink || `/album/${album.id}`}>
@ -84,8 +92,3 @@ export const AlbumBox = ({ album, customLink, ...props }) => {
</AlbumBoxLink> </AlbumBoxLink>
) )
} }
AlbumBox.propTypes = {
album: PropTypes.object,
customLink: PropTypes.string,
}

View File

@ -5,7 +5,7 @@ import { useSubscription, gql } from '@apollo/client'
import { authToken } from '../../helpers/authentication' import { authToken } from '../../helpers/authentication'
import { NotificationType } from '../../../__generated__/globalTypes' import { NotificationType } from '../../../__generated__/globalTypes'
const notificationSubscription = gql` const NOTIFICATION_SUBSCRIPTION = gql`
subscription notificationSubscription { subscription notificationSubscription {
notification { notification {
key key
@ -49,7 +49,7 @@ const SubscriptionsHook = ({
} }
const { data, error } = useSubscription<notificationSubscription>( const { data, error } = useSubscription<notificationSubscription>(
notificationSubscription NOTIFICATION_SUBSCRIPTION
) )
useEffect(() => { useEffect(() => {

View File

@ -3,8 +3,7 @@ import styled from 'styled-components'
import { Loader } from 'semantic-ui-react' 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 PropTypes from 'prop-types' import { SidebarContext, UpdateSidebarFn } from '../sidebar/Sidebar'
import { SidebarContext } from '../sidebar/Sidebar'
import MediaSidebar from '../sidebar/MediaSidebar' import MediaSidebar from '../sidebar/MediaSidebar'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -31,6 +30,24 @@ const ClearWrap = styled.div`
clear: both; clear: both;
` `
type PhotoGalleryProps = {
loading: boolean
media: {
id: string
title: string
thumbnail?: {
url: string
}
}[]
activeIndex: number
presenting: boolean
onSelectImage(index: number): void
setPresenting(callback: (presenting: boolean) => void): void
nextImage(): void
previousImage(): void
onFavorite(): void
}
const PhotoGallery = ({ const PhotoGallery = ({
activeIndex = -1, activeIndex = -1,
media, media,
@ -41,17 +58,15 @@ const PhotoGallery = ({
nextImage, nextImage,
previousImage, previousImage,
onFavorite, onFavorite,
}) => { }: PhotoGalleryProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { updateSidebar } = useContext(SidebarContext) const { updateSidebar } = useContext(SidebarContext)
const activeImage = media && activeIndex != -1 && media[activeIndex] const activeImage = (media && activeIndex != -1 && media[activeIndex]) || {}
const getPhotoElements = updateSidebar => { const getPhotoElements = (updateSidebar: UpdateSidebarFn) => {
let photoElements = [] let photoElements = []
if (media) { if (media) {
media.filter(media => media.thumbnail)
photoElements = media.map((photo, index) => { photoElements = media.map((photo, index) => {
const active = activeIndex == index const active = activeIndex == index
@ -98,16 +113,4 @@ const PhotoGallery = ({
) )
} }
PhotoGallery.propTypes = {
loading: PropTypes.bool,
media: PropTypes.array,
activeIndex: PropTypes.number,
presenting: PropTypes.bool,
onSelectImage: PropTypes.func,
setPresenting: PropTypes.func,
nextImage: PropTypes.func,
previousImage: PropTypes.func,
onFavorite: PropTypes.func,
}
export default PhotoGallery export default PhotoGallery

View File

@ -1,11 +1,11 @@
import React from 'react' import React, { DetailedHTMLProps, ImgHTMLAttributes } from 'react'
import PropTypes from 'prop-types'
const isNativeLazyLoadSupported = 'loading' in HTMLImageElement.prototype const isNativeLazyLoadSupported = 'loading' in HTMLImageElement.prototype
const placeholder = 'data:image/gif;base64,R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' const placeholder =
'data:image/gif;base64,R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
const getProtectedUrl = url => { const getProtectedUrl = (url?: string) => {
if (url == null) return null if (url == undefined) return undefined
const imgUrl = new URL(url, location.origin) const imgUrl = new URL(url, location.origin)
@ -18,25 +18,40 @@ const getProtectedUrl = url => {
return imgUrl.href return imgUrl.href
} }
export interface ProtectedImageProps
extends DetailedHTMLProps<
ImgHTMLAttributes<HTMLImageElement>,
HTMLImageElement
> {
lazyLoading?: boolean
}
/** /**
* An image that needs authorization to load * An image that needs authorization to load
* Set lazyLoading to true if you want the image to be loaded once it enters the viewport * Set lazyLoading to true if you want the image to be loaded once it enters the viewport
* Native lazy load via HTMLImageElement.loading attribute will be preferred if it is supported by the browser, * Native lazy load via HTMLImageElement.loading attribute will be preferred if it is supported by the browser,
* otherwise IntersectionObserver will be used. * otherwise IntersectionObserver will be used.
*/ */
export const ProtectedImage = ({ src, lazyLoading, ...props }) => { export const ProtectedImage = ({
src,
lazyLoading,
...props
}: ProtectedImageProps) => {
const lazyLoadProps: { 'data-src'?: string; loading?: 'lazy' | 'eager' } = {}
if (!isNativeLazyLoadSupported && lazyLoading) { if (!isNativeLazyLoadSupported && lazyLoading) {
props['data-src'] = getProtectedUrl(src) lazyLoadProps['data-src'] = getProtectedUrl(src)
} }
if (isNativeLazyLoadSupported && lazyLoading) { if (isNativeLazyLoadSupported && lazyLoading) {
props.loading = 'lazy' lazyLoadProps.loading = 'lazy'
} }
return ( return (
<img <img
key={src} key={src}
{...props} {...props}
{...lazyLoadProps}
src={ src={
lazyLoading && !isNativeLazyLoadSupported lazyLoading && !isNativeLazyLoadSupported
? placeholder ? placeholder
@ -47,12 +62,21 @@ export const ProtectedImage = ({ src, lazyLoading, ...props }) => {
) )
} }
ProtectedImage.propTypes = { export interface ProtectedVideoProps_Media {
src: PropTypes.string, id: string
lazyLoading: PropTypes.bool, thumbnail: null | {
url: string
}
videoWeb: {
url: string
}
} }
export const ProtectedVideo = ({ media, ...props }) => ( export interface ProtectedVideoProps {
media: ProtectedVideoProps_Media
}
export const ProtectedVideo = ({ media, ...props }: ProtectedVideoProps) => (
<video <video
{...props} {...props}
controls controls
@ -63,7 +87,3 @@ export const ProtectedVideo = ({ media, ...props }) => (
<source src={getProtectedUrl(media.videoWeb.url)} type="video/mp4" /> <source src={getProtectedUrl(media.videoWeb.url)} type="video/mp4" />
</video> </video>
) )
ProtectedVideo.propTypes = {
media: PropTypes.object.isRequired,
}

View File

@ -1,6 +1,6 @@
import React, { ReactChild, useEffect } from 'react' import React, { ReactChild, useEffect } from 'react'
import PropTypes, { ReactComponentLike } from 'prop-types' import PropTypes, { ReactComponentLike } from 'prop-types'
import { Route, Redirect } from 'react-router-dom' import { Route, Redirect, RouteProps } from 'react-router-dom'
import { useLazyQuery } from '@apollo/client' import { useLazyQuery } from '@apollo/client'
import { authToken } from '../../helpers/authentication' import { authToken } from '../../helpers/authentication'
import { ADMIN_QUERY } from '../../Layout' import { ADMIN_QUERY } from '../../Layout'
@ -27,9 +27,9 @@ export const Authorized = ({ children }: { children: JSX.Element }) => {
return token ? children : null return token ? children : null
} }
type AuthorizedRouteProps = { interface AuthorizedRouteProps extends Omit<RouteProps, 'component'> {
component: ReactComponentLike component: ReactComponentLike
admin: boolean admin?: boolean
} }
const AuthorizedRoute = ({ const AuthorizedRoute = ({

View File

@ -8,8 +8,8 @@ import { useTranslation } from 'react-i18next'
const AuthorizedRoute = React.lazy(() => import('./AuthorizedRoute')) const AuthorizedRoute = React.lazy(() => import('./AuthorizedRoute'))
const AlbumsPage = React.lazy(() => const AlbumsPage = React.lazy(
import('../../Pages/AllAlbumsPage/AlbumsPage') () => import('../../Pages/AllAlbumsPage/AlbumsPage')
) )
const AlbumPage = React.lazy(() => import('../../Pages/AlbumPage/AlbumPage')) const AlbumPage = React.lazy(() => import('../../Pages/AlbumPage/AlbumPage'))
const PhotosPage = React.lazy(() => import('../../Pages/PhotosPage/PhotosPage')) const PhotosPage = React.lazy(() => import('../../Pages/PhotosPage/PhotosPage'))
@ -18,12 +18,12 @@ const SharePage = React.lazy(() => import('../../Pages/SharePage/SharePage'))
const PeoplePage = React.lazy(() => import('../../Pages/PeoplePage/PeoplePage')) const PeoplePage = React.lazy(() => import('../../Pages/PeoplePage/PeoplePage'))
const LoginPage = React.lazy(() => import('../../Pages/LoginPage/LoginPage')) const LoginPage = React.lazy(() => import('../../Pages/LoginPage/LoginPage'))
const InitialSetupPage = React.lazy(() => const InitialSetupPage = React.lazy(
import('../../Pages/LoginPage/InitialSetupPage') () => import('../../Pages/LoginPage/InitialSetupPage')
) )
const SettingsPage = React.lazy(() => const SettingsPage = React.lazy(
import('../../Pages/SettingsPage/SettingsPage') () => import('../../Pages/SettingsPage/SettingsPage')
) )
const Routes = () => { const Routes = () => {
@ -32,7 +32,7 @@ const Routes = () => {
return ( return (
<React.Suspense <React.Suspense
fallback={ fallback={
<Layout> <Layout title={t('general.loading.page', 'Loading page')}>
<Loader active>{t('general.loading.page', 'Loading page')}</Loader> <Loader active>{t('general.loading.page', 'Loading page')}</Loader>
</Layout> </Layout>
} }

View File

@ -370,7 +370,7 @@ const MediaSidebar = ({ media, hidePreview }) => {
return <SidebarContent media={media} hidePreview={hidePreview} /> return <SidebarContent media={media} hidePreview={hidePreview} />
} }
if (error) return error if (error) return <div>{error.message}</div>
if (loading || data == null) { if (loading || data == null) {
return <SidebarContent media={media} hidePreview={hidePreview} /> return <SidebarContent media={media} hidePreview={hidePreview} />

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
import styled from 'styled-components' import styled from 'styled-components'
import { Icon } from 'semantic-ui-react' import { Icon } from 'semantic-ui-react'
const SidebarContainer = styled.div` const SidebarContainer = styled.div<{ highlighted: boolean }>`
width: 28vw; width: 28vw;
max-width: 500px; max-width: 500px;
min-width: 300px; min-width: 300px;
@ -38,15 +38,34 @@ const SidebarDismissButton = styled(Icon)`
} }
` `
export const SidebarContext = createContext() export type UpdateSidebarFn = (content: React.ReactNode) => void
interface SidebarContextType {
updateSidebar: UpdateSidebarFn
content: React.ReactNode
}
export const SidebarContext = createContext<SidebarContextType>({
updateSidebar: content => {
console.warn(
'SidebarContext: updateSidebar was called before initialezed',
content
)
},
content: null,
})
SidebarContext.displayName = 'SidebarContext' SidebarContext.displayName = 'SidebarContext'
const Sidebar = ({ children }) => { type SidebarProps = {
const [state, setState] = useState({ children: React.ReactElement
}
const Sidebar = ({ children }: SidebarProps) => {
const [state, setState] = useState<{ content: React.ReactNode | null }>({
content: null, content: null,
}) })
const update = content => { const update = (content: React.ReactNode | null) => {
setState({ content }) setState({ content })
} }

View File

@ -1,5 +1,7 @@
import i18n from 'i18next' import i18n from 'i18next'
import { initReactI18next } from 'react-i18next' import { initReactI18next, TFunction } from 'react-i18next'
export type TranslationFn = TFunction<'translation'>
export default function setupLocalization(): void { export default function setupLocalization(): void {
i18n.use(initReactI18next).init({ i18n.use(initReactI18next).init({

View File

@ -74,5 +74,6 @@
"skipLibCheck": true /* Skip type checking of declaration files. */, "skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}, },
"exclude": ["node_modules/", "dist/"] "include": ["src/**/*"],
"exclude": ["node_modules/**/*", "dist/**/*"]
} }