kopia lustrzana https://github.com/Stopka/fedisearch
Properly applied code quality tools
rodzic
8958ab363a
commit
2dda770993
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es2021": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@next/next/recommended",
|
|
||||||
"standard"
|
|
||||||
],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true
|
|
||||||
},
|
|
||||||
"ecmaVersion": 12,
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"react",
|
|
||||||
"@typescript-eslint",
|
|
||||||
"react",
|
|
||||||
"react-hooks",
|
|
||||||
"jsx-a11y",
|
|
||||||
"import",
|
|
||||||
"@next/next"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"no-use-before-define": "off",
|
|
||||||
"@typescript-eslint/no-use-before-define": ["error"]
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"react": {
|
|
||||||
"version": "detect"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"printWidth": 100,
|
|
||||||
"singleQuote": true
|
|
||||||
}
|
|
|
@ -15,5 +15,8 @@ module.exports = {
|
||||||
permanent: true
|
permanent: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
typescript: {
|
||||||
|
tsconfigPath: '../tsconfig.json'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Plik diff jest za duży
Load Diff
|
@ -16,17 +16,20 @@
|
||||||
"@apollo/client": "^3.6.9",
|
"@apollo/client": "^3.6.9",
|
||||||
"@datapunt/matomo-tracker-js": "^0.5.1",
|
"@datapunt/matomo-tracker-js": "^0.5.1",
|
||||||
"@elastic/elasticsearch": "^8.2.1",
|
"@elastic/elasticsearch": "^8.2.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
"@fortawesome/fontawesome-common-types": "^6.2.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
"@fortawesome/fontawesome-svg-core": "^6.2.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
"@fortawesome/free-brands-svg-icons": "^6.2.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
"@fortawesome/free-regular-svg-icons": "^6.2.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.17",
|
"@fortawesome/free-solid-svg-icons": "^6.2.0",
|
||||||
"@hookform/resolvers": "^2.8.5",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
"@hookform/resolvers": "^2.9.10",
|
||||||
|
"@popperjs/core": "^2.11.6",
|
||||||
"@svgr/webpack": "^6.2.1",
|
"@svgr/webpack": "^6.2.1",
|
||||||
"apollo-server-micro": "^3.10.1",
|
"apollo-server-micro": "^3.10.1",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"bootstrap": "^5.1.3",
|
"bootstrap": "^5.1.3",
|
||||||
"graphql": "^16.5.0",
|
"graphql": "^16.5.0",
|
||||||
|
"micro": "^9.4.1",
|
||||||
"micro-cors": "^0.1.1",
|
"micro-cors": "^0.1.1",
|
||||||
"next": "^12.2.5",
|
"next": "^12.2.5",
|
||||||
"nexus": "^1.3.0",
|
"nexus": "^1.3.0",
|
||||||
|
@ -40,25 +43,48 @@
|
||||||
"zod": "^3.11.6"
|
"zod": "^3.11.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/eslint-plugin-next": "^12.0.7",
|
"@next/eslint-plugin-next": "^13.0.0",
|
||||||
"@types/jest": "^27.0.2",
|
"@types/jest": "^29.2.0",
|
||||||
"@types/micro-cors": "^0.1.2",
|
"@types/micro-cors": "^0.1.2",
|
||||||
"@types/node": "^18.7.18",
|
"@types/node": "^18.7.18",
|
||||||
"@types/npmlog": "^4.1.3",
|
"@types/npmlog": "^4.1.3",
|
||||||
"@types/react": "^17.0.14",
|
"@types/react": "^17.0.14",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
"@typescript-eslint/eslint-plugin": "^5.37.0",
|
||||||
"@typescript-eslint/parser": "^5.4.0",
|
"@typescript-eslint/parser": "^5.37.0",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^8.0.1",
|
||||||
"eslint-config-standard": "^16.0.3",
|
"eslint-config-standard-react": "^12.0.0",
|
||||||
"eslint-plugin-import": "^2.25.3",
|
"eslint-config-standard-with-typescript": "^23.0.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-n": "^15.2.5",
|
||||||
"eslint-plugin-promise": "^5.1.1",
|
"eslint-plugin-promise": "^6.0.1",
|
||||||
"eslint-plugin-react": "^7.27.1",
|
"eslint-plugin-react": "^7.31.8",
|
||||||
"eslint-plugin-react-hooks": "^4.3.0",
|
"jest": "^29.2.2",
|
||||||
"jest": "^27.3.0",
|
"ts-jest": "^29.0.3",
|
||||||
"standard": "*",
|
|
||||||
"ts-jest": "^27.0.7",
|
|
||||||
"typescript": "^4.3.5"
|
"typescript": "^4.3.5"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"standard-with-typescript",
|
||||||
|
"standard-react"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"project": [
|
||||||
|
"tsconfig.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-misused-promises": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"checksVoidReturn": {
|
||||||
|
"attributes": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import FallbackImage from './FallbackImage'
|
import FallbackImage from './FallbackImage'
|
||||||
|
|
||||||
const Avatar:React.FC<{url:string|null|undefined}> = ({ url }) => {
|
const Avatar: React.FC<{ url: string | null | undefined }> = ({ url }) => {
|
||||||
return (
|
return (
|
||||||
<FallbackImage
|
<FallbackImage
|
||||||
className={'avatar'}
|
className={'avatar'}
|
||||||
src={url}
|
src={url ?? undefined}
|
||||||
fallbackSrc={'/avatar.svg'}
|
fallbackSrc={'/avatar.svg'}
|
||||||
alt={'Avatar'}
|
alt={'Avatar'}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
export default Avatar
|
export default Avatar
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
import React, { ImgHTMLAttributes, useEffect, useState } from 'react'
|
import React, { ImgHTMLAttributes, ReactElement, useEffect, useState } from 'react'
|
||||||
export default function FallbackImage ({ fallbackSrc, src, alt, ...props }: ImgHTMLAttributes<HTMLImageElement>&{fallbackSrc?:string}) {
|
|
||||||
|
export default function FallbackImage ({
|
||||||
|
fallbackSrc,
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
...props
|
||||||
|
}: ImgHTMLAttributes<HTMLImageElement> & { fallbackSrc?: string }): ReactElement {
|
||||||
const [showFallback, setShowFallback] = useState<boolean>(false)
|
const [showFallback, setShowFallback] = useState<boolean>(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowFallback(!src)
|
setShowFallback(src === undefined || src === null || src === '')
|
||||||
}, [src])
|
}, [src])
|
||||||
const handleError = (event): void => {
|
const handleError = (event): void => {
|
||||||
if (props.onError) {
|
if (props.onError != null) {
|
||||||
props.onError(event)
|
props.onError(event)
|
||||||
}
|
}
|
||||||
if (!fallbackSrc) {
|
if (fallbackSrc === undefined || fallbackSrc === '') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setShowFallback(true)
|
setShowFallback(true)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect } from 'react'
|
import React, { ReactElement, useEffect } from 'react'
|
||||||
import striptags from 'striptags'
|
import striptags from 'striptags'
|
||||||
import Avatar from './Avatar'
|
import Avatar from './Avatar'
|
||||||
import SoftwareBadge from './badges/SoftwareBadge'
|
import SoftwareBadge from './badges/SoftwareBadge'
|
||||||
|
@ -14,16 +14,16 @@ import { FeedResultItem } from '../graphql/client/queries/ListFeedsQuery'
|
||||||
|
|
||||||
const FeedResult = ({
|
const FeedResult = ({
|
||||||
feed
|
feed
|
||||||
}:{ feed: FeedResultItem }) => {
|
}: { feed: FeedResultItem }): ReactElement => {
|
||||||
const fallbackEmojiImage = '/emoji.svg'
|
const fallbackEmojiImage = '/emoji.svg'
|
||||||
|
|
||||||
const handleEmojiImageError = (event) => {
|
const handleEmojiImageError = (event): void => {
|
||||||
event.target.src = fallbackEmojiImage
|
event.target.src = fallbackEmojiImage
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.querySelectorAll('.with-emoji img').forEach(element => {
|
document.querySelectorAll('.with-emoji img').forEach(element => {
|
||||||
if (element.attributes['data-error-handler']) {
|
if (element.attributes['data-error-handler'] === 'attached') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
element.addEventListener('error', handleEmojiImageError)
|
element.addEventListener('error', handleEmojiImageError)
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import FeedResult from './FeedResult'
|
import FeedResult from './FeedResult'
|
||||||
import { FeedResultItem } from '../graphql/client/queries/ListFeedsQuery'
|
import { FeedResultItem } from '../graphql/client/queries/ListFeedsQuery'
|
||||||
|
|
||||||
const FeedResults = ({
|
const FeedResults = ({
|
||||||
feeds
|
feeds
|
||||||
}:{ feeds: FeedResultItem[] }) => {
|
}: { feeds: FeedResultItem[] }): ReactElement => {
|
||||||
if (feeds.length === 0) {
|
if (feeds.length === 0) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const Footer:React.FC = () => {
|
const Footer: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<footer className={'text-center mt-5'}>
|
<footer className={'text-center mt-5'}>
|
||||||
©{(new Date()).getFullYear()} <a href={'https://skorpil.cz'}>Štěpán Škorpil</a>
|
©{(new Date()).getFullYear()} <a href={'https://skorpil.cz'}>Štěpán Škorpil</a>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
export function getFlagEmoji (countryCode:string):string {
|
export function getFlagEmoji (countryCode: string): string {
|
||||||
const codePoints = countryCode
|
const codePoints = countryCode
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
.split('')
|
.split('')
|
||||||
|
@ -8,18 +8,28 @@ export function getFlagEmoji (countryCode:string):string {
|
||||||
return String.fromCodePoint(...codePoints)
|
return String.fromCodePoint(...codePoints)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GeoParams{
|
export interface GeoParams {
|
||||||
countryCode?: string,
|
countryCode?: string
|
||||||
countryName?:string,
|
countryName?: string
|
||||||
city?: string
|
city?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Geo ({ countryCode, countryName, city }:GeoParams):React.ReactElement|null {
|
export default function Geo ({ countryCode, countryName, city }: GeoParams): React.ReactElement | null {
|
||||||
if (!countryCode && !city) {
|
const alignedCountryCode = countryCode ?? ''
|
||||||
|
const alignedCity = city ?? ''
|
||||||
|
if (alignedCountryCode === '' && alignedCity === '') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return <div className={'geo'}>
|
return <div className={'geo'}>
|
||||||
{city ? <span className={'city'}>{city}</span> : ''}
|
{
|
||||||
{countryCode ? <span className={'country'} title={`${countryName ?? countryCode}`}>{getFlagEmoji(countryCode)}</span> : ''}
|
alignedCity !== ''
|
||||||
|
? <span className={'city'}>{alignedCity}</span>
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
{
|
||||||
|
alignedCountryCode !== ''
|
||||||
|
? <span className={'country'} title={`${countryName ?? alignedCountryCode}`}>{getFlagEmoji(alignedCountryCode)}</span>
|
||||||
|
: ''
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,10 @@ const Loader: React.FC<{ children: ReactNode, loading: boolean, hideContent?: bo
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (table) {
|
if (table !== undefined || table !== 0) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showTop && loading
|
{(showTop ?? false) && loading
|
||||||
? (
|
? (
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr className={className}>
|
<tr className={className}>
|
||||||
|
@ -31,8 +31,8 @@ const Loader: React.FC<{ children: ReactNode, loading: boolean, hideContent?: bo
|
||||||
</tbody>
|
</tbody>
|
||||||
)
|
)
|
||||||
: ''}
|
: ''}
|
||||||
{hideContent && loading ? '' : children}
|
{(hideContent ?? false) && loading ? '' : children}
|
||||||
{showBottom && loading
|
{(showBottom ?? false) && loading
|
||||||
? (
|
? (
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr className={className}>
|
<tr className={className}>
|
||||||
|
@ -50,9 +50,9 @@ const Loader: React.FC<{ children: ReactNode, loading: boolean, hideContent?: bo
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showTop && loading ? spinner : ''}
|
{(showTop ?? false) && loading ? spinner : ''}
|
||||||
{hideContent && loading ? '' : children}
|
{(hideContent ?? false) && loading ? '' : children}
|
||||||
{showBottom && loading ? spinner : ''}
|
{(showBottom ?? false) && loading ? spinner : ''}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import NavItem from './NavItem'
|
||||||
import { faUser, faServer, faChartPie } from '@fortawesome/free-solid-svg-icons'
|
import { faUser, faServer, faChartPie } from '@fortawesome/free-solid-svg-icons'
|
||||||
import FallbackImage from './FallbackImage'
|
import FallbackImage from './FallbackImage'
|
||||||
|
|
||||||
const NavBar:React.FC = () => {
|
const NavBar: React.FC = () => {
|
||||||
const [showMenu, setShowMenu] = useState<boolean>(false)
|
const [showMenu, setShowMenu] = useState<boolean>(false)
|
||||||
return (
|
return (
|
||||||
<nav className="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
|
<nav className="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useRouter } from 'next/router'
|
||||||
import { IconProp } from '@fortawesome/fontawesome-svg-core'
|
import { IconProp } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
|
||||||
const NavItem:FC<{path:string, label:string, icon:IconProp}> = ({ path, label, icon }) => {
|
const NavItem: FC<{ path: string, label: string, icon: IconProp }> = ({ path, label, icon }) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const active = router.pathname === path
|
const active = router.pathname === path
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -2,8 +2,8 @@ import React from 'react'
|
||||||
import Avatar from './Avatar'
|
import Avatar from './Avatar'
|
||||||
import { ParentFeedItem } from '../graphql/client/queries/ListFeedsQuery'
|
import { ParentFeedItem } from '../graphql/client/queries/ListFeedsQuery'
|
||||||
|
|
||||||
const ParentFeed: React.FC<{feed:ParentFeedItem|null}> = ({ feed }) => {
|
const ParentFeed: React.FC<{ feed: ParentFeedItem | null }> = ({ feed }) => {
|
||||||
if (!feed) {
|
if (feed == null) {
|
||||||
return (<></>)
|
return (<></>)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const ProgressBar: React.FC<{ percents: number, way?: 'left' | 'right' | 'top' | 'bottom', color?:string }> = ({ percents, way, color }) => {
|
const ProgressBar: React.FC<{ percents: number, way?: 'left' | 'right' | 'top' | 'bottom', color?: string }> = ({ percents, way, color }) => {
|
||||||
way = way ?? 'right'
|
way = way ?? 'right'
|
||||||
percents = Math.round(percents)
|
percents = Math.round(percents)
|
||||||
color = color ?? 'var(--accent-color)'
|
color = color ?? 'var(--accent-color)'
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { faSortUp, faSortDown } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
|
||||||
const SortToggle: React.FC<{
|
const SortToggle: React.FC<{
|
||||||
onToggle:(StatsRequestSortBy)=>void,
|
onToggle: (StatsRequestSortBy) => void
|
||||||
field:string,
|
field: string
|
||||||
sort: Sort
|
sort: Sort
|
||||||
}> = ({ onToggle, field, sort, children }) => {
|
}> = ({ onToggle, field, sort, children }) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -2,12 +2,12 @@ import React from 'react'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { IconProp } from '@fortawesome/fontawesome-svg-core'
|
import { IconProp } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
|
||||||
const Badge:React.FC<{ faIcon:IconProp, label:string, value:string|number|null, className?:string, showUnknown?:boolean }> = ({ faIcon, label, value, className, showUnknown }) => {
|
const Badge: React.FC<{ faIcon: IconProp, label: string, value: string | number | null, className?: string, showUnknown?: boolean }> = ({ faIcon, label, value, className, showUnknown }) => {
|
||||||
if (value === null && showUnknown !== true) {
|
if (value === null && showUnknown !== true) {
|
||||||
return (<></>)
|
return (<></>)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={'badge bg-secondary ' + className} title={label}>
|
<div className={`badge bg-secondary ${className ?? ''}`} title={label}>
|
||||||
<FontAwesomeIcon icon={faIcon} className={'margin-right'}/>
|
<FontAwesomeIcon icon={faIcon} className={'margin-right'}/>
|
||||||
<span className="visually-hidden">{label}:</span>
|
<span className="visually-hidden">{label}:</span>
|
||||||
<span className={'value'}>{value === null ? '?' : value}</span>
|
<span className={'value'}>{value === null ? '?' : value}</span>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||||
import { faRobot } from '@fortawesome/free-solid-svg-icons'
|
import { faRobot } from '@fortawesome/free-solid-svg-icons'
|
||||||
import Badge from './Badge'
|
import Badge from './Badge'
|
||||||
|
|
||||||
const BotBadge:React.FC<{ bot: boolean | null}> = ({ bot }) => {
|
const BotBadge: React.FC<{ bot: boolean | null }> = ({ bot }) => {
|
||||||
return (
|
return (
|
||||||
<Badge faIcon={faRobot}
|
<Badge faIcon={faRobot}
|
||||||
label={'Bot'}
|
label={'Bot'}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||||
import { faCalendarPlus } from '@fortawesome/free-solid-svg-icons'
|
import { faCalendarPlus } from '@fortawesome/free-solid-svg-icons'
|
||||||
import Badge from './Badge'
|
import Badge from './Badge'
|
||||||
|
|
||||||
const CreatedAtBadge:React.FC<{ createdAt: string | null }> = ({ createdAt }) => {
|
const CreatedAtBadge: React.FC<{ createdAt: string | null }> = ({ createdAt }) => {
|
||||||
return (
|
return (
|
||||||
<Badge faIcon={faCalendarPlus}
|
<Badge faIcon={faCalendarPlus}
|
||||||
label={'Created at'}
|
label={'Created at'}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||||
import { faRss, faUser } from '@fortawesome/free-solid-svg-icons'
|
import { faRss, faUser } from '@fortawesome/free-solid-svg-icons'
|
||||||
import Badge from './Badge'
|
import Badge from './Badge'
|
||||||
|
|
||||||
const FeedTypeBadge:React.FC<{ type: 'account' | 'channel' }> = ({ type }) => {
|
const FeedTypeBadge: React.FC<{ type: 'account' | 'channel' }> = ({ type }) => {
|
||||||
return (
|
return (
|
||||||
<Badge faIcon={type === 'channel' ? faRss : faUser}
|
<Badge faIcon={type === 'channel' ? faRss : faUser}
|
||||||
label={'Feed type'}
|
label={'Feed type'}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { faUserFriends } from '@fortawesome/free-solid-svg-icons'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Badge from './Badge'
|
import Badge from './Badge'
|
||||||
|
|
||||||
const FollowersBadge:React.FC<{ followers: number|null}> = ({ followers }) => {
|
const FollowersBadge: React.FC<{ followers: number | null }> = ({ followers }) => {
|
||||||
return (
|
return (
|
||||||
<Badge faIcon={faUserFriends}
|
<Badge faIcon={faUserFriends}
|
||||||
label={'Followers'}
|
label={'Followers'}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { faEye } from '@fortawesome/free-solid-svg-icons'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Badge from './Badge'
|
import Badge from './Badge'
|
||||||
|
|
||||||
const FollowingBadge:React.FC<{ following: number|null}> = ({ following }) => {
|
const FollowingBadge: React.FC<{ following: number | null }> = ({ following }) => {
|
||||||
return (
|
return (
|
||||||
<Badge faIcon={faEye}
|
<Badge faIcon={faEye}
|
||||||
label={'Following'}
|
label={'Following'}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||||
import Badge from './Badge'
|
import Badge from './Badge'
|
||||||
import { faCalendarCheck } from '@fortawesome/free-solid-svg-icons'
|
import { faCalendarCheck } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
const LastPostAtBadge:React.FC<{ lastStatusAt: string | null }> = ({ lastStatusAt }) => {
|
const LastPostAtBadge: React.FC<{ lastStatusAt: string | null }> = ({ lastStatusAt }) => {
|
||||||
return (
|
return (
|
||||||
<Badge faIcon={faCalendarCheck}
|
<Badge faIcon={faCalendarCheck}
|
||||||
label={'Last status at'}
|
label={'Last status at'}
|
||||||
|
|
|
@ -8,8 +8,8 @@ const SoftwareBadge: React.FC<{ softwareName: string | null }> = ({ softwareName
|
||||||
<FallbackImage className={'icon'}
|
<FallbackImage className={'icon'}
|
||||||
src={softwareName !== null ? `/software/${softwareName}.svg` : fallbackImage}
|
src={softwareName !== null ? `/software/${softwareName}.svg` : fallbackImage}
|
||||||
fallbackSrc={fallbackImage}
|
fallbackSrc={fallbackImage}
|
||||||
alt={softwareName}
|
alt={softwareName ?? undefined}
|
||||||
title={softwareName}
|
title={softwareName ?? undefined}
|
||||||
/>
|
/>
|
||||||
<span className={'value'}>{softwareName}</span>
|
<span className={'value'}>{softwareName}</span>
|
||||||
</div>)
|
</div>)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||||
import { faCommentAlt } from '@fortawesome/free-solid-svg-icons'
|
import { faCommentAlt } from '@fortawesome/free-solid-svg-icons'
|
||||||
import Badge from './Badge'
|
import Badge from './Badge'
|
||||||
|
|
||||||
const StatusesCountBadge:React.FC<{ statusesCount: number | null }> = ({ statusesCount }) => {
|
const StatusesCountBadge: React.FC<{ statusesCount: number | null }> = ({ statusesCount }) => {
|
||||||
return (
|
return (
|
||||||
<Badge faIcon={faCommentAlt}
|
<Badge faIcon={faCommentAlt}
|
||||||
label={'Status count'}
|
label={'Status count'}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { ApolloClient, InMemoryCache } from '@apollo/client'
|
import { ApolloClient, InMemoryCache, NormalizedCacheObject } from '@apollo/client'
|
||||||
|
|
||||||
export default function createGraphqlClient () {
|
export default function createGraphqlClient (): ApolloClient<NormalizedCacheObject> {
|
||||||
return new ApolloClient({
|
return new ApolloClient({
|
||||||
uri: '/api/graphql',
|
uri: '/api/graphql',
|
||||||
cache: new InMemoryCache()
|
cache: new InMemoryCache()
|
||||||
|
|
|
@ -57,56 +57,57 @@ export const ListFeedsQuery = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export type ParentFeedItem = {
|
export interface ParentFeedItem {
|
||||||
id: string,
|
id: string
|
||||||
avatar: string,
|
avatar: string
|
||||||
displayName: string
|
displayName: string
|
||||||
|
name: string
|
||||||
|
domain: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FeedResultItem {
|
||||||
|
id: string
|
||||||
|
avatar: string
|
||||||
|
displayName: string
|
||||||
|
foundAt: string
|
||||||
|
bot: boolean
|
||||||
|
createdAt: string
|
||||||
|
description: string
|
||||||
|
followersCount: number
|
||||||
|
followingCount: number
|
||||||
|
lastStatusAt: string
|
||||||
|
locked: boolean
|
||||||
|
name: string
|
||||||
|
refreshedAt: string
|
||||||
|
statusesCount: number
|
||||||
|
type: 'account' | 'channel'
|
||||||
|
url: string
|
||||||
|
fields: Array<{
|
||||||
name: string
|
name: string
|
||||||
|
value: string
|
||||||
|
}>
|
||||||
|
node: {
|
||||||
domain: string
|
domain: string
|
||||||
url:string
|
foundAt: string
|
||||||
|
geoip: {
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
city_name: string
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
country_iso_code: string
|
||||||
|
}
|
||||||
|
halfYearActiveUserCount: number
|
||||||
|
id: string
|
||||||
|
monthActiveUserCount: number
|
||||||
|
name: string
|
||||||
|
openRegistrations: boolean
|
||||||
|
refreshAttemptedAt: string
|
||||||
|
refreshedAt: string
|
||||||
|
softwareName: string
|
||||||
|
}
|
||||||
|
parent: ParentFeedItem | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FeedResultItem = {
|
export interface ListFeedsResult {
|
||||||
id: string,
|
listFeeds: List<FeedResultItem>
|
||||||
avatar: string,
|
|
||||||
displayName: string,
|
|
||||||
foundAt: string,
|
|
||||||
bot: boolean,
|
|
||||||
createdAt: string,
|
|
||||||
description: string,
|
|
||||||
followersCount: number,
|
|
||||||
followingCount: number,
|
|
||||||
lastStatusAt: string,
|
|
||||||
locked: boolean,
|
|
||||||
name: string,
|
|
||||||
refreshedAt: string,
|
|
||||||
statusesCount: number,
|
|
||||||
type: 'account' | 'channel'
|
|
||||||
url: string,
|
|
||||||
fields: {
|
|
||||||
name: string, value: string
|
|
||||||
}[],
|
|
||||||
node: {
|
|
||||||
domain: string,
|
|
||||||
foundAt: string,
|
|
||||||
geoip: {
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
city_name: string,
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
country_iso_code: string,
|
|
||||||
},
|
|
||||||
halfYearActiveUserCount: number,
|
|
||||||
id: string,
|
|
||||||
monthActiveUserCount: number,
|
|
||||||
name: string,
|
|
||||||
openRegistrations: boolean,
|
|
||||||
refreshAttemptedAt: string,
|
|
||||||
refreshedAt: string,
|
|
||||||
softwareName: string
|
|
||||||
},
|
|
||||||
parent: ParentFeedItem|null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ListFeedsResult = {
|
|
||||||
listFeeds: List<FeedResultItem>
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,30 +32,30 @@ export const ListNodesQuery = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export type NodeResultItem = {
|
export interface NodeResultItem {
|
||||||
domain: string,
|
domain: string
|
||||||
foundAt: string,
|
foundAt: string
|
||||||
geoip: {
|
geoip: {
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
city_name: string,
|
city_name: string
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
country_iso_code: string,
|
country_iso_code: string
|
||||||
},
|
}
|
||||||
halfYearActiveUserCount: number,
|
halfYearActiveUserCount: number
|
||||||
id: string,
|
id: string
|
||||||
monthActiveUserCount: number,
|
monthActiveUserCount: number
|
||||||
name: string,
|
name: string
|
||||||
openRegistrations: boolean,
|
openRegistrations: boolean
|
||||||
refreshAttemptedAt: string,
|
refreshAttemptedAt: string
|
||||||
refreshedAt: string,
|
refreshedAt: string
|
||||||
softwareName: string,
|
softwareName: string
|
||||||
softwareVersion: string,
|
softwareVersion: string
|
||||||
standardizedSoftwareVersion:string,
|
standardizedSoftwareVersion: string
|
||||||
totalUserCount: number,
|
totalUserCount: number
|
||||||
statusesCount: number,
|
statusesCount: number
|
||||||
accountFeedCount: number
|
accountFeedCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ListNodesResult = {
|
export interface ListNodesResult {
|
||||||
listNodes: List<NodeResultItem>;
|
listNodes: List<NodeResultItem>
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,13 +14,13 @@ export const ListStatsQuery = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export type StatsResultItem = {
|
export interface StatsResultItem {
|
||||||
softwareName: string,
|
softwareName: string
|
||||||
nodeCount: number,
|
nodeCount: number
|
||||||
accountFeedCount: number,
|
accountFeedCount: number
|
||||||
channelFeedCount: number
|
channelFeedCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ListStatsResult = {
|
export interface ListStatsResult {
|
||||||
listStats: List<StatsResultItem>;
|
listStats: List<StatsResultItem>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { PagingType } from '../../server/schema/types'
|
import { PagingType } from '../../server/schema/types'
|
||||||
|
|
||||||
export type List<TItem> = {
|
export interface List<TItem> {
|
||||||
paging: PagingType,
|
paging: PagingType
|
||||||
items: TItem[]
|
items: TItem[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { FeedQueryInputType } from '../types/FeedQueryInput'
|
import { FeedQueryInputType } from '../types/FeedQueryInput'
|
||||||
import { PagingInputType } from '../types/PagingInput'
|
import { PagingInputType } from '../types/PagingInput'
|
||||||
|
|
||||||
export type ListFeedsVariables = {
|
export interface ListFeedsVariables {
|
||||||
paging: PagingInputType;
|
paging: PagingInputType
|
||||||
query: FeedQueryInputType
|
query: FeedQueryInputType
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { PagingInputType } from '../types/PagingInput'
|
import { PagingInputType } from '../types/PagingInput'
|
||||||
import { NodeQueryInputType } from '../types/NodeQueryInput'
|
import { NodeQueryInputType } from '../types/NodeQueryInput'
|
||||||
|
|
||||||
export type ListNodesVariables = {
|
export interface ListNodesVariables {
|
||||||
paging: PagingInputType;
|
paging: PagingInputType
|
||||||
query: NodeQueryInputType
|
query: NodeQueryInputType
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { StatsQueryInputType } from '../types/StatsQueryInput'
|
import { StatsQueryInputType } from '../types/StatsQueryInput'
|
||||||
|
|
||||||
export type ListStatsVariables = {
|
export interface ListStatsVariables {
|
||||||
query: StatsQueryInputType
|
query: StatsQueryInputType
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
export const NodeSortingByValues:readonly [string, ...string[]] = [
|
export const NodeSortingByValues: readonly [string, ...string[]] = [
|
||||||
'domain',
|
'domain',
|
||||||
'softwareName',
|
'softwareName',
|
||||||
'totalUserCount',
|
'totalUserCount',
|
||||||
|
@ -14,4 +14,4 @@ export const NodeSortingByValues:readonly [string, ...string[]] = [
|
||||||
|
|
||||||
export const nodeSortingBySchema = z.enum(NodeSortingByValues)
|
export const nodeSortingBySchema = z.enum(NodeSortingByValues)
|
||||||
|
|
||||||
export type NodeSoringByEnumType = z.infer<typeof nodeSortingBySchema>;
|
export type NodeSoringByEnumType = z.infer<typeof nodeSortingBySchema>
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
export type PagingInputType = {
|
export interface PagingInputType {
|
||||||
page: number
|
page: number
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
export const createSortingInputSchema = (members:z.ZodEnum<[string, ...string[]]>) => {
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
|
export const createSortingInputSchema = (members: z.ZodEnum<[string, ...string[]]>) => {
|
||||||
return z.object({
|
return z.object({
|
||||||
sortBy: members,
|
sortBy: members,
|
||||||
sortWay: z.enum(['asc', 'desc'])
|
sortWay: z.enum(['asc', 'desc'])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SortingInputType<TMembers> = {
|
export interface SortingInputType<TMembers> {
|
||||||
sortBy: TMembers
|
sortBy: TMembers
|
||||||
sortWay: 'asc'|'desc'
|
sortWay: 'asc' | 'desc'
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
export const StatsSortingByValues:readonly [string, ...string[]] = [
|
export const StatsSortingByValues: readonly [string, ...string[]] = [
|
||||||
'softwareName',
|
'softwareName',
|
||||||
'nodeCount',
|
'nodeCount',
|
||||||
'accountFeedCount',
|
'accountFeedCount',
|
||||||
|
@ -9,4 +9,4 @@ export const StatsSortingByValues:readonly [string, ...string[]] = [
|
||||||
|
|
||||||
export const statsSortingBySchema = z.enum(StatsSortingByValues)
|
export const statsSortingBySchema = z.enum(StatsSortingByValues)
|
||||||
|
|
||||||
export type StatsSoringByEnumType = z.infer<typeof statsSortingBySchema>;
|
export type StatsSoringByEnumType = z.infer<typeof statsSortingBySchema>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { ElasticClient } from '../../../lib/storage/ElasticClient'
|
import { ElasticClient } from '../../../lib/storage/ElasticClient'
|
||||||
|
|
||||||
type Context = {
|
interface Context {
|
||||||
elasticClient: ElasticClient
|
elasticClient: ElasticClient
|
||||||
defaultPaging: {
|
defaultPaging: {
|
||||||
limit: 20
|
limit: 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Context
|
export default Context
|
||||||
|
|
|
@ -3,7 +3,7 @@ import resolvers from './resolvers'
|
||||||
import schema from './schema'
|
import schema from './schema'
|
||||||
import { createContext } from './context'
|
import { createContext } from './context'
|
||||||
|
|
||||||
export default function createGraphqlServer () {
|
export default function createGraphqlServer (): ApolloServer {
|
||||||
return new ApolloServer({
|
return new ApolloServer({
|
||||||
schema,
|
schema,
|
||||||
resolvers,
|
resolvers,
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { join } from 'path'
|
||||||
import * as types from './types'
|
import * as types from './types'
|
||||||
import * as queries from './queries'
|
import * as queries from './queries'
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import * as elastic from './sources/elastic'
|
|
||||||
|
|
||||||
const schema = makeSchema({
|
const schema = makeSchema({
|
||||||
types: {
|
types: {
|
||||||
|
|
|
@ -21,7 +21,7 @@ export const listNodes = extendType({
|
||||||
default: { default: '', sortBy: 'refreshedAt', sortWay: 'desc' }
|
default: { default: '', sortBy: 'refreshedAt', sortWay: 'desc' }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
resolve: async (event, { paging, query }:ListNodesVariables, { elasticClient, defaultPaging }: Context) => {
|
resolve: async (event, { paging, query }: ListNodesVariables, { elasticClient, defaultPaging }: Context) => {
|
||||||
console.info('Searching nodes', { paging, query })
|
console.info('Searching nodes', { paging, query })
|
||||||
|
|
||||||
const results = await elasticClient.search<Node>({
|
const results = await elasticClient.search<Node>({
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { ListStatsVariables } from '../../../common/queries/listStats'
|
||||||
import nodeIndex from '../../../../lib/storage/Definitions/nodeIndex'
|
import nodeIndex from '../../../../lib/storage/Definitions/nodeIndex'
|
||||||
import { StatsQueryInputType } from '../../../common/types/StatsQueryInput'
|
import { StatsQueryInputType } from '../../../common/types/StatsQueryInput'
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
const getSort = (query: StatsQueryInputType) => {
|
const getSort = (query: StatsQueryInputType) => {
|
||||||
switch (query.sortBy) {
|
switch (query.sortBy) {
|
||||||
case 'nodeCount':
|
case 'nodeCount':
|
||||||
|
@ -30,7 +31,7 @@ export const listStats = extendType({
|
||||||
default: { sortBy: 'nodeCount', sortWay: 'desc' }
|
default: { sortBy: 'nodeCount', sortWay: 'desc' }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
resolve: async (event, { query }:ListStatsVariables, { elasticClient }: Context) => {
|
resolve: async (event, { query }: ListStatsVariables, { elasticClient }: Context) => {
|
||||||
console.info('Searching stats', { query })
|
console.info('Searching stats', { query })
|
||||||
|
|
||||||
const results = await elasticClient.search({
|
const results = await elasticClient.search({
|
||||||
|
@ -59,7 +60,7 @@ export const listStats = extendType({
|
||||||
sort: {
|
sort: {
|
||||||
bucket_sort: {
|
bucket_sort: {
|
||||||
sort: [
|
sort: [
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
getSort(query)
|
getSort(query)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -68,16 +69,16 @@ export const listStats = extendType({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
type Aggregation = {
|
interface Aggregation {
|
||||||
buckets:{
|
buckets: Array<{
|
||||||
key:string,
|
key: string
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
doc_count:number
|
doc_count: number
|
||||||
accountFeedCount: {value:number}
|
accountFeedCount: { value: number }
|
||||||
channelFeedCount: {value:number}
|
channelFeedCount: { value: number }
|
||||||
}[]
|
}>
|
||||||
}
|
}
|
||||||
const software = results.aggregations.software as Aggregation
|
const software = results?.aggregations?.software as Aggregation
|
||||||
return {
|
return {
|
||||||
items: software.buckets.map(bucket => {
|
items: software.buckets.map(bucket => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export default function getFlagEmoji (countryCode:string):string {
|
export default function getFlagEmoji (countryCode: string): string {
|
||||||
const codePoints = countryCode
|
const codePoints = countryCode
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
.split('')
|
.split('')
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { UserOptions } from '@datapunt/matomo-tracker-js/es/types'
|
import { UserOptions } from '@datapunt/matomo-tracker-js/es/types'
|
||||||
import MatomoTracker from '@datapunt/matomo-tracker-js'
|
import MatomoTracker from '@datapunt/matomo-tracker-js'
|
||||||
|
|
||||||
let matomo:MatomoTracker|undefined
|
let matomo: MatomoTracker | undefined
|
||||||
|
|
||||||
const getMatomo = (config:UserOptions):MatomoTracker => {
|
const getMatomo = (config: UserOptions): MatomoTracker => {
|
||||||
if (!matomo) {
|
if (matomo == null) {
|
||||||
console.info('Starting Matomo', config)
|
console.info('Starting Matomo', config)
|
||||||
matomo = new MatomoTracker(config)
|
matomo = new MatomoTracker(config)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
export default function getNodeId (name:string, domain:string):string {
|
export default function getNodeId (name: string, domain: string): string {
|
||||||
return `${name}@${domain}`
|
return `${name}@${domain}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
export const matomoConfig = {
|
export const matomoConfig = {
|
||||||
urlBase: typeof process.env.MATOMO_URL === 'string' && process.env.MATOMO_URL !== ''
|
urlBase:
|
||||||
? process.env.MATOMO_URL
|
process.env.MATOMO_URL !== undefined && process.env.MATOMO_URL !== ''
|
||||||
: 'https://domain.tld',
|
? process.env.MATOMO_URL
|
||||||
siteId: parseInt(typeof process.env.MATOMO_SITE_ID === 'string' && process.env.MATOMO_SITE_ID !== ''
|
: 'https://domain.tld',
|
||||||
? process.env.MATOMO_SITE_ID
|
siteId: parseInt(
|
||||||
: '1'
|
process.env.MATOMO_SITE_ID !== undefined && process.env.MATOMO_SITE_ID !== ''
|
||||||
|
? process.env.MATOMO_SITE_ID
|
||||||
|
: '1'
|
||||||
),
|
),
|
||||||
disabled: !process.env.MATOMO_URL || !process.env.MATOMO_SITE_ID
|
disabled: (process.env.MATOMO_URL ?? '') === '' || (process.env.MATOMO_SITE_ID ?? '') === ''
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export default function prepareSimpleQuery (search:string):string {
|
export default function prepareSimpleQuery (search: string): string {
|
||||||
const tokens = search.split(/\s+/)
|
const tokens = search.split(/\s+/)
|
||||||
const searchContainsWildcard = tokens.filter(token => token.length > 0 && token.slice(-1) === '*').length > 0
|
const searchContainsWildcard = tokens.filter(token => token.length > 0 && token.slice(-1) === '*').length > 0
|
||||||
return tokens.map(token => searchContainsWildcard ? token : token + '*').join(' ')
|
return tokens.map(token => searchContainsWildcard ? token : token + '*').join(' ')
|
||||||
|
|
|
@ -1,29 +1,29 @@
|
||||||
import Field from './Field'
|
import Field from './Field'
|
||||||
|
|
||||||
interface Feed {
|
interface Feed {
|
||||||
domain: string
|
domain: string
|
||||||
foundAt: number,
|
foundAt: number
|
||||||
refreshedAt?: number,
|
refreshedAt?: number
|
||||||
name: string,
|
name: string
|
||||||
fullName: string,
|
fullName: string
|
||||||
displayName: string,
|
displayName: string
|
||||||
description: string,
|
description: string
|
||||||
strippedDescription?: string,
|
strippedDescription?: string
|
||||||
followersCount?: number,
|
followersCount?: number
|
||||||
followingCount?: number,
|
followingCount?: number
|
||||||
statusesCount?: number,
|
statusesCount?: number
|
||||||
lastStatusAt?: number,
|
lastStatusAt?: number
|
||||||
createdAt?: number,
|
createdAt?: number
|
||||||
bot?: boolean,
|
bot?: boolean
|
||||||
locked: boolean,
|
locked: boolean
|
||||||
url: string,
|
url: string
|
||||||
avatar?: string,
|
avatar?: string
|
||||||
type: 'account' | 'channel',
|
type: 'account' | 'channel'
|
||||||
parentFeedName?: string,
|
parentFeedName?: string
|
||||||
parentFeedDomain?: string
|
parentFeedDomain?: string
|
||||||
fields: Field[],
|
fields: Field[]
|
||||||
extractedEmails: string[],
|
extractedEmails: string[]
|
||||||
extractedTags: string[]
|
extractedTags: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Feed
|
export default Feed
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
interface Field {
|
interface Field {
|
||||||
name: string,
|
name: string
|
||||||
value: string
|
value: string
|
||||||
strippedName?: string
|
strippedName?: string
|
||||||
strippedValue?: string
|
strippedValue?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Field
|
export default Field
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
interface Geo {
|
interface Geo {
|
||||||
cityName?: string,
|
cityName?: string
|
||||||
continentName?: string,
|
continentName?: string
|
||||||
countryIsoCode?: string,
|
countryIsoCode?: string
|
||||||
countryName?: string,
|
countryName?: string
|
||||||
latitude: number
|
latitude: number
|
||||||
longitude: number
|
longitude: number
|
||||||
location?: string,
|
location?: string
|
||||||
regionIsoCode?: string,
|
regionIsoCode?: string
|
||||||
regionName?: string
|
regionName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Geo
|
export default Geo
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
import Geo from './Geo'
|
import Geo from './Geo'
|
||||||
|
|
||||||
interface Node {
|
interface Node {
|
||||||
name?:string,
|
name?: string
|
||||||
strippedName?:string,
|
strippedName?: string
|
||||||
foundAt: number,
|
foundAt: number
|
||||||
refreshAttemptedAt?: number
|
refreshAttemptedAt?: number
|
||||||
refreshedAt?: number
|
refreshedAt?: number
|
||||||
openRegistrations?: boolean
|
openRegistrations?: boolean
|
||||||
domain: string,
|
domain: string
|
||||||
serverIps?: string[],
|
serverIps?: string[]
|
||||||
geoip?: Geo[],
|
geoip?: Geo[]
|
||||||
softwareName?: string;
|
softwareName?: string
|
||||||
softwareVersion?: string
|
softwareVersion?: string
|
||||||
standardizedSoftwareVersion?: string
|
standardizedSoftwareVersion?: string
|
||||||
halfYearActiveUserCount?: number,
|
halfYearActiveUserCount?: number
|
||||||
monthActiveUserCount?: number,
|
monthActiveUserCount?: number
|
||||||
statusesCount?: number,
|
statusesCount?: number
|
||||||
totalUserCount?: number,
|
totalUserCount?: number
|
||||||
discoveredByDomain?:string,
|
discoveredByDomain?: string
|
||||||
accountFeedCount?: number,
|
accountFeedCount?: number
|
||||||
channelFeedCount?: number,
|
channelFeedCount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Node
|
export default Node
|
||||||
|
|
|
@ -6,7 +6,7 @@ const elasticClient = new Client({
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
username: process.env.ELASTIC_USER ?? 'elastic',
|
username: process.env.ELASTIC_USER ?? 'elastic',
|
||||||
password: process.env.ELASTIC_PASSWORD
|
password: process.env.ELASTIC_PASSWORD ?? ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ZodSchema } from 'zod'
|
import { ZodSchema } from 'zod'
|
||||||
|
|
||||||
export function preserveUndefined<Source, Target> (cast: (value:Source)=>Target) {
|
export function preserveUndefined<Source, Target> (cast: (value: Source) => Target) {
|
||||||
return (value:Source|undefined):Target|undefined => {
|
return (value: Source | undefined): Target | undefined => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,8 @@ export function preserveUndefined<Source, Target> (cast: (value:Source)=>Target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function preserveNull<Source, Target> (cast: (value:Source)=>Target) {
|
export function preserveNull<Source, Target> (cast: (value: Source) => Target) {
|
||||||
return (value:Source|null):Target|null => {
|
return (value: Source | null): Target | null => {
|
||||||
if (value === null) {
|
if (value === null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -18,11 +18,11 @@ export function preserveNull<Source, Target> (cast: (value:Source)=>Target) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function undefinedToDefault<Type> (defaultValue:Type): (value:Type|undefined)=>Type {
|
export function undefinedToDefault<Type> (defaultValue: Type): (value: Type | undefined) => Type {
|
||||||
return (value) => typeof value === 'undefined' ? defaultValue : value
|
return (value) => typeof value === 'undefined' ? defaultValue : value
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stringTrimmed (value: string|undefined): string {
|
export function stringTrimmed (value: string | undefined): string {
|
||||||
return (value ?? '').trim().replace(/^\++|\++$/g, '')
|
return (value ?? '').trim().replace(/^\++|\++$/g, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ export function stringToInt (value: string): number {
|
||||||
return parseInt(value)
|
return parseInt(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stringToBool (value:string):boolean {
|
export function stringToBool (value: string): boolean {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case 'true':
|
case 'true':
|
||||||
case '1':
|
case '1':
|
||||||
|
@ -42,7 +42,8 @@ export function stringToBool (value:string):boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transform<Target> (originalSchema:ZodSchema<any>, cast:(value:unknown)=>Target, newSchema:ZodSchema<any>) {
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
|
export function transform<Target> (originalSchema: ZodSchema<any>, cast: (value: unknown) => Target, newSchema: ZodSchema<any>) {
|
||||||
return originalSchema.refine(
|
return originalSchema.refine(
|
||||||
value => {
|
value => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import '../styles/global.scss'
|
import '../styles/global.scss'
|
||||||
import { AppProps } from 'next/app'
|
import { AppProps } from 'next/app'
|
||||||
import React from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import { ApolloProvider } from '@apollo/client'
|
import { ApolloProvider } from '@apollo/client'
|
||||||
import createGraphqlClient from '../graphql/client/createGraphqlClient'
|
import createGraphqlClient from '../graphql/client/createGraphqlClient'
|
||||||
|
|
||||||
const graphqlClient = createGraphqlClient()
|
const graphqlClient = createGraphqlClient()
|
||||||
|
|
||||||
const App = ({ Component, pageProps }: AppProps) => {
|
const App = ({ Component, pageProps }: AppProps): ReactElement => {
|
||||||
return <ApolloProvider client={graphqlClient}>
|
return <ApolloProvider client={graphqlClient}>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
|
|
|
@ -8,7 +8,7 @@ const graphqlServer = createGraphqlServer()
|
||||||
|
|
||||||
const startedServer = graphqlServer.start()
|
const startedServer = graphqlServer.start()
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) :Promise<void> => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
|
||||||
if (req.method === 'OPTIONS') {
|
if (req.method === 'OPTIONS') {
|
||||||
res.end()
|
res.end()
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
import Loader from '../components/Loader'
|
import Loader from '../components/Loader'
|
||||||
import FeedResults from '../components/FeedResults'
|
import FeedResults from '../components/FeedResults'
|
||||||
import Layout, { siteTitle } from '../components/Layout'
|
import Layout, { siteTitle } from '../components/Layout'
|
||||||
|
@ -16,7 +16,7 @@ import getMatomo from '../lib/getMatomo'
|
||||||
import { feedQueryInputSchema, FeedQueryInputType } from '../graphql/common/types/FeedQueryInput'
|
import { feedQueryInputSchema, FeedQueryInputType } from '../graphql/common/types/FeedQueryInput'
|
||||||
import { ListFeedsVariables } from '../graphql/common/queries/listFeeds'
|
import { ListFeedsVariables } from '../graphql/common/queries/listFeeds'
|
||||||
|
|
||||||
const Feeds: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = ({ matomoConfig }) => {
|
const Feeds: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = ({ matomoConfig }): ReactElement => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const routerQuery = feedQueryInputSchema.parse(router.query)
|
const routerQuery = feedQueryInputSchema.parse(router.query)
|
||||||
const [page, setPage] = useState<number>(0)
|
const [page, setPage] = useState<number>(0)
|
||||||
|
@ -28,8 +28,9 @@ const Feeds: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
query
|
query
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
useEffect(() => {
|
useEffect((): void => {
|
||||||
router.push({ query })
|
router.push({ query })
|
||||||
|
.catch((error) => console.error(error))
|
||||||
getMatomo(matomoConfig).trackEvent({
|
getMatomo(matomoConfig).trackEvent({
|
||||||
category: 'feeds',
|
category: 'feeds',
|
||||||
action: 'new-search'
|
action: 'new-search'
|
||||||
|
@ -48,7 +49,7 @@ const Feeds: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
})
|
})
|
||||||
}, [page])
|
}, [page])
|
||||||
|
|
||||||
const handleQueryChange = (event) => {
|
const handleQueryChange = (event): void => {
|
||||||
const inputElement = event.target
|
const inputElement = event.target
|
||||||
const value = inputElement.value
|
const value = inputElement.value
|
||||||
const name = inputElement.name
|
const name = inputElement.name
|
||||||
|
@ -60,7 +61,7 @@ const Feeds: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
setPage(0)
|
setPage(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSearchSubmit = async (event) => {
|
const handleSearchSubmit = async (event): Promise<void> => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setPageLoading(true)
|
setPageLoading(true)
|
||||||
setPage(0)
|
setPage(0)
|
||||||
|
@ -68,7 +69,7 @@ const Feeds: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
setPageLoading(false)
|
setPageLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLoadMore = async (event) => {
|
const handleLoadMore = async (event): Promise<void> => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setPageLoading(true)
|
setPageLoading(true)
|
||||||
await fetchMore({
|
await fetchMore({
|
||||||
|
@ -117,12 +118,12 @@ const Feeds: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
|
|
||||||
<Loader loading={loading || pageLoading} showBottom={true}>
|
<Loader loading={loading || pageLoading} showBottom={true}>
|
||||||
{
|
{
|
||||||
data && query.search
|
(data != null) && query.search.length > 0
|
||||||
? <FeedResults feeds={data.listFeeds.items} />
|
? <FeedResults feeds={data.listFeeds.items}/>
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
</Loader>
|
</Loader>
|
||||||
{!loading && !pageLoading && data?.listFeeds?.paging?.hasNext
|
{!loading && !pageLoading && data?.listFeeds?.paging?.hasNext !== undefined && data?.listFeeds?.paging?.hasNext
|
||||||
? (
|
? (
|
||||||
<div className={'d-flex justify-content-center'}>
|
<div className={'d-flex justify-content-center'}>
|
||||||
<button className={'btn btn-secondary'} onClick={handleLoadMore}>
|
<button className={'btn btn-secondary'} onClick={handleLoadMore}>
|
||||||
|
@ -132,10 +133,10 @@ const Feeds: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: ''}
|
: ''}
|
||||||
{error
|
{(error != null)
|
||||||
? (<div className={'d-flex justify-content-center'}>
|
? (<div className={'d-flex justify-content-center'}>
|
||||||
<FontAwesomeIcon icon={faExclamationTriangle} className={'margin-right'}/>
|
<FontAwesomeIcon icon={faExclamationTriangle} className={'margin-right'}/>
|
||||||
<span>{error.message}</span>
|
<span>{error.message}</span>
|
||||||
</div>)
|
</div>)
|
||||||
: ''}
|
: ''}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { NodeSoringByEnumType } from '../graphql/common/types/NodeSortingByEnum'
|
||||||
|
|
||||||
const Nodes: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = ({ matomoConfig }) => {
|
const Nodes: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> = ({ matomoConfig }) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
let routerQuery:NodeQueryInputType
|
let routerQuery: NodeQueryInputType
|
||||||
try {
|
try {
|
||||||
routerQuery = nodeQueryInputSchema.parse(router.query)
|
routerQuery = nodeQueryInputSchema.parse(router.query)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -56,26 +56,26 @@ const Nodes: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
}, [page])
|
}, [page])
|
||||||
useEffect(() => {
|
useEffect((): void => {
|
||||||
router.push({ query })
|
void router.push({ query })
|
||||||
getMatomo(matomoConfig).trackEvent({
|
getMatomo(matomoConfig).trackEvent({
|
||||||
category: 'nodes',
|
category: 'nodes',
|
||||||
action: 'new-search'
|
action: 'new-search'
|
||||||
})
|
})
|
||||||
}, [query])
|
}, [query])
|
||||||
|
|
||||||
const handleQueryChange = (event) => {
|
const handleQueryChange = (event): void => {
|
||||||
const targetInput = event.target
|
const targetInput = event.target
|
||||||
const value = targetInput.value
|
const value = targetInput.value
|
||||||
const name = targetInput.name
|
const name = targetInput.name
|
||||||
const newQuery:NodeQueryInputType = { ...query }
|
const newQuery: NodeQueryInputType = { ...query }
|
||||||
newQuery[name] = value
|
newQuery[name] = value
|
||||||
console.info('Query changed', { name, value })
|
console.info('Query changed', { name, value })
|
||||||
setQuery(newQuery)
|
setQuery(newQuery)
|
||||||
setPage(0)
|
setPage(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSearchSubmit = async (event) => {
|
const handleSearchSubmit = async (event): Promise<void> => {
|
||||||
setPageLoading(true)
|
setPageLoading(true)
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setQuery(query)
|
setQuery(query)
|
||||||
|
@ -84,7 +84,7 @@ const Nodes: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
setPageLoading(false)
|
setPageLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLoadMore = async (event) => {
|
const handleLoadMore = async (event): Promise<void> => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setPage(page + 1)
|
setPage(page + 1)
|
||||||
console.info('Loading next page', { query, page })
|
console.info('Loading next page', { query, page })
|
||||||
|
@ -107,7 +107,7 @@ const Nodes: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
setPageLoading(false)
|
setPageLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleSort = (sortBy: NodeSoringByEnumType) => {
|
const toggleSort = (sortBy: NodeSoringByEnumType): void => {
|
||||||
const sortWay = query.sortBy === sortBy && query.sortWay === 'asc' ? 'desc' : 'asc'
|
const sortWay = query.sortBy === sortBy && query.sortWay === 'asc' ? 'desc' : 'asc'
|
||||||
getMatomo(matomoConfig).trackEvent({
|
getMatomo(matomoConfig).trackEvent({
|
||||||
category: 'nodes',
|
category: 'nodes',
|
||||||
|
@ -155,7 +155,7 @@ const Nodes: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
</form>
|
</form>
|
||||||
<Loader loading={loading || pageLoading} showBottom={true}>
|
<Loader loading={loading || pageLoading} showBottom={true}>
|
||||||
{
|
{
|
||||||
data
|
(data != null)
|
||||||
? (
|
? (
|
||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
<table className={'table table-dark table-striped table-bordered nodes'}>
|
<table className={'table table-dark table-striped table-bordered nodes'}>
|
||||||
|
@ -212,7 +212,7 @@ const Nodes: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.listNodes.items.length
|
{(data.listNodes.items.length > 0)
|
||||||
? data.listNodes.items.map((node, index) => {
|
? data.listNodes.items.map((node, index) => {
|
||||||
return (
|
return (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
|
@ -228,7 +228,7 @@ const Nodes: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
<td className={'text-end'}>{node.halfYearActiveUserCount ?? '?'}</td>
|
<td className={'text-end'}>{node.halfYearActiveUserCount ?? '?'}</td>
|
||||||
<td className={'text-end'}>{node.statusesCount ?? '?'}</td>
|
<td className={'text-end'}>{node.statusesCount ?? '?'}</td>
|
||||||
<td>{node.openRegistrations === null ? '?' : (node.openRegistrations ? 'Opened' : 'Closed')}</td>
|
<td>{node.openRegistrations === null ? '?' : (node.openRegistrations ? 'Opened' : 'Closed')}</td>
|
||||||
<td>{node.refreshedAt ? (new Date(node.refreshedAt)).toLocaleDateString() : 'Never'}</td>
|
<td>{node.refreshedAt !== '' ? (new Date(node.refreshedAt)).toLocaleDateString() : 'Never'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -244,7 +244,7 @@ const Nodes: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
</Loader>
|
</Loader>
|
||||||
{data?.listNodes?.paging?.hasNext && !loading && !pageLoading
|
{!loading && !pageLoading && data?.listNodes?.paging?.hasNext !== undefined && data?.listNodes?.paging?.hasNext
|
||||||
? (
|
? (
|
||||||
<div className={'d-flex justify-content-center'}>
|
<div className={'d-flex justify-content-center'}>
|
||||||
<button className={'btn btn-secondary'} onClick={handleLoadMore}>
|
<button className={'btn btn-secondary'} onClick={handleLoadMore}>
|
||||||
|
@ -254,7 +254,7 @@ const Nodes: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: ''}
|
: ''}
|
||||||
{error
|
{(error != null)
|
||||||
? (<div className={'d-flex justify-content-center'}>
|
? (<div className={'d-flex justify-content-center'}>
|
||||||
<FontAwesomeIcon icon={faExclamationTriangle} className={'margin-right'}/>
|
<FontAwesomeIcon icon={faExclamationTriangle} className={'margin-right'}/>
|
||||||
<span>{error.message}</span>
|
<span>{error.message}</span>
|
||||||
|
|
|
@ -37,14 +37,14 @@ const Stats: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
router.push({ query })
|
void router.push({ query })
|
||||||
getMatomo(matomoConfig).trackEvent({
|
getMatomo(matomoConfig).trackEvent({
|
||||||
category: 'stats',
|
category: 'stats',
|
||||||
action: 'new-search'
|
action: 'new-search'
|
||||||
})
|
})
|
||||||
}, [query])
|
}, [query])
|
||||||
|
|
||||||
const toggleSort = (sortBy: StatsSoringByEnumType) => {
|
const toggleSort = (sortBy: StatsSoringByEnumType): void => {
|
||||||
const sortWay = query.sortBy === sortBy && query.sortWay === 'asc' ? 'desc' : 'asc'
|
const sortWay = query.sortBy === sortBy && query.sortWay === 'asc' ? 'desc' : 'asc'
|
||||||
getMatomo(matomoConfig).trackEvent({
|
getMatomo(matomoConfig).trackEvent({
|
||||||
category: 'stats',
|
category: 'stats',
|
||||||
|
@ -73,7 +73,7 @@ const Stats: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
accountFeedCount: 0,
|
accountFeedCount: 0,
|
||||||
channelFeedCount: 0
|
channelFeedCount: 0
|
||||||
}
|
}
|
||||||
if (data) {
|
if (data != null) {
|
||||||
data.listStats.items.forEach(item => {
|
data.listStats.items.forEach(item => {
|
||||||
if (item.softwareName === null) {
|
if (item.softwareName === null) {
|
||||||
return
|
return
|
||||||
|
@ -119,7 +119,7 @@ const Stats: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<Loader loading={loading} table={4} showTop={true} hideContent={true}>
|
<Loader loading={loading} table={4} showTop={true} hideContent={true}>
|
||||||
{!data
|
{(data == null)
|
||||||
? (
|
? (
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -183,7 +183,7 @@ const Stats: React.FC<InferGetServerSidePropsType<typeof getServerSideProps>> =
|
||||||
}
|
}
|
||||||
</Loader>
|
</Loader>
|
||||||
</table>
|
</table>
|
||||||
{error
|
{(error != null)
|
||||||
? (<div className={'d-flex justify-content-center'}>
|
? (<div className={'d-flex justify-content-center'}>
|
||||||
<FontAwesomeIcon icon={faExclamationTriangle} className={'margin-right'}/>
|
<FontAwesomeIcon icon={faExclamationTriangle} className={'margin-right'}/>
|
||||||
<span>{error.message}</span>
|
<span>{error.message}</span>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export type Sort = {
|
export interface Sort {
|
||||||
sortBy?: string,
|
sortBy?: string
|
||||||
sortWay?: 'asc' | 'desc'
|
sortWay?: 'asc' | 'desc'
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
"jsx": "preserve"
|
"jsx": "preserve"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"defaultSeverity": "error",
|
|
||||||
"extends": [
|
|
||||||
"tslint:recommended"
|
|
||||||
],
|
|
||||||
"jsRules": {},
|
|
||||||
"rules": {
|
|
||||||
"no-console": false
|
|
||||||
},
|
|
||||||
"rulesDirectory": []
|
|
||||||
}
|
|
Plik diff jest za duży
Load Diff
Ładowanie…
Reference in New Issue