Continue with typescript migration
This commit is contained in:
parent
264ee54e7f
commit
c5d2f3dc8b
|
@ -1,10 +1,18 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
es6: true,
|
es6: true,
|
||||||
},
|
},
|
||||||
ignorePatterns: ['**/*.ts', '**/*.tsx'],
|
ignorePatterns: ['node_modules', 'dist'],
|
||||||
extends: ['eslint:recommended', 'plugin:react/recommended'],
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:@typescript-eslint/eslint-recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'prettier',
|
||||||
|
],
|
||||||
globals: {
|
globals: {
|
||||||
Atomics: 'readonly',
|
Atomics: 'readonly',
|
||||||
SharedArrayBuffer: 'readonly',
|
SharedArrayBuffer: 'readonly',
|
||||||
|
@ -19,17 +27,20 @@ module.exports = {
|
||||||
ecmaVersion: 2018,
|
ecmaVersion: 2018,
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
plugins: ['react', 'react-hooks'],
|
plugins: ['react', 'react-hooks', '@typescript-eslint'],
|
||||||
rules: {
|
rules: {
|
||||||
'no-unused-vars': 'warn',
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': 'warn',
|
||||||
'react/display-name': 'off',
|
'react/display-name': 'off',
|
||||||
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
version: 'detect',
|
version: 'detect',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
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'],
|
||||||
|
@ -43,5 +54,11 @@ module.exports = {
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
|
files: ['**/*.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ module.exports = function (api) {
|
||||||
const isTest = api.env('test')
|
const isTest = api.env('test')
|
||||||
const isProduction = api.env('NODE_ENV') == 'production'
|
const isProduction = api.env('NODE_ENV') == 'production'
|
||||||
|
|
||||||
let presets = ['@babel/preset-react']
|
let presets = ['@babel/preset-typescript', '@babel/preset-react']
|
||||||
let plugins = []
|
let plugins = []
|
||||||
|
|
||||||
if (isTest) {
|
if (isTest) {
|
||||||
|
|
20
ui/build.mjs
20
ui/build.mjs
|
@ -23,7 +23,7 @@ const esbuildOptions = {
|
||||||
entryPoints: ['src/index.tsx'],
|
entryPoints: ['src/index.tsx'],
|
||||||
plugins: [
|
plugins: [
|
||||||
babel({
|
babel({
|
||||||
filter: /photoview\/ui\/src\/.*\.js$/,
|
filter: /photoview\/ui\/src\/.*\.(js|tsx?)$/,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
publicPath: process.env.UI_PUBLIC_URL || '/',
|
publicPath: process.env.UI_PUBLIC_URL || '/',
|
||||||
|
@ -66,25 +66,25 @@ if (watchMode) {
|
||||||
open: false,
|
open: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
bs.watch('src/**/*.js').on('change', async args => {
|
bs.watch('src/**/*.@(js|tsx|ts)').on('change', async args => {
|
||||||
console.log('reloading', args)
|
console.log('reloading', args)
|
||||||
builderPromise = (await builderPromise).rebuild()
|
builderPromise = (await builderPromise).rebuild()
|
||||||
bs.reload(args)
|
bs.reload(args)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const esbuildPromise = esbuild
|
const build = async () => {
|
||||||
.build(esbuildOptions)
|
await esbuild.build(esbuildOptions)
|
||||||
.then(() => console.log('esbuild done'))
|
|
||||||
|
|
||||||
const workboxPromise = workboxBuild
|
console.log('esbuild done')
|
||||||
.generateSW({
|
|
||||||
|
await workboxBuild.generateSW({
|
||||||
globDirectory: 'dist/',
|
globDirectory: 'dist/',
|
||||||
globPatterns: ['**/*.{png,svg,woff2,ttf,eot,woff,js,ico,html,json,css}'],
|
globPatterns: ['**/*.{png,svg,woff2,ttf,eot,woff,js,ico,html,json,css}'],
|
||||||
swDest: 'dist/service-worker.js',
|
swDest: 'dist/service-worker.js',
|
||||||
})
|
})
|
||||||
.then(() => console.log('workbox done'))
|
|
||||||
|
|
||||||
Promise.all([esbuildPromise, workboxPromise]).then(() =>
|
console.log('workbox done')
|
||||||
console.log('build complete')
|
console.log('build complete')
|
||||||
)
|
}
|
||||||
|
build()
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -15,6 +15,7 @@
|
||||||
"@babel/plugin-transform-runtime": "^7.13.15",
|
"@babel/plugin-transform-runtime": "^7.13.15",
|
||||||
"@babel/preset-env": "^7.13.15",
|
"@babel/preset-env": "^7.13.15",
|
||||||
"@babel/preset-react": "^7.13.13",
|
"@babel/preset-react": "^7.13.13",
|
||||||
|
"@babel/preset-typescript": "^7.13.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^26.6.3",
|
"babel-jest": "^26.6.3",
|
||||||
"babel-plugin-graphql-tag": "^3.2.0",
|
"babel-plugin-graphql-tag": "^3.2.0",
|
||||||
|
@ -56,7 +57,7 @@
|
||||||
"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",
|
"lint": "eslint ./src --max-warnings 0 --cache --config .eslintrc.js",
|
||||||
"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)"
|
||||||
|
@ -69,6 +70,10 @@
|
||||||
"@types/react-helmet": "^6.1.1",
|
"@types/react-helmet": "^6.1.1",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"@types/styled-components": "^5.1.9",
|
"@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",
|
"husky": "^6.0.0",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"lint-staged": "^10.5.4",
|
"lint-staged": "^10.5.4",
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
declare module '*.svg' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
|
@ -80,7 +80,7 @@ const SideButtonLink = styled(NavLink)`
|
||||||
`
|
`
|
||||||
|
|
||||||
type SideButtonProps = {
|
type SideButtonProps = {
|
||||||
children: any
|
children: ReactChild | ReactChild[]
|
||||||
to: string
|
to: string
|
||||||
exact: boolean
|
exact: boolean
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import {
|
||||||
split,
|
split,
|
||||||
ApolloLink,
|
ApolloLink,
|
||||||
HttpLink,
|
HttpLink,
|
||||||
|
ServerError,
|
||||||
|
FieldMergeFunction,
|
||||||
} from '@apollo/client'
|
} from '@apollo/client'
|
||||||
import { getMainDefinition } from '@apollo/client/utilities'
|
import { getMainDefinition } from '@apollo/client/utilities'
|
||||||
import { onError } from '@apollo/client/link/error'
|
import { onError } from '@apollo/client/link/error'
|
||||||
|
@ -12,6 +14,7 @@ import { WebSocketLink } from '@apollo/client/link/ws'
|
||||||
import urlJoin from 'url-join'
|
import urlJoin from 'url-join'
|
||||||
import { clearTokenCookie } from './helpers/authentication'
|
import { clearTokenCookie } from './helpers/authentication'
|
||||||
import { MessageState } from './components/messages/Messages'
|
import { MessageState } from './components/messages/Messages'
|
||||||
|
import { Message } from './components/messages/SubscriptionsHook'
|
||||||
|
|
||||||
export const GRAPHQL_ENDPOINT = process.env.PHOTOVIEW_API_ENDPOINT
|
export const GRAPHQL_ENDPOINT = process.env.PHOTOVIEW_API_ENDPOINT
|
||||||
? urlJoin(process.env.PHOTOVIEW_API_ENDPOINT, '/graphql')
|
? urlJoin(process.env.PHOTOVIEW_API_ENDPOINT, '/graphql')
|
||||||
|
@ -26,12 +29,12 @@ console.log('GRAPHQL ENDPOINT', GRAPHQL_ENDPOINT)
|
||||||
|
|
||||||
const apiProtocol = new URL(GRAPHQL_ENDPOINT).protocol
|
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:'
|
websocketUri.protocol = apiProtocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
|
|
||||||
const wsLink = new WebSocketLink({
|
const wsLink = new WebSocketLink({
|
||||||
uri: websocketUri,
|
uri: websocketUri.toString(),
|
||||||
credentials: 'include',
|
// credentials: 'include',
|
||||||
})
|
})
|
||||||
|
|
||||||
const link = split(
|
const link = split(
|
||||||
|
@ -48,7 +51,7 @@ const link = split(
|
||||||
)
|
)
|
||||||
|
|
||||||
const linkError = onError(({ graphQLErrors, networkError }) => {
|
const linkError = onError(({ graphQLErrors, networkError }) => {
|
||||||
let errorMessages = []
|
const errorMessages = []
|
||||||
|
|
||||||
if (graphQLErrors) {
|
if (graphQLErrors) {
|
||||||
graphQLErrors.map(({ message, locations, path }) =>
|
graphQLErrors.map(({ message, locations, path }) =>
|
||||||
|
@ -82,7 +85,7 @@ const linkError = onError(({ graphQLErrors, networkError }) => {
|
||||||
console.log(`[Network error]: ${JSON.stringify(networkError)}`)
|
console.log(`[Network error]: ${JSON.stringify(networkError)}`)
|
||||||
clearTokenCookie()
|
clearTokenCookie()
|
||||||
|
|
||||||
const errors = networkError.result.errors
|
const errors = (networkError as ServerError).result.errors
|
||||||
|
|
||||||
if (errors.length == 1) {
|
if (errors.length == 1) {
|
||||||
errorMessages.push({
|
errorMessages.push({
|
||||||
|
@ -92,7 +95,9 @@ const linkError = onError(({ graphQLErrors, networkError }) => {
|
||||||
} else if (errors.length > 1) {
|
} else if (errors.length > 1) {
|
||||||
errorMessages.push({
|
errorMessages.push({
|
||||||
header: 'Multiple server errors',
|
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,
|
...msg,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
MessageState.set(messages => [...messages, ...newMessages])
|
MessageState.set((messages: Message[]) => [...messages, ...newMessages])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
type PaginateCacheType = {
|
||||||
|
keyArgs: string[]
|
||||||
|
merge: FieldMergeFunction
|
||||||
|
}
|
||||||
|
|
||||||
// Modified version of Apollo's offsetLimitPagination()
|
// Modified version of Apollo's offsetLimitPagination()
|
||||||
const paginateCache = keyArgs => ({
|
const paginateCache = (keyArgs: string[]) =>
|
||||||
keyArgs,
|
({
|
||||||
merge(existing, incoming, { args, fieldName }) {
|
keyArgs,
|
||||||
const merged = existing ? existing.slice(0) : []
|
merge(existing, incoming, { args, fieldName }) {
|
||||||
if (args?.paginate) {
|
const merged = existing ? existing.slice(0) : []
|
||||||
const { offset = 0 } = args.paginate
|
if (args?.paginate) {
|
||||||
for (let i = 0; i < incoming.length; ++i) {
|
const { offset = 0 } = args.paginate
|
||||||
merged[offset + i] = incoming[i]
|
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 {
|
return merged
|
||||||
throw new Error(`Paginate argument is missing for query: ${fieldName}`)
|
},
|
||||||
}
|
} as PaginateCacheType)
|
||||||
return merged
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const memoryCache = new InMemoryCache({
|
const memoryCache = new InMemoryCache({
|
||||||
typePolicies: {
|
typePolicies: {
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect, useContext } from 'react'
|
import React, { useEffect, useContext } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
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 { Link } from 'react-router-dom'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { Icon } from 'semantic-ui-react'
|
import { Icon } from 'semantic-ui-react'
|
||||||
|
@ -8,6 +8,7 @@ import { SidebarContext } from './sidebar/Sidebar'
|
||||||
import AlbumSidebar from './sidebar/AlbumSidebar'
|
import AlbumSidebar from './sidebar/AlbumSidebar'
|
||||||
import { useLazyQuery, gql } from '@apollo/client'
|
import { useLazyQuery, gql } from '@apollo/client'
|
||||||
import { authToken } from '../helpers/authentication'
|
import { authToken } from '../helpers/authentication'
|
||||||
|
import { albumPathQuery } from './__generated__/albumPathQuery'
|
||||||
|
|
||||||
const Header = styled.h1`
|
const Header = styled.h1`
|
||||||
margin: 24px 0 8px 0 !important;
|
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} />
|
return <StyledIcon name="settings" size="small" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,8 +49,18 @@ const ALBUM_PATH_QUERY = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const AlbumTitle = ({ album, disableLink = false }) => {
|
type AlbumTitleProps = {
|
||||||
const [fetchPath, { data: pathData }] = useLazyQuery(ALBUM_PATH_QUERY)
|
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)
|
const { updateSidebar } = useContext(SidebarContext)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -68,10 +79,7 @@ const AlbumTitle = ({ album, disableLink = false }) => {
|
||||||
|
|
||||||
let title = <span>{album.title}</span>
|
let title = <span>{album.title}</span>
|
||||||
|
|
||||||
let path = []
|
const path = pathData?.album.path || []
|
||||||
if (pathData) {
|
|
||||||
path = pathData.album.path
|
|
||||||
}
|
|
||||||
|
|
||||||
const breadcrumbSections = path
|
const breadcrumbSections = path
|
||||||
.slice()
|
.slice()
|
|
@ -1,8 +1,12 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { Loader } from 'semantic-ui-react'
|
import { Loader } from 'semantic-ui-react'
|
||||||
|
|
||||||
const PaginateLoader = ({ active, text }) => (
|
type PaginateLoaderProps = {
|
||||||
|
active: boolean
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaginateLoader = ({ active, text }: PaginateLoaderProps) => (
|
||||||
<Loader
|
<Loader
|
||||||
style={{ margin: '42px 0 24px 0', opacity: active ? '1' : '0' }}
|
style={{ margin: '42px 0 24px 0', opacity: active ? '1' : '0' }}
|
||||||
inline="centered"
|
inline="centered"
|
||||||
|
@ -12,9 +16,4 @@ const PaginateLoader = ({ active, text }) => (
|
||||||
</Loader>
|
</Loader>
|
||||||
)
|
)
|
||||||
|
|
||||||
PaginateLoader.propTypes = {
|
|
||||||
active: PropTypes.bool,
|
|
||||||
text: PropTypes.string,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PaginateLoader
|
export default PaginateLoader
|
|
@ -1,11 +1,15 @@
|
||||||
import React, { useState, useRef, useEffect } from 'react'
|
import React, { useState, useRef, useEffect } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { useLazyQuery, gql } from '@apollo/client'
|
import { useLazyQuery, gql } from '@apollo/client'
|
||||||
import { debounce } from '../../helpers/utils'
|
import { debounce, DebouncedFn } from '../../helpers/utils'
|
||||||
import { ProtectedImage } from '../photoGallery/ProtectedMedia'
|
import { ProtectedImage } from '../photoGallery/ProtectedMedia'
|
||||||
import { NavLink } from 'react-router-dom'
|
import { NavLink } from 'react-router-dom'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import {
|
||||||
|
searchQuery,
|
||||||
|
searchQuery_search_albums,
|
||||||
|
searchQuery_search_media,
|
||||||
|
} from './__generated__/searchQuery'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
height: 60px;
|
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')};
|
display: ${({ show }) => (show ? 'block' : 'none')};
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -79,27 +83,29 @@ const SEARCH_QUERY = gql`
|
||||||
|
|
||||||
const SearchBar = () => {
|
const SearchBar = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [fetchSearches, fetchResult] = useLazyQuery(SEARCH_QUERY)
|
const [fetchSearches, fetchResult] = useLazyQuery<searchQuery>(SEARCH_QUERY)
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const [fetched, setFetched] = useState(false)
|
const [fetched, setFetched] = useState(false)
|
||||||
|
|
||||||
let debouncedFetch = useRef(null)
|
type QueryFn = (query: string) => void
|
||||||
|
|
||||||
|
const debouncedFetch = useRef<null | DebouncedFn<QueryFn>>(null)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
debouncedFetch.current = debounce(query => {
|
debouncedFetch.current = debounce<QueryFn>(query => {
|
||||||
fetchSearches({ variables: { query } })
|
fetchSearches({ variables: { query } })
|
||||||
setFetched(true)
|
setFetched(true)
|
||||||
}, 250)
|
}, 250)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
debouncedFetch.current.cancel()
|
debouncedFetch.current?.cancel()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const fetchEvent = e => {
|
const fetchEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
e.persist()
|
e.persist()
|
||||||
|
|
||||||
setQuery(e.target.value)
|
setQuery(e.target.value)
|
||||||
if (e.target.value.trim() != '') {
|
if (e.target.value.trim() != '' && debouncedFetch.current) {
|
||||||
debouncedFetch.current(e.target.value.trim())
|
debouncedFetch.current(e.target.value.trim())
|
||||||
} else {
|
} else {
|
||||||
setFetched(false)
|
setFetched(false)
|
||||||
|
@ -108,7 +114,12 @@ const SearchBar = () => {
|
||||||
|
|
||||||
let results = null
|
let results = null
|
||||||
if (query.trim().length > 0 && fetched) {
|
if (query.trim().length > 0 && fetched) {
|
||||||
results = <SearchResults result={fetchResult} />
|
results = (
|
||||||
|
<SearchResults
|
||||||
|
searchData={fetchResult.data}
|
||||||
|
loading={fetchResult.loading}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -128,17 +139,21 @@ const ResultTitle = styled.h1`
|
||||||
margin: 12px 0 0.25rem;
|
margin: 12px 0 0.25rem;
|
||||||
`
|
`
|
||||||
|
|
||||||
const SearchResults = ({ result }) => {
|
type SearchResultsProps = {
|
||||||
const { t } = useTranslation()
|
searchData?: searchQuery
|
||||||
const { data, loading } = result
|
loading: boolean
|
||||||
const query = data && data.search.query
|
}
|
||||||
|
|
||||||
const media = (data && data.search.media) || []
|
const SearchResults = ({ searchData, loading }: SearchResultsProps) => {
|
||||||
const albums = (data && data.search.albums) || []
|
const { t } = useTranslation()
|
||||||
|
const query = searchData?.search.query || ''
|
||||||
|
|
||||||
|
const media = searchData?.search.media || []
|
||||||
|
const albums = searchData?.search.albums || []
|
||||||
|
|
||||||
let message = null
|
let message = null
|
||||||
if (loading) message = t('header.search.loading', 'Loading results...')
|
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')
|
message = t('header.search.no_results', 'No results found')
|
||||||
|
|
||||||
const albumElements = albums.map(album => (
|
const albumElements = albums.map(album => (
|
||||||
|
@ -155,7 +170,7 @@ const SearchResults = ({ result }) => {
|
||||||
// Prevent input blur event
|
// Prevent input blur event
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}}
|
}}
|
||||||
show={data}
|
show={!!searchData}
|
||||||
>
|
>
|
||||||
{message}
|
{message}
|
||||||
{albumElements.length > 0 && (
|
{albumElements.length > 0 && (
|
||||||
|
@ -174,10 +189,6 @@ const SearchResults = ({ result }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchResults.propTypes = {
|
|
||||||
result: PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
const RowLink = styled(NavLink)`
|
const RowLink = styled(NavLink)`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -205,31 +216,31 @@ const RowTitle = styled.span`
|
||||||
padding-left: 8px;
|
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}`}>
|
<RowLink to={`/album/${media.album.id}`}>
|
||||||
<PhotoSearchThumbnail src={media?.thumbnail?.url} />
|
<PhotoSearchThumbnail src={media?.thumbnail?.url} />
|
||||||
<RowTitle>{searchHighlighted(query, media.title)}</RowTitle>
|
<RowTitle>{searchHighlighted(query, media.title)}</RowTitle>
|
||||||
</RowLink>
|
</RowLink>
|
||||||
)
|
)
|
||||||
|
|
||||||
PhotoRow.propTypes = {
|
type AlbumRowArgs = {
|
||||||
query: PropTypes.string.isRequired,
|
query: string
|
||||||
media: PropTypes.object.isRequired,
|
album: searchQuery_search_albums
|
||||||
}
|
}
|
||||||
|
|
||||||
const AlbumRow = ({ query, album }) => (
|
const AlbumRow = ({ query, album }: AlbumRowArgs) => (
|
||||||
<RowLink to={`/album/${album.id}`}>
|
<RowLink to={`/album/${album.id}`}>
|
||||||
<AlbumSearchThumbnail src={album?.thumbnail?.thumbnail?.url} />
|
<AlbumSearchThumbnail src={album?.thumbnail?.thumbnail?.url} />
|
||||||
<RowTitle>{searchHighlighted(query, album.title)}</RowTitle>
|
<RowTitle>{searchHighlighted(query, album.title)}</RowTitle>
|
||||||
</RowLink>
|
</RowLink>
|
||||||
)
|
)
|
||||||
|
|
||||||
AlbumRow.propTypes = {
|
const searchHighlighted = (query: string, text: string) => {
|
||||||
query: PropTypes.string.isRequired,
|
|
||||||
album: PropTypes.object.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchHighlighted = (query, text) => {
|
|
||||||
const i = text.toLowerCase().indexOf(query.toLowerCase())
|
const i = text.toLowerCase().indexOf(query.toLowerCase())
|
||||||
|
|
||||||
if (i == -1) {
|
if (i == -1) {
|
|
@ -17,9 +17,11 @@ const Container = styled.div`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export let MessageState = {
|
export const MessageState = {
|
||||||
set: null,
|
set: fn => {
|
||||||
get: null,
|
console.warn('set function is not defined yet, called with', fn)
|
||||||
|
},
|
||||||
|
get: [],
|
||||||
add: message => {
|
add: message => {
|
||||||
MessageState.set(messages => {
|
MessageState.set(messages => {
|
||||||
const newMessages = messages.filter(msg => msg.key != message.key)
|
const newMessages = messages.filter(msg => msg.key != message.key)
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
import { notificationSubscription } from './__generated__/notificationSubscription'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useSubscription, gql } from '@apollo/client'
|
import { useSubscription, gql } from '@apollo/client'
|
||||||
import { authToken } from '../../helpers/authentication'
|
import { authToken } from '../../helpers/authentication'
|
||||||
|
import { NotificationType } from '../../../__generated__/globalTypes'
|
||||||
|
|
||||||
const notificationSubscription = gql`
|
const notificationSubscription = gql`
|
||||||
subscription notificationSubscription {
|
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()) {
|
if (!authToken()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = useSubscription(notificationSubscription)
|
const { data, error } = useSubscription<notificationSubscription>(
|
||||||
|
notificationSubscription
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -33,7 +58,7 @@ const SubscriptionsHook = ({ messages, setMessages }) => {
|
||||||
...state,
|
...state,
|
||||||
{
|
{
|
||||||
key: Math.random().toString(26),
|
key: Math.random().toString(26),
|
||||||
type: 'message',
|
type: NotificationType.Message,
|
||||||
props: {
|
props: {
|
||||||
header: 'Network error',
|
header: 'Network error',
|
||||||
content: error.message,
|
content: error.message,
|
||||||
|
@ -54,16 +79,16 @@ const SubscriptionsHook = ({ messages, setMessages }) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newNotification = {
|
const newNotification: Message = {
|
||||||
key: msg.key,
|
key: msg.key,
|
||||||
type: msg.type.toLowerCase(),
|
type: msg.type,
|
||||||
timeout: msg.timeout,
|
timeout: msg.timeout || undefined,
|
||||||
props: {
|
props: {
|
||||||
header: msg.header,
|
header: msg.header,
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
negative: msg.negative,
|
negative: msg.negative,
|
||||||
positive: msg.positive,
|
positive: msg.positive,
|
||||||
percent: msg.progress,
|
percent: msg.progress || undefined,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useEffect } from 'react'
|
import React, { ReactChild, useEffect } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes, { ReactComponentLike } from 'prop-types'
|
||||||
import { Route, Redirect } from 'react-router-dom'
|
import { Route, Redirect } from 'react-router-dom'
|
||||||
import { useLazyQuery } from '@apollo/client'
|
import { useLazyQuery } from '@apollo/client'
|
||||||
import { authToken } from '../../helpers/authentication'
|
import { authToken } from '../../helpers/authentication'
|
||||||
|
@ -21,22 +21,31 @@ export const useIsAdmin = (enabled = true) => {
|
||||||
return data?.myUser?.admin
|
return data?.myUser?.admin
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Authorized = ({ children }) => {
|
export const Authorized = ({ children }: { children: JSX.Element }) => {
|
||||||
const token = authToken()
|
const token = authToken()
|
||||||
|
|
||||||
return token ? children : null
|
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 token = authToken()
|
||||||
const isAdmin = useIsAdmin(admin)
|
const isAdmin = useIsAdmin(admin)
|
||||||
|
|
||||||
let unauthorizedRedirect = null
|
let unauthorizedRedirect: null | ReactChild = null
|
||||||
if (!token) {
|
if (!token) {
|
||||||
unauthorizedRedirect = <Redirect to="/login" />
|
unauthorizedRedirect = <Redirect to="/login" />
|
||||||
}
|
}
|
||||||
|
|
||||||
let adminRedirect = null
|
let adminRedirect: null | ReactChild = null
|
||||||
if (token && admin) {
|
if (token && admin) {
|
||||||
if (isAdmin === false) {
|
if (isAdmin === false) {
|
||||||
adminRedirect = <Redirect to="/" />
|
adminRedirect = <Redirect to="/" />
|
|
@ -1,4 +1,6 @@
|
||||||
class LazyLoad {
|
class LazyLoad {
|
||||||
|
observer: null | IntersectionObserver
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.observe = this.observe.bind(this)
|
this.observe = this.observe.bind(this)
|
||||||
this.loadImages = this.loadImages.bind(this)
|
this.loadImages = this.loadImages.bind(this)
|
||||||
|
@ -6,22 +8,22 @@ class LazyLoad {
|
||||||
this.observer = null
|
this.observer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
observe(images) {
|
observe(images: Element[]) {
|
||||||
if (!this.observer) {
|
if (!this.observer) {
|
||||||
this.observer = new IntersectionObserver(entries => {
|
this.observer = new IntersectionObserver(entries => {
|
||||||
entries.forEach(entry => {
|
entries.forEach(entry => {
|
||||||
if (entry.isIntersecting || entry.intersectionRatio > 0) {
|
if (entry.isIntersecting || entry.intersectionRatio > 0) {
|
||||||
const element = entry.target
|
const element = entry.target
|
||||||
this.setSrcAttribute(element)
|
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)
|
const images = Array.from(elements)
|
||||||
if (images.length) {
|
if (images.length) {
|
||||||
if ('IntersectionObserver' in window) {
|
if ('IntersectionObserver' in window) {
|
||||||
|
@ -36,11 +38,18 @@ class LazyLoad {
|
||||||
this.observer && this.observer.disconnect()
|
this.observer && this.observer.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
setSrcAttribute(element) {
|
setSrcAttribute(element: Element) {
|
||||||
if (element.hasAttribute('data-src')) {
|
if (element.hasAttribute('data-src')) {
|
||||||
const src = element.getAttribute('data-src')
|
const src = element.getAttribute('data-src')
|
||||||
element.removeAttribute('data-src')
|
if (src) {
|
||||||
element.setAttribute('src', src)
|
element.removeAttribute('data-src')
|
||||||
|
element.setAttribute('src', src)
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
'WARN: expected element to have `data-src` property',
|
||||||
|
element
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
export function saveTokenCookie(token) {
|
export function saveTokenCookie(token: string) {
|
||||||
const maxAge = 14 * 24 * 60 * 60
|
const maxAge = 14 * 24 * 60 * 60
|
||||||
|
|
||||||
document.cookie = `auth-token=${token} ;max-age=${maxAge} ;path=/ ;sameSite=Lax`
|
document.cookie = `auth-token=${token} ;max-age=${maxAge} ;path=/ ;sameSite=Lax`
|
||||||
|
@ -13,11 +13,11 @@ export function authToken() {
|
||||||
return match && match[1]
|
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`
|
document.cookie = `share-token-pw-${shareToken}=${password} ;path=/ ;sameSite=Lax`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSharePassword(shareToken) {
|
export function getSharePassword(shareToken: string) {
|
||||||
const match = document.cookie.match(
|
const match = document.cookie.match(
|
||||||
`share-token-pw-${shareToken}=([\\d\\w]+)`
|
`share-token-pw-${shareToken}=([\\d\\w]+)`
|
||||||
)
|
)
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
|
@ -1,26 +1,44 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
const useScrollPagination = ({ loading, fetchMore, data, getItems }) => {
|
interface ScrollPaginationArgs<D> {
|
||||||
const observer = useRef(null)
|
loading: boolean
|
||||||
const observerElem = useRef(null)
|
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 [finished, setFinished] = useState(false)
|
||||||
|
|
||||||
const reconfigureIntersectionObserver = () => {
|
const reconfigureIntersectionObserver = () => {
|
||||||
var options = {
|
const options = {
|
||||||
root: null,
|
root: null,
|
||||||
rootMargin: '-100% 0px 0px 0px',
|
rootMargin: '-100% 0px 0px 0px',
|
||||||
threshold: 0,
|
threshold: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete old observer
|
// delete old observer
|
||||||
if (observer.current) observer.current.disconnect()
|
observer.current?.disconnect()
|
||||||
|
|
||||||
if (finished) return
|
if (finished) return
|
||||||
|
|
||||||
// configure new observer
|
// configure new observer
|
||||||
observer.current = new IntersectionObserver(entities => {
|
observer.current = new IntersectionObserver(entities => {
|
||||||
if (entities.find(x => x.isIntersecting == false)) {
|
if (entities.find(x => x.isIntersecting == false)) {
|
||||||
let itemCount = getItems(data).length
|
const itemCount = getItems(data).length
|
||||||
fetchMore({
|
fetchMore({
|
||||||
variables: {
|
variables: {
|
||||||
offset: itemCount,
|
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
|
observerElem.current = node
|
||||||
|
|
||||||
// cleanup
|
// cleanup
|
||||||
|
@ -55,7 +73,7 @@ const useScrollPagination = ({ loading, fetchMore, data, getItems }) => {
|
||||||
|
|
||||||
// only observe when not loading
|
// only observe when not loading
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (observer.current != null) {
|
if (observer.current && observerElem.current) {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
observer.current.unobserve(observerElem.current)
|
observer.current.unobserve(observerElem.current)
|
||||||
} else {
|
} else {
|
|
@ -6,7 +6,7 @@ function useURLParameters() {
|
||||||
const url = new URL(urlString)
|
const url = new URL(urlString)
|
||||||
const params = new URLSearchParams(url.search)
|
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
|
return params.has(key) ? params.get(key) : defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,12 +15,12 @@ function useURLParameters() {
|
||||||
setUrlString(document.location.href)
|
setUrlString(document.location.href)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setParam = (key, value) => {
|
const setParam = (key: string, value: string) => {
|
||||||
params.set(key, value)
|
params.set(key, value)
|
||||||
updateParams()
|
updateParams()
|
||||||
}
|
}
|
||||||
|
|
||||||
const setParams = pairs => {
|
const setParams = (pairs: { key: string; value: string }[]) => {
|
||||||
for (const pair of pairs) {
|
for (const pair of pairs) {
|
||||||
params.set(pair.key, pair.value)
|
params.set(pair.key, pair.value)
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import i18n from 'i18next'
|
import i18n from 'i18next'
|
||||||
import { initReactI18next } from 'react-i18next'
|
import { initReactI18next } from 'react-i18next'
|
||||||
|
|
||||||
export default function setupLocalization() {
|
export default function setupLocalization(): void {
|
||||||
i18n.use(initReactI18next).init({
|
i18n.use(initReactI18next).init({
|
||||||
resources: {
|
resources: {
|
||||||
en: {
|
en: {
|
|
@ -4,9 +4,12 @@
|
||||||
|
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
// "incremental": true, /* Enable incremental compilation */
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
// "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
"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'. */
|
"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. */
|
"lib": [
|
||||||
|
"es2015",
|
||||||
|
"dom"
|
||||||
|
] /* Specify library files to be included in the compilation. */,
|
||||||
"allowJs": true /* Allow javascript files to be compiled. */,
|
"allowJs": true /* Allow javascript files to be compiled. */,
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
"jsx": "preserve" /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */,
|
"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. */
|
// "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'. */
|
// "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": [], /* 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. */
|
// "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. */
|
// "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'. */,
|
||||||
|
@ -68,5 +73,6 @@
|
||||||
/* Advanced Options */
|
/* Advanced Options */
|
||||||
"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/"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue