Continue with typescript migration
This commit is contained in:
parent
264ee54e7f
commit
c5d2f3dc8b
|
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
20
ui/build.mjs
20
ui/build.mjs
|
@ -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()
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
declare module '*.svg' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
|
@ -80,7 +80,7 @@ const SideButtonLink = styled(NavLink)`
|
|||
`
|
||||
|
||||
type SideButtonProps = {
|
||||
children: any
|
||||
children: ReactChild | ReactChild[]
|
||||
to: string
|
||||
exact: boolean
|
||||
}
|
||||
|
|
|
@ -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: {
|
|
@ -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()
|
|
@ -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
|
|
@ -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) {
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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="/" />
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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]+)`
|
||||
)
|
|
@ -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'
|
||||
|
||||
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 {
|
|
@ -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)
|
||||
}
|
|
@ -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: {
|
|
@ -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/"]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue