From f4e65eb58e8981b621bbdd27b289e27ab487abcf Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Mon, 12 Apr 2021 22:42:23 +0200 Subject: [PATCH] Further work on typescript migration --- ui/package-lock.json | 22 +++++++- ui/package.json | 12 +++-- .../{AlbumBox.js => AlbumBox.tsx} | 31 ++++++----- .../components/messages/SubscriptionsHook.ts | 4 +- .../{PhotoGallery.js => PhotoGallery.tsx} | 41 ++++++++------- .../{ProtectedMedia.js => ProtectedMedia.tsx} | 52 +++++++++++++------ ui/src/components/routes/AuthorizedRoute.tsx | 6 +-- ui/src/components/routes/Routes.tsx | 14 ++--- ui/src/components/sidebar/MediaSidebar.js | 2 +- .../sidebar/{Sidebar.js => Sidebar.tsx} | 29 +++++++++-- ui/src/localization.ts | 4 +- ui/tsconfig.json | 3 +- 12 files changed, 146 insertions(+), 74 deletions(-) rename ui/src/components/albumGallery/{AlbumBox.js => AlbumBox.tsx} (70%) rename ui/src/components/photoGallery/{PhotoGallery.js => PhotoGallery.tsx} (78%) rename ui/src/components/photoGallery/{ProtectedMedia.js => ProtectedMedia.tsx} (54%) rename ui/src/components/sidebar/{Sidebar.js => Sidebar.tsx} (70%) diff --git a/ui/package-lock.json b/ui/package-lock.json index 22986217..d15438cc 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -68,7 +68,8 @@ "husky": "^6.0.0", "jest": "^26.6.3", "lint-staged": "^10.5.4", - "prettier": "^2.2.1" + "prettier": "^2.2.1", + "tsc-files": "^1.1.2" } }, "node_modules/@apollo/client": { @@ -13373,6 +13374,18 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", "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": { "version": "1.13.0", "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": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", diff --git a/ui/package.json b/ui/package.json index 04be30a7..f1363461 100644 --- a/ui/package.json +++ b/ui/package.json @@ -57,7 +57,9 @@ "start": "node --experimental-modules build.mjs watch", "build": "NODE_ENV=production node --experimental-modules build.mjs", "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", "genSchemaTypes": "npx apollo client:codegen --target=typescript", "prepare": "(cd .. && npx husky install)" @@ -77,7 +79,8 @@ "husky": "^6.0.0", "jest": "^26.6.3", "lint-staged": "^10.5.4", - "prettier": "^2.2.1" + "prettier": "^2.2.1", + "tsc-files": "^1.1.2" }, "cache": { "swDest": "service-worker.js" @@ -99,7 +102,8 @@ } }, "lint-staged": { - "*.{js,json,css,md,graphql}": "prettier --write", - "*.js": "eslint --cache --fix --max-warnings 0" + "*.{ts,tsx,js,json,css,md,graphql}": "prettier --write", + "*.{js,ts,tsx}": "eslint --cache --fix --max-warnings 0", + "*.{ts,tsx}": "tsc-files --noEmit" } } diff --git a/ui/src/components/albumGallery/AlbumBox.js b/ui/src/components/albumGallery/AlbumBox.tsx similarity index 70% rename from ui/src/components/albumGallery/AlbumBox.js rename to ui/src/components/albumGallery/AlbumBox.tsx index 1743c23f..0b6364d3 100644 --- a/ui/src/components/albumGallery/AlbumBox.js +++ b/ui/src/components/albumGallery/AlbumBox.tsx @@ -1,5 +1,4 @@ import React, { useState } from 'react' -import PropTypes from 'prop-types' import styled from 'styled-components' import { Link } from 'react-router-dom' import { ProtectedImage } from '../photoGallery/ProtectedMedia' @@ -28,7 +27,7 @@ const Image = styled(ProtectedImage)` object-position: center; ` -const Placeholder = styled.div` +const Placeholder = styled.div<{ overlap?: boolean; loaded?: boolean }>` width: 220px; height: 220px; 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) if (src) { return ( - setLoaded(loaded)} src={src} /> - + setLoaded(true)} src={src} /> + ) } @@ -62,11 +65,16 @@ const AlbumBoxImage = ({ src, ...props }) => { return } -AlbumBoxImage.propTypes = { - src: PropTypes.string, +type AlbumBoxProps = { + 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) { return ( @@ -75,7 +83,7 @@ export const AlbumBox = ({ album, customLink, ...props }) => { ) } - let thumbnail = album.thumbnail?.thumbnail?.url + const thumbnail = album.thumbnail?.thumbnail?.url return ( @@ -84,8 +92,3 @@ export const AlbumBox = ({ album, customLink, ...props }) => { ) } - -AlbumBox.propTypes = { - album: PropTypes.object, - customLink: PropTypes.string, -} diff --git a/ui/src/components/messages/SubscriptionsHook.ts b/ui/src/components/messages/SubscriptionsHook.ts index de1b2870..18cefacc 100644 --- a/ui/src/components/messages/SubscriptionsHook.ts +++ b/ui/src/components/messages/SubscriptionsHook.ts @@ -5,7 +5,7 @@ import { useSubscription, gql } from '@apollo/client' import { authToken } from '../../helpers/authentication' import { NotificationType } from '../../../__generated__/globalTypes' -const notificationSubscription = gql` +const NOTIFICATION_SUBSCRIPTION = gql` subscription notificationSubscription { notification { key @@ -49,7 +49,7 @@ const SubscriptionsHook = ({ } const { data, error } = useSubscription( - notificationSubscription + NOTIFICATION_SUBSCRIPTION ) useEffect(() => { diff --git a/ui/src/components/photoGallery/PhotoGallery.js b/ui/src/components/photoGallery/PhotoGallery.tsx similarity index 78% rename from ui/src/components/photoGallery/PhotoGallery.js rename to ui/src/components/photoGallery/PhotoGallery.tsx index f3a0c66a..41d003a0 100644 --- a/ui/src/components/photoGallery/PhotoGallery.js +++ b/ui/src/components/photoGallery/PhotoGallery.tsx @@ -3,8 +3,7 @@ import styled from 'styled-components' import { Loader } from 'semantic-ui-react' import { MediaThumbnail, PhotoThumbnail } from './MediaThumbnail' import PresentView from './presentView/PresentView' -import PropTypes from 'prop-types' -import { SidebarContext } from '../sidebar/Sidebar' +import { SidebarContext, UpdateSidebarFn } from '../sidebar/Sidebar' import MediaSidebar from '../sidebar/MediaSidebar' import { useTranslation } from 'react-i18next' @@ -31,6 +30,24 @@ const ClearWrap = styled.div` 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 = ({ activeIndex = -1, media, @@ -41,17 +58,15 @@ const PhotoGallery = ({ nextImage, previousImage, onFavorite, -}) => { +}: PhotoGalleryProps) => { const { t } = useTranslation() 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 = [] if (media) { - media.filter(media => media.thumbnail) - photoElements = media.map((photo, 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 diff --git a/ui/src/components/photoGallery/ProtectedMedia.js b/ui/src/components/photoGallery/ProtectedMedia.tsx similarity index 54% rename from ui/src/components/photoGallery/ProtectedMedia.js rename to ui/src/components/photoGallery/ProtectedMedia.tsx index a8551298..0df00910 100644 --- a/ui/src/components/photoGallery/ProtectedMedia.js +++ b/ui/src/components/photoGallery/ProtectedMedia.tsx @@ -1,11 +1,11 @@ -import React from 'react' -import PropTypes from 'prop-types' +import React, { DetailedHTMLProps, ImgHTMLAttributes } from 'react' const isNativeLazyLoadSupported = 'loading' in HTMLImageElement.prototype -const placeholder = 'data:image/gif;base64,R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' +const placeholder = + 'data:image/gif;base64,R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' -const getProtectedUrl = url => { - if (url == null) return null +const getProtectedUrl = (url?: string) => { + if (url == undefined) return undefined const imgUrl = new URL(url, location.origin) @@ -18,25 +18,40 @@ const getProtectedUrl = url => { return imgUrl.href } +export interface ProtectedImageProps + extends DetailedHTMLProps< + ImgHTMLAttributes, + HTMLImageElement + > { + lazyLoading?: boolean +} + /** * An image that needs authorization to load * 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, * 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) { - props['data-src'] = getProtectedUrl(src) + lazyLoadProps['data-src'] = getProtectedUrl(src) } if (isNativeLazyLoadSupported && lazyLoading) { - props.loading = 'lazy' + lazyLoadProps.loading = 'lazy' } return ( { ) } -ProtectedImage.propTypes = { - src: PropTypes.string, - lazyLoading: PropTypes.bool, +export interface ProtectedVideoProps_Media { + id: string + thumbnail: null | { + url: string + } + videoWeb: { + url: string + } } -export const ProtectedVideo = ({ media, ...props }) => ( +export interface ProtectedVideoProps { + media: ProtectedVideoProps_Media +} + +export const ProtectedVideo = ({ media, ...props }: ProtectedVideoProps) => ( ) - -ProtectedVideo.propTypes = { - media: PropTypes.object.isRequired, -} diff --git a/ui/src/components/routes/AuthorizedRoute.tsx b/ui/src/components/routes/AuthorizedRoute.tsx index 2ba606e1..6779f3d5 100644 --- a/ui/src/components/routes/AuthorizedRoute.tsx +++ b/ui/src/components/routes/AuthorizedRoute.tsx @@ -1,6 +1,6 @@ import React, { ReactChild, useEffect } from 'react' 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 { authToken } from '../../helpers/authentication' import { ADMIN_QUERY } from '../../Layout' @@ -27,9 +27,9 @@ export const Authorized = ({ children }: { children: JSX.Element }) => { return token ? children : null } -type AuthorizedRouteProps = { +interface AuthorizedRouteProps extends Omit { component: ReactComponentLike - admin: boolean + admin?: boolean } const AuthorizedRoute = ({ diff --git a/ui/src/components/routes/Routes.tsx b/ui/src/components/routes/Routes.tsx index 07397523..6bf93eb8 100644 --- a/ui/src/components/routes/Routes.tsx +++ b/ui/src/components/routes/Routes.tsx @@ -8,8 +8,8 @@ import { useTranslation } from 'react-i18next' const AuthorizedRoute = React.lazy(() => import('./AuthorizedRoute')) -const AlbumsPage = React.lazy(() => - import('../../Pages/AllAlbumsPage/AlbumsPage') +const AlbumsPage = React.lazy( + () => import('../../Pages/AllAlbumsPage/AlbumsPage') ) const AlbumPage = React.lazy(() => import('../../Pages/AlbumPage/AlbumPage')) 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 LoginPage = React.lazy(() => import('../../Pages/LoginPage/LoginPage')) -const InitialSetupPage = React.lazy(() => - import('../../Pages/LoginPage/InitialSetupPage') +const InitialSetupPage = React.lazy( + () => import('../../Pages/LoginPage/InitialSetupPage') ) -const SettingsPage = React.lazy(() => - import('../../Pages/SettingsPage/SettingsPage') +const SettingsPage = React.lazy( + () => import('../../Pages/SettingsPage/SettingsPage') ) const Routes = () => { @@ -32,7 +32,7 @@ const Routes = () => { return ( + {t('general.loading.page', 'Loading page')} } diff --git a/ui/src/components/sidebar/MediaSidebar.js b/ui/src/components/sidebar/MediaSidebar.js index e1d2bdfa..0bc82be0 100644 --- a/ui/src/components/sidebar/MediaSidebar.js +++ b/ui/src/components/sidebar/MediaSidebar.js @@ -370,7 +370,7 @@ const MediaSidebar = ({ media, hidePreview }) => { return } - if (error) return error + if (error) return
{error.message}
if (loading || data == null) { return diff --git a/ui/src/components/sidebar/Sidebar.js b/ui/src/components/sidebar/Sidebar.tsx similarity index 70% rename from ui/src/components/sidebar/Sidebar.js rename to ui/src/components/sidebar/Sidebar.tsx index 508d2635..aed641ac 100644 --- a/ui/src/components/sidebar/Sidebar.js +++ b/ui/src/components/sidebar/Sidebar.tsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import styled from 'styled-components' import { Icon } from 'semantic-ui-react' -const SidebarContainer = styled.div` +const SidebarContainer = styled.div<{ highlighted: boolean }>` width: 28vw; max-width: 500px; 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({ + updateSidebar: content => { + console.warn( + 'SidebarContext: updateSidebar was called before initialezed', + content + ) + }, + content: null, +}) SidebarContext.displayName = 'SidebarContext' -const Sidebar = ({ children }) => { - const [state, setState] = useState({ +type SidebarProps = { + children: React.ReactElement +} + +const Sidebar = ({ children }: SidebarProps) => { + const [state, setState] = useState<{ content: React.ReactNode | null }>({ content: null, }) - const update = content => { + const update = (content: React.ReactNode | null) => { setState({ content }) } diff --git a/ui/src/localization.ts b/ui/src/localization.ts index 882ad838..f770aabe 100644 --- a/ui/src/localization.ts +++ b/ui/src/localization.ts @@ -1,5 +1,7 @@ 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 { i18n.use(initReactI18next).init({ diff --git a/ui/tsconfig.json b/ui/tsconfig.json index fd4fb474..76533d9d 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -74,5 +74,6 @@ "skipLibCheck": true /* Skip type checking of declaration files. */, "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ }, - "exclude": ["node_modules/", "dist/"] + "include": ["src/**/*"], + "exclude": ["node_modules/**/*", "dist/**/*"] }