1
Fork 0

Continue with typescript migration

This commit is contained in:
viktorstrate 2021-04-12 16:06:06 +02:00
parent 264ee54e7f
commit c5d2f3dc8b
No known key found for this signature in database
GPG Key ID: 3F855605109C1E8A
23 changed files with 882 additions and 435 deletions

View File

@ -1,10 +1,18 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
env: {
browser: true,
es6: true,
},
ignorePatterns: ['**/*.ts', '**/*.tsx'],
extends: ['eslint:recommended', 'plugin:react/recommended'],
ignorePatterns: ['node_modules', 'dist'],
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
@ -19,17 +27,20 @@ module.exports = {
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: ['react', 'react-hooks'],
plugins: ['react', 'react-hooks', '@typescript-eslint'],
rules: {
'no-unused-vars': 'warn',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
'react/display-name': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
},
settings: {
react: {
version: 'detect',
},
},
parser: 'babel-eslint',
// parser: 'babel-eslint',
overrides: [
Object.assign(require('eslint-plugin-jest').configs.recommended, {
files: ['**/*.test.js'],
@ -43,5 +54,11 @@ module.exports = {
}
),
}),
{
files: ['**/*.js'],
rules: {
'@typescript-eslint/explicit-module-boundary-types': 'off',
},
},
],
}

View File

@ -2,7 +2,7 @@ module.exports = function (api) {
const isTest = api.env('test')
const isProduction = api.env('NODE_ENV') == 'production'
let presets = ['@babel/preset-react']
let presets = ['@babel/preset-typescript', '@babel/preset-react']
let plugins = []
if (isTest) {

View File

@ -23,7 +23,7 @@ const esbuildOptions = {
entryPoints: ['src/index.tsx'],
plugins: [
babel({
filter: /photoview\/ui\/src\/.*\.js$/,
filter: /photoview\/ui\/src\/.*\.(js|tsx?)$/,
}),
],
publicPath: process.env.UI_PUBLIC_URL || '/',
@ -66,25 +66,25 @@ if (watchMode) {
open: false,
})
bs.watch('src/**/*.js').on('change', async args => {
bs.watch('src/**/*.@(js|tsx|ts)').on('change', async args => {
console.log('reloading', args)
builderPromise = (await builderPromise).rebuild()
bs.reload(args)
})
} else {
const esbuildPromise = esbuild
.build(esbuildOptions)
.then(() => console.log('esbuild done'))
const build = async () => {
await esbuild.build(esbuildOptions)
const workboxPromise = workboxBuild
.generateSW({
console.log('esbuild done')
await workboxBuild.generateSW({
globDirectory: 'dist/',
globPatterns: ['**/*.{png,svg,woff2,ttf,eot,woff,js,ico,html,json,css}'],
swDest: 'dist/service-worker.js',
})
.then(() => console.log('workbox done'))
Promise.all([esbuildPromise, workboxPromise]).then(() =>
console.log('workbox done')
console.log('build complete')
)
}
build()
}

864
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,7 @@
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.13.15",
"@babel/preset-react": "^7.13.13",
"@babel/preset-typescript": "^7.13.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.3",
"babel-plugin-graphql-tag": "^3.2.0",
@ -56,7 +57,7 @@
"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",
"lint": "eslint ./src --max-warnings 0 --cache --config .eslintrc.js",
"jest": "jest",
"genSchemaTypes": "npx apollo client:codegen --target=typescript",
"prepare": "(cd .. && npx husky install)"
@ -69,6 +70,10 @@
"@types/react-helmet": "^6.1.1",
"@types/react-router-dom": "^5.1.7",
"@types/styled-components": "^5.1.9",
"@types/url-join": "^4.0.0",
"@typescript-eslint/eslint-plugin": "^4.21.0",
"@typescript-eslint/parser": "^4.21.0",
"eslint-config-prettier": "^8.1.0",
"husky": "^6.0.0",
"jest": "^26.6.3",
"lint-staged": "^10.5.4",

4
ui/src/@types/index.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module '*.svg' {
const src: string
export default src
}

View File

@ -80,7 +80,7 @@ const SideButtonLink = styled(NavLink)`
`
type SideButtonProps = {
children: any
children: ReactChild | ReactChild[]
to: string
exact: boolean
}

View File

@ -4,6 +4,8 @@ import {
split,
ApolloLink,
HttpLink,
ServerError,
FieldMergeFunction,
} from '@apollo/client'
import { getMainDefinition } from '@apollo/client/utilities'
import { onError } from '@apollo/client/link/error'
@ -12,6 +14,7 @@ import { WebSocketLink } from '@apollo/client/link/ws'
import urlJoin from 'url-join'
import { clearTokenCookie } from './helpers/authentication'
import { MessageState } from './components/messages/Messages'
import { Message } from './components/messages/SubscriptionsHook'
export const GRAPHQL_ENDPOINT = process.env.PHOTOVIEW_API_ENDPOINT
? urlJoin(process.env.PHOTOVIEW_API_ENDPOINT, '/graphql')
@ -26,12 +29,12 @@ console.log('GRAPHQL ENDPOINT', GRAPHQL_ENDPOINT)
const apiProtocol = new URL(GRAPHQL_ENDPOINT).protocol
let websocketUri = new URL(GRAPHQL_ENDPOINT)
const websocketUri = new URL(GRAPHQL_ENDPOINT)
websocketUri.protocol = apiProtocol === 'https:' ? 'wss:' : 'ws:'
const wsLink = new WebSocketLink({
uri: websocketUri,
credentials: 'include',
uri: websocketUri.toString(),
// credentials: 'include',
})
const link = split(
@ -48,7 +51,7 @@ const link = split(
)
const linkError = onError(({ graphQLErrors, networkError }) => {
let errorMessages = []
const errorMessages = []
if (graphQLErrors) {
graphQLErrors.map(({ message, locations, path }) =>
@ -82,7 +85,7 @@ const linkError = onError(({ graphQLErrors, networkError }) => {
console.log(`[Network error]: ${JSON.stringify(networkError)}`)
clearTokenCookie()
const errors = networkError.result.errors
const errors = (networkError as ServerError).result.errors
if (errors.length == 1) {
errorMessages.push({
@ -92,7 +95,9 @@ const linkError = onError(({ graphQLErrors, networkError }) => {
} else if (errors.length > 1) {
errorMessages.push({
header: 'Multiple server errors',
content: `Received ${graphQLErrors.length} errors from the server. You are being logged out in an attempt to recover.`,
content: `Received ${
graphQLErrors?.length || 0
} errors from the server. You are being logged out in an attempt to recover.`,
})
}
}
@ -106,26 +111,32 @@ const linkError = onError(({ graphQLErrors, networkError }) => {
...msg,
},
}))
MessageState.set(messages => [...messages, ...newMessages])
MessageState.set((messages: Message[]) => [...messages, ...newMessages])
}
})
type PaginateCacheType = {
keyArgs: string[]
merge: FieldMergeFunction
}
// Modified version of Apollo's offsetLimitPagination()
const paginateCache = keyArgs => ({
keyArgs,
merge(existing, incoming, { args, fieldName }) {
const merged = existing ? existing.slice(0) : []
if (args?.paginate) {
const { offset = 0 } = args.paginate
for (let i = 0; i < incoming.length; ++i) {
merged[offset + i] = incoming[i]
const paginateCache = (keyArgs: string[]) =>
({
keyArgs,
merge(existing, incoming, { args, fieldName }) {
const merged = existing ? existing.slice(0) : []
if (args?.paginate) {
const { offset = 0 } = args.paginate
for (let i = 0; i < incoming.length; ++i) {
merged[offset + i] = incoming[i]
}
} else {
throw new Error(`Paginate argument is missing for query: ${fieldName}`)
}
} else {
throw new Error(`Paginate argument is missing for query: ${fieldName}`)
}
return merged
},
})
return merged
},
} as PaginateCacheType)
const memoryCache = new InMemoryCache({
typePolicies: {

View File

@ -1,6 +1,6 @@
import React, { useEffect, useContext } from 'react'
import PropTypes from 'prop-types'
import { Breadcrumb } from 'semantic-ui-react'
import { Breadcrumb, IconProps } from 'semantic-ui-react'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import { Icon } from 'semantic-ui-react'
@ -8,6 +8,7 @@ import { SidebarContext } from './sidebar/Sidebar'
import AlbumSidebar from './sidebar/AlbumSidebar'
import { useLazyQuery, gql } from '@apollo/client'
import { authToken } from '../helpers/authentication'
import { albumPathQuery } from './__generated__/albumPathQuery'
const Header = styled.h1`
margin: 24px 0 8px 0 !important;
@ -32,7 +33,7 @@ const StyledIcon = styled(Icon)`
}
`
const SettingsIcon = props => {
const SettingsIcon = (props: IconProps) => {
return <StyledIcon name="settings" size="small" {...props} />
}
@ -48,8 +49,18 @@ const ALBUM_PATH_QUERY = gql`
}
`
const AlbumTitle = ({ album, disableLink = false }) => {
const [fetchPath, { data: pathData }] = useLazyQuery(ALBUM_PATH_QUERY)
type AlbumTitleProps = {
album: {
id: string
title: string
}
disableLink: boolean
}
const AlbumTitle = ({ album, disableLink = false }: AlbumTitleProps) => {
const [fetchPath, { data: pathData }] = useLazyQuery<albumPathQuery>(
ALBUM_PATH_QUERY
)
const { updateSidebar } = useContext(SidebarContext)
useEffect(() => {
@ -68,10 +79,7 @@ const AlbumTitle = ({ album, disableLink = false }) => {
let title = <span>{album.title}</span>
let path = []
if (pathData) {
path = pathData.album.path
}
const path = pathData?.album.path || []
const breadcrumbSections = path
.slice()

View File

@ -1,8 +1,12 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Loader } from 'semantic-ui-react'
const PaginateLoader = ({ active, text }) => (
type PaginateLoaderProps = {
active: boolean
text: string
}
const PaginateLoader = ({ active, text }: PaginateLoaderProps) => (
<Loader
style={{ margin: '42px 0 24px 0', opacity: active ? '1' : '0' }}
inline="centered"
@ -12,9 +16,4 @@ const PaginateLoader = ({ active, text }) => (
</Loader>
)
PaginateLoader.propTypes = {
active: PropTypes.bool,
text: PropTypes.string,
}
export default PaginateLoader

View File

@ -1,11 +1,15 @@
import React, { useState, useRef, useEffect } from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import { useLazyQuery, gql } from '@apollo/client'
import { debounce } from '../../helpers/utils'
import { debounce, DebouncedFn } from '../../helpers/utils'
import { ProtectedImage } from '../photoGallery/ProtectedMedia'
import { NavLink } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import {
searchQuery,
searchQuery_search_albums,
searchQuery_search_media,
} from './__generated__/searchQuery'
const Container = styled.div`
height: 60px;
@ -30,7 +34,7 @@ const SearchField = styled.input`
}
`
const Results = styled.div`
const Results = styled.div<{ show: boolean }>`
display: ${({ show }) => (show ? 'block' : 'none')};
position: absolute;
width: 100%;
@ -79,27 +83,29 @@ const SEARCH_QUERY = gql`
const SearchBar = () => {
const { t } = useTranslation()
const [fetchSearches, fetchResult] = useLazyQuery(SEARCH_QUERY)
const [fetchSearches, fetchResult] = useLazyQuery<searchQuery>(SEARCH_QUERY)
const [query, setQuery] = useState('')
const [fetched, setFetched] = useState(false)
let debouncedFetch = useRef(null)
type QueryFn = (query: string) => void
const debouncedFetch = useRef<null | DebouncedFn<QueryFn>>(null)
useEffect(() => {
debouncedFetch.current = debounce(query => {
debouncedFetch.current = debounce<QueryFn>(query => {
fetchSearches({ variables: { query } })
setFetched(true)
}, 250)
return () => {
debouncedFetch.current.cancel()
debouncedFetch.current?.cancel()
}
}, [])
const fetchEvent = e => {
const fetchEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
e.persist()
setQuery(e.target.value)
if (e.target.value.trim() != '') {
if (e.target.value.trim() != '' && debouncedFetch.current) {
debouncedFetch.current(e.target.value.trim())
} else {
setFetched(false)
@ -108,7 +114,12 @@ const SearchBar = () => {
let results = null
if (query.trim().length > 0 && fetched) {
results = <SearchResults result={fetchResult} />
results = (
<SearchResults
searchData={fetchResult.data}
loading={fetchResult.loading}
/>
)
}
return (
@ -128,17 +139,21 @@ const ResultTitle = styled.h1`
margin: 12px 0 0.25rem;
`
const SearchResults = ({ result }) => {
const { t } = useTranslation()
const { data, loading } = result
const query = data && data.search.query
type SearchResultsProps = {
searchData?: searchQuery
loading: boolean
}
const media = (data && data.search.media) || []
const albums = (data && data.search.albums) || []
const SearchResults = ({ searchData, loading }: SearchResultsProps) => {
const { t } = useTranslation()
const query = searchData?.search.query || ''
const media = searchData?.search.media || []
const albums = searchData?.search.albums || []
let message = null
if (loading) message = t('header.search.loading', 'Loading results...')
else if (data && media.length == 0 && albums.length == 0)
else if (searchData && media.length == 0 && albums.length == 0)
message = t('header.search.no_results', 'No results found')
const albumElements = albums.map(album => (
@ -155,7 +170,7 @@ const SearchResults = ({ result }) => {
// Prevent input blur event
e.preventDefault()
}}
show={data}
show={!!searchData}
>
{message}
{albumElements.length > 0 && (
@ -174,10 +189,6 @@ const SearchResults = ({ result }) => {
)
}
SearchResults.propTypes = {
result: PropTypes.object,
}
const RowLink = styled(NavLink)`
display: flex;
align-items: center;
@ -205,31 +216,31 @@ const RowTitle = styled.span`
padding-left: 8px;
`
const PhotoRow = ({ query, media }) => (
type PhotoRowArgs = {
query: string
media: searchQuery_search_media
}
const PhotoRow = ({ query, media }: PhotoRowArgs) => (
<RowLink to={`/album/${media.album.id}`}>
<PhotoSearchThumbnail src={media?.thumbnail?.url} />
<RowTitle>{searchHighlighted(query, media.title)}</RowTitle>
</RowLink>
)
PhotoRow.propTypes = {
query: PropTypes.string.isRequired,
media: PropTypes.object.isRequired,
type AlbumRowArgs = {
query: string
album: searchQuery_search_albums
}
const AlbumRow = ({ query, album }) => (
const AlbumRow = ({ query, album }: AlbumRowArgs) => (
<RowLink to={`/album/${album.id}`}>
<AlbumSearchThumbnail src={album?.thumbnail?.thumbnail?.url} />
<RowTitle>{searchHighlighted(query, album.title)}</RowTitle>
</RowLink>
)
AlbumRow.propTypes = {
query: PropTypes.string.isRequired,
album: PropTypes.object.isRequired,
}
const searchHighlighted = (query, text) => {
const searchHighlighted = (query: string, text: string) => {
const i = text.toLowerCase().indexOf(query.toLowerCase())
if (i == -1) {

View File

@ -17,9 +17,11 @@ const Container = styled.div`
}
`
export let MessageState = {
set: null,
get: null,
export const MessageState = {
set: fn => {
console.warn('set function is not defined yet, called with', fn)
},
get: [],
add: message => {
MessageState.set(messages => {
const newMessages = messages.filter(msg => msg.key != message.key)

View File

@ -1,7 +1,9 @@
import { notificationSubscription } from './__generated__/notificationSubscription'
import PropTypes from 'prop-types'
import { useEffect } from 'react'
import { useSubscription, gql } from '@apollo/client'
import { authToken } from '../../helpers/authentication'
import { NotificationType } from '../../../__generated__/globalTypes'
const notificationSubscription = gql`
subscription notificationSubscription {
@ -18,14 +20,37 @@ const notificationSubscription = gql`
}
`
let messageTimeoutHandles = new Map()
const messageTimeoutHandles = new Map()
const SubscriptionsHook = ({ messages, setMessages }) => {
export interface Message {
key: string
type: NotificationType
timeout?: number
props: {
header: string
content: string
negative?: boolean
positive?: boolean
percent?: number
}
}
type SubscriptionHookProps = {
messages: Message[]
setMessages: React.Dispatch<React.SetStateAction<Message[]>>
}
const SubscriptionsHook = ({
messages,
setMessages,
}: SubscriptionHookProps) => {
if (!authToken()) {
return null
}
const { data, error } = useSubscription(notificationSubscription)
const { data, error } = useSubscription<notificationSubscription>(
notificationSubscription
)
useEffect(() => {
if (error) {
@ -33,7 +58,7 @@ const SubscriptionsHook = ({ messages, setMessages }) => {
...state,
{
key: Math.random().toString(26),
type: 'message',
type: NotificationType.Message,
props: {
header: 'Network error',
content: error.message,
@ -54,16 +79,16 @@ const SubscriptionsHook = ({ messages, setMessages }) => {
return
}
const newNotification = {
const newNotification: Message = {
key: msg.key,
type: msg.type.toLowerCase(),
timeout: msg.timeout,
type: msg.type,
timeout: msg.timeout || undefined,
props: {
header: msg.header,
content: msg.content,
negative: msg.negative,
positive: msg.positive,
percent: msg.progress,
percent: msg.progress || undefined,
},
}

View File

@ -1,5 +1,5 @@
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import React, { ReactChild, useEffect } from 'react'
import PropTypes, { ReactComponentLike } from 'prop-types'
import { Route, Redirect } from 'react-router-dom'
import { useLazyQuery } from '@apollo/client'
import { authToken } from '../../helpers/authentication'
@ -21,22 +21,31 @@ export const useIsAdmin = (enabled = true) => {
return data?.myUser?.admin
}
export const Authorized = ({ children }) => {
export const Authorized = ({ children }: { children: JSX.Element }) => {
const token = authToken()
return token ? children : null
}
const AuthorizedRoute = ({ component: Component, admin = false, ...props }) => {
type AuthorizedRouteProps = {
component: ReactComponentLike
admin: boolean
}
const AuthorizedRoute = ({
component: Component,
admin = false,
...props
}: AuthorizedRouteProps) => {
const token = authToken()
const isAdmin = useIsAdmin(admin)
let unauthorizedRedirect = null
let unauthorizedRedirect: null | ReactChild = null
if (!token) {
unauthorizedRedirect = <Redirect to="/login" />
}
let adminRedirect = null
let adminRedirect: null | ReactChild = null
if (token && admin) {
if (isAdmin === false) {
adminRedirect = <Redirect to="/" />

View File

@ -1,4 +1,6 @@
class LazyLoad {
observer: null | IntersectionObserver
constructor() {
this.observe = this.observe.bind(this)
this.loadImages = this.loadImages.bind(this)
@ -6,22 +8,22 @@ class LazyLoad {
this.observer = null
}
observe(images) {
observe(images: Element[]) {
if (!this.observer) {
this.observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting || entry.intersectionRatio > 0) {
const element = entry.target
this.setSrcAttribute(element)
this.observer.unobserve(element)
this.observer?.unobserve(element)
}
})
})
}
Array.from(images).forEach(image => this.observer.observe(image))
Array.from(images).forEach(image => this.observer?.observe(image))
}
loadImages(elements) {
loadImages(elements: Element[]) {
const images = Array.from(elements)
if (images.length) {
if ('IntersectionObserver' in window) {
@ -36,11 +38,18 @@ class LazyLoad {
this.observer && this.observer.disconnect()
}
setSrcAttribute(element) {
setSrcAttribute(element: Element) {
if (element.hasAttribute('data-src')) {
const src = element.getAttribute('data-src')
element.removeAttribute('data-src')
element.setAttribute('src', src)
if (src) {
element.removeAttribute('data-src')
element.setAttribute('src', src)
} else {
console.warn(
'WARN: expected element to have `data-src` property',
element
)
}
}
}
}

View File

@ -1,4 +1,4 @@
export function saveTokenCookie(token) {
export function saveTokenCookie(token: string) {
const maxAge = 14 * 24 * 60 * 60
document.cookie = `auth-token=${token} ;max-age=${maxAge} ;path=/ ;sameSite=Lax`
@ -13,11 +13,11 @@ export function authToken() {
return match && match[1]
}
export function saveSharePassword(shareToken, password) {
export function saveSharePassword(shareToken: string, password: string) {
document.cookie = `share-token-pw-${shareToken}=${password} ;path=/ ;sameSite=Lax`
}
export function getSharePassword(shareToken) {
export function getSharePassword(shareToken: string) {
const match = document.cookie.match(
`share-token-pw-${shareToken}=([\\d\\w]+)`
)

View File

@ -1,28 +0,0 @@
export function debounce(func, wait, triggerRising) {
let timeout = null
const debounced = (...args) => {
if (timeout) {
clearTimeout(timeout)
timeout = null
} else if (triggerRising) {
func(...args)
}
timeout = setTimeout(() => {
timeout = null
func(...args)
}, wait)
}
debounced.cancel = () => {
clearTimeout(timeout)
timeout = null
}
return debounced
}
export function isNil(value) {
return value === undefined || value === null
}

39
ui/src/helpers/utils.ts Normal file
View File

@ -0,0 +1,39 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export interface DebouncedFn<F extends (...args: any[]) => any> {
(...args: Parameters<F>): void
cancel(): void
}
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number,
triggerRising?: boolean
): DebouncedFn<T> {
let timeout: number | undefined = undefined
const debounced = (...args: Parameters<T>) => {
if (timeout) {
clearTimeout(timeout)
timeout = undefined
} else if (triggerRising) {
func(...args)
}
timeout = window.setTimeout(() => {
timeout = undefined
func(...args)
}, wait)
}
debounced.cancel = () => {
clearTimeout(timeout)
timeout = undefined
}
return debounced
}
export function isNil(value: any) {
return value === undefined || value === null
}

View File

@ -1,26 +1,44 @@
import { useCallback, useEffect, useRef, useState } from 'react'
const useScrollPagination = ({ loading, fetchMore, data, getItems }) => {
const observer = useRef(null)
const observerElem = useRef(null)
interface ScrollPaginationArgs<D> {
loading: boolean
data: D
fetchMore(args: { variables: { offset: number } }): Promise<{ data: D }>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getItems(data: D): any[]
}
type ScrollPaginationResult = {
finished: boolean
containerElem(node: null | Element): void
}
function useScrollPagination<D>({
loading,
fetchMore,
data,
getItems,
}: ScrollPaginationArgs<D>): ScrollPaginationResult {
const observer = useRef<IntersectionObserver | null>(null)
const observerElem = useRef<Element | null>(null)
const [finished, setFinished] = useState(false)
const reconfigureIntersectionObserver = () => {
var options = {
const options = {
root: null,
rootMargin: '-100% 0px 0px 0px',
threshold: 0,
}
// delete old observer
if (observer.current) observer.current.disconnect()
observer.current?.disconnect()
if (finished) return
// configure new observer
observer.current = new IntersectionObserver(entities => {
if (entities.find(x => x.isIntersecting == false)) {
let itemCount = getItems(data).length
const itemCount = getItems(data).length
fetchMore({
variables: {
offset: itemCount,
@ -40,7 +58,7 @@ const useScrollPagination = ({ loading, fetchMore, data, getItems }) => {
}
}
const containerElem = useCallback(node => {
const containerElem = useCallback((node: null | Element): void => {
observerElem.current = node
// cleanup
@ -55,7 +73,7 @@ const useScrollPagination = ({ loading, fetchMore, data, getItems }) => {
// only observe when not loading
useEffect(() => {
if (observer.current != null) {
if (observer.current && observerElem.current) {
if (loading) {
observer.current.unobserve(observerElem.current)
} else {

View File

@ -6,7 +6,7 @@ function useURLParameters() {
const url = new URL(urlString)
const params = new URLSearchParams(url.search)
const getParam = (key, defaultValue = null) => {
const getParam = (key: string, defaultValue = null) => {
return params.has(key) ? params.get(key) : defaultValue
}
@ -15,12 +15,12 @@ function useURLParameters() {
setUrlString(document.location.href)
}
const setParam = (key, value) => {
const setParam = (key: string, value: string) => {
params.set(key, value)
updateParams()
}
const setParams = pairs => {
const setParams = (pairs: { key: string; value: string }[]) => {
for (const pair of pairs) {
params.set(pair.key, pair.value)
}

View File

@ -1,7 +1,7 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
export default function setupLocalization() {
export default function setupLocalization(): void {
i18n.use(initReactI18next).init({
resources: {
en: {

View File

@ -4,9 +4,12 @@
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
// "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
// "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
"lib": [
"es2015",
"dom"
] /* Specify library files to be included in the compilation. */,
"allowJs": true /* Allow javascript files to be compiled. */,
// "checkJs": true, /* Report errors in .js files. */
"jsx": "preserve" /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */,
@ -48,7 +51,9 @@
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "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. */
// "typeRoots": [], /* List of folders to include type definitions from. */
"typeRoots": [
"./src/@types/"
] /* List of folders to include type definitions from. */,
// "types": [], /* 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. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
@ -68,5 +73,6 @@
/* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
},
"exclude": ["node_modules/", "dist/"]
}