@imerss/inat-components 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -0
- package/declaration.d.ts +2 -0
- package/dist/index.d.ts +214 -0
- package/dist/index.js +876 -0
- package/package.json +53 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +20 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +25 -0
- package/public/robots.txt +3 -0
- package/rollup.config.js +36 -0
- package/src/__shared/api/common-taxa.ts +56 -0
- package/src/__shared/api/favourites.ts +61 -0
- package/src/__shared/api/index.ts +4 -0
- package/src/__shared/api/recent-observations.ts +43 -0
- package/src/__shared/api/summary.ts +84 -0
- package/src/__shared/constants.ts +4 -0
- package/src/__shared/index.ts +4 -0
- package/src/__shared/typings.ts +69 -0
- package/src/__shared/utils/configUtils.ts +21 -0
- package/src/__shared/utils/dateUtils.ts +14 -0
- package/src/__shared/utils/index.ts +3 -0
- package/src/__shared/utils/numberUtils.ts +1 -0
- package/src/components/common-taxa/__tests__/common-taxa.test.tsx +14 -0
- package/src/components/common-taxa/common-taxa.tsx +99 -0
- package/src/components/error/__tests__/error.test.tsx +9 -0
- package/src/components/error/error.module.scss +7 -0
- package/src/components/error/error.tsx +15 -0
- package/src/components/favourites/__tests__/favourites.test.tsx +13 -0
- package/src/components/favourites/favourites.tsx +98 -0
- package/src/components/hooks/hooks.tsx +24 -0
- package/src/components/loader/__tests__/loader.test.tsx +10 -0
- package/src/components/loader/loader.tsx +26 -0
- package/src/components/no-results/__tests__/no-results.test.tsx +9 -0
- package/src/components/no-results/no-results.tsx +9 -0
- package/src/components/observation/__tests__/observation.test.tsx +14 -0
- package/src/components/observation/observation.module.scss +20 -0
- package/src/components/observation/observation.tsx +29 -0
- package/src/components/recent-observations/__tests__/recent-observations.test.tsx +9 -0
- package/src/components/recent-observations/recent-observations.tsx +101 -0
- package/src/components/seasonality-graph/__tests__/seasonality-graph.test.tsx +9 -0
- package/src/components/seasonality-graph/seasonality-graph.module.scss +7 -0
- package/src/components/seasonality-graph/seasonality-graph.tsx +46 -0
- package/src/components/shared/css/general.module.scss +69 -0
- package/src/components/summary/ObserverList.tsx +18 -0
- package/src/components/summary/__tests__/summary.test.tsx +18 -0
- package/src/components/summary/summary.module.scss +88 -0
- package/src/components/summary/summary.tsx +83 -0
- package/src/components/tabs/__tests__/tabs.test.tsx +16 -0
- package/src/components/tabs/tabs.module.scss +28 -0
- package/src/components/tabs/tabs.tsx +44 -0
- package/src/components/taxon-panel/__tests__/taxon-panel.test.tsx +10 -0
- package/src/components/taxon-panel/taxon-panel.module.scss +7 -0
- package/src/components/taxon-panel/taxon-panel.tsx +104 -0
- package/src/components/year-dropdown/__tests__/year-dropdown.test.tsx +14 -0
- package/src/components/year-dropdown/year-dropdown.module.scss +3 -0
- package/src/components/year-dropdown/year-dropdown.tsx +33 -0
- package/src/index.ts +15 -0
- package/tsconfig.json +6 -0
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@imerss/inat-components",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "rollup -c",
|
|
8
|
+
"watch": "rollup -cw"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"date-fns": "^2.28.0",
|
|
12
|
+
"react-spinners": "^0.13.2",
|
|
13
|
+
"sleep-promise": "^9.1.0"
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"react": ">=16.8.0",
|
|
17
|
+
"react-dom": ">=16.8.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@modular-css/rollup": "^28.2.2",
|
|
21
|
+
"@rollup/plugin-typescript": "^8.3.3",
|
|
22
|
+
"@testing-library/react": "^13.3.0",
|
|
23
|
+
"@types/cli-progress": "^3.11.0",
|
|
24
|
+
"@types/jest": "^28.1.4",
|
|
25
|
+
"jest": "^28.1.2",
|
|
26
|
+
"node-sass": "^7.0.1",
|
|
27
|
+
"postcss": "^8.4.14",
|
|
28
|
+
"react-charts": "3.0.0-beta.38",
|
|
29
|
+
"rollup": "^2.75.7",
|
|
30
|
+
"rollup-plugin-dts": "^4.2.2",
|
|
31
|
+
"rollup-plugin-postcss": "^4.0.2",
|
|
32
|
+
"tslib": "^2.4.0",
|
|
33
|
+
"typescript": "^4.7.4"
|
|
34
|
+
},
|
|
35
|
+
"eslintConfig": {
|
|
36
|
+
"extends": [
|
|
37
|
+
"react-app",
|
|
38
|
+
"react-app/jest"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"browserslist": {
|
|
42
|
+
"production": [
|
|
43
|
+
">0.2%",
|
|
44
|
+
"not dead",
|
|
45
|
+
"not op_mini all"
|
|
46
|
+
],
|
|
47
|
+
"development": [
|
|
48
|
+
"last 1 chrome version",
|
|
49
|
+
"last 1 firefox version",
|
|
50
|
+
"last 1 safari version"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<meta name="theme-color" content="#000000" />
|
|
8
|
+
<meta
|
|
9
|
+
name="description"
|
|
10
|
+
content="Web site created using create-react-app"
|
|
11
|
+
/>
|
|
12
|
+
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
13
|
+
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
14
|
+
<title>iNat components</title>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
18
|
+
<div id="root"></div>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"short_name": "React App",
|
|
3
|
+
"name": "Create React App Sample",
|
|
4
|
+
"icons": [
|
|
5
|
+
{
|
|
6
|
+
"src": "favicon.ico",
|
|
7
|
+
"sizes": "64x64 32x32 24x24 16x16",
|
|
8
|
+
"type": "image/x-icon"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"src": "logo192.png",
|
|
12
|
+
"type": "image/png",
|
|
13
|
+
"sizes": "192x192"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"src": "logo512.png",
|
|
17
|
+
"type": "image/png",
|
|
18
|
+
"sizes": "512x512"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"start_url": ".",
|
|
22
|
+
"display": "standalone",
|
|
23
|
+
"theme_color": "#000000",
|
|
24
|
+
"background_color": "#ffffff"
|
|
25
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import typescript from "@rollup/plugin-typescript";
|
|
2
|
+
import postcss from "rollup-plugin-postcss";
|
|
3
|
+
import dts from "rollup-plugin-dts";
|
|
4
|
+
|
|
5
|
+
export default [
|
|
6
|
+
// front-end components + generate() node utils method, exposed as "@imerss/inat-components"
|
|
7
|
+
{
|
|
8
|
+
input: "src/index.ts",
|
|
9
|
+
output: {
|
|
10
|
+
dir: "dist",
|
|
11
|
+
format: "es"
|
|
12
|
+
},
|
|
13
|
+
external: [
|
|
14
|
+
"react"
|
|
15
|
+
],
|
|
16
|
+
plugins: [
|
|
17
|
+
postcss({
|
|
18
|
+
modules: true,
|
|
19
|
+
plugins: []
|
|
20
|
+
}),
|
|
21
|
+
typescript()
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
// not beautiful, but rolls up all typings for export
|
|
26
|
+
{
|
|
27
|
+
input: "src/index.ts",
|
|
28
|
+
output: {
|
|
29
|
+
dir: "dist",
|
|
30
|
+
format: "es"
|
|
31
|
+
},
|
|
32
|
+
plugins: [
|
|
33
|
+
dts()
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as C from "../constants";
|
|
2
|
+
|
|
3
|
+
export type CommonTaxaCallProps = {
|
|
4
|
+
year: string | number;
|
|
5
|
+
taxonId: string | number;
|
|
6
|
+
placeId: string | number;
|
|
7
|
+
perPage: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type CommonTaxData = {
|
|
11
|
+
id: number;
|
|
12
|
+
imageUrl: string;
|
|
13
|
+
taxonName: string;
|
|
14
|
+
taxonCommonName: string;
|
|
15
|
+
obsCount: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type CommonTaxaRespData = {
|
|
19
|
+
totalResults: number;
|
|
20
|
+
results: [CommonTaxData]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const getCommonTaxa = async ({ year, taxonId, placeId, perPage }: CommonTaxaCallProps): Promise<CommonTaxaRespData> => {
|
|
24
|
+
let url = `${C.BASE_API_URL}/v1/observations/species_counts?verifiable=true&spam=false&place_id=${placeId}&taxon_id=${taxonId}&locale=en-US&per_page=${perPage}`;
|
|
25
|
+
if (year !== "all") {
|
|
26
|
+
url += `&d1=${year}-01-01&d2=${year}-12-31`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const response = await fetch(url, {
|
|
30
|
+
method: 'GET',
|
|
31
|
+
headers: {
|
|
32
|
+
Accept: 'application/json'
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const resp = await response.json();
|
|
37
|
+
const sortedTaxa = resp.results.sort((a: any, b: any) => {
|
|
38
|
+
if (a.count > b.count) {
|
|
39
|
+
return -1;
|
|
40
|
+
} else if (a.count < b.count) {
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
totalResults: resp.total_results,
|
|
48
|
+
results: sortedTaxa.map((row: any) => ({
|
|
49
|
+
id: row.taxon.id,
|
|
50
|
+
imageUrl: row.taxon?.default_photo?.square_url || "",
|
|
51
|
+
obsCount: row.count,
|
|
52
|
+
taxonName: row.taxon.name || "",
|
|
53
|
+
taxonCommonName: row.taxon.preferred_common_name
|
|
54
|
+
}))
|
|
55
|
+
};
|
|
56
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as C from "../constants";
|
|
2
|
+
|
|
3
|
+
export type FavouritesCallProps = {
|
|
4
|
+
year: string | number;
|
|
5
|
+
taxonId: string | number;
|
|
6
|
+
placeId: string | number;
|
|
7
|
+
perPage: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type FavouritesData = {
|
|
11
|
+
id: number;
|
|
12
|
+
imageUrl: string;
|
|
13
|
+
taxonName: string;
|
|
14
|
+
taxonCommonName: string;
|
|
15
|
+
obsDate: string;
|
|
16
|
+
obsCount: number;
|
|
17
|
+
observerUsername: string;
|
|
18
|
+
numFaves: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type FavouritesRespData = {
|
|
22
|
+
totalResults: number;
|
|
23
|
+
results: [FavouritesData]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const getFavourites = async ({ year, taxonId, placeId, perPage }: FavouritesCallProps): Promise<FavouritesRespData> => {
|
|
27
|
+
let url = `${C.BASE_API_URL}/v1/observations?verifiable=true&order_by=votes&order=desc&page=1&spam=false&place_id=${placeId}&taxon_id=${taxonId}&locale=en-US&per_page=${perPage}`;
|
|
28
|
+
if (year !== "all") {
|
|
29
|
+
url += `&d1=${year}-01-01&d2=${year}-12-31`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const response = await fetch(url, {
|
|
33
|
+
// method: 'GET',
|
|
34
|
+
// headers: {
|
|
35
|
+
// Accept: 'application/json'
|
|
36
|
+
// }
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const resp = await response.json();
|
|
40
|
+
const sortedTaxa = resp.results.filter((i: any) => i.faves.length > 0).sort((a: any, b: any) => {
|
|
41
|
+
if (a.faves.length > b.faves.length) {
|
|
42
|
+
return -1;
|
|
43
|
+
} else if (a.faves.length < b.faves.length) {
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
return 0;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
totalResults: resp.total_results,
|
|
51
|
+
results: sortedTaxa.map((row: any) => ({
|
|
52
|
+
id: row.id,
|
|
53
|
+
imageUrl: row.taxon?.default_photo?.square_url || "",
|
|
54
|
+
obsDate: row.observed_on_string,
|
|
55
|
+
obsUrl: row.uri,
|
|
56
|
+
taxonName: row.taxon.name || "",
|
|
57
|
+
taxonCommonName: row.taxon.preferred_common_name,
|
|
58
|
+
numFaves: row.faves.length
|
|
59
|
+
}))
|
|
60
|
+
};
|
|
61
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as C from "../constants";
|
|
2
|
+
|
|
3
|
+
export type RecentObservationsCallProps = {
|
|
4
|
+
taxonId: string | number;
|
|
5
|
+
placeId: string | number;
|
|
6
|
+
perPage: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type RecentObservationData = {
|
|
10
|
+
id: number;
|
|
11
|
+
imageUrl: string;
|
|
12
|
+
obsUrl: string;
|
|
13
|
+
obsDate: string;
|
|
14
|
+
taxonName: string;
|
|
15
|
+
taxonCommonName: string;
|
|
16
|
+
observerUsername: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type RecentObservationsRespData = {
|
|
20
|
+
totalResults: number;
|
|
21
|
+
results: [RecentObservationData]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const getRecentObservations = async ({ taxonId, placeId, perPage }: RecentObservationsCallProps): Promise<RecentObservationsRespData> => {
|
|
25
|
+
const url = `${C.BASE_API_URL}/v1/observations?photos=true&per_page=${perPage}&taxon_id=${taxonId}&place_id=${placeId}&order=desc&order_by=observed_on`;
|
|
26
|
+
const response = await fetch(url);
|
|
27
|
+
const obs = await response.json();
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
totalResults: obs.total_results,
|
|
31
|
+
results: obs.results.map((obs: any) => {
|
|
32
|
+
return {
|
|
33
|
+
id: obs.id,
|
|
34
|
+
imageUrl: obs.observation_photos[0].photo.url,
|
|
35
|
+
obsUrl: obs.uri,
|
|
36
|
+
obsDate: obs.observed_on_string,
|
|
37
|
+
taxonName: obs?.taxon?.name || "",
|
|
38
|
+
taxonCommonName: obs?.taxon.preferred_common_name,
|
|
39
|
+
observerUsername: obs.user.login
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as C from '../constants';
|
|
2
|
+
|
|
3
|
+
export type SummaryApiProps = {
|
|
4
|
+
taxonId: number;
|
|
5
|
+
placeId: number;
|
|
6
|
+
year: string | number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const getSummary = async ({ taxonId, placeId, year }: SummaryApiProps): Promise<any> => {
|
|
10
|
+
const observers = await getObserverSummary(taxonId, placeId, year);
|
|
11
|
+
const observations = await getObservationSummary(taxonId, placeId, year);
|
|
12
|
+
const seasonalityData = await getSeasonalityData(taxonId, placeId, year);
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
observers,
|
|
16
|
+
observations,
|
|
17
|
+
seasonalityData
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type Observer = {
|
|
22
|
+
id: number;
|
|
23
|
+
userName: string;
|
|
24
|
+
numObservations: number;
|
|
25
|
+
iconUrl: string | null;
|
|
26
|
+
}
|
|
27
|
+
export type SummaryData = {
|
|
28
|
+
totalCount: number;
|
|
29
|
+
top: [Observer];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const getObserverSummary = async (taxonId: number, placeId: number, year: string | number): Promise<SummaryData> => {
|
|
33
|
+
let url = `${C.BASE_API_URL}/v1/observations/observers?verifiable=true&order_by=votes&order=desc&page=1&spam=false&place_id=${placeId}&taxon_id=${taxonId}&locale=en-US&per_page=10`;
|
|
34
|
+
if (year !== "all") {
|
|
35
|
+
url += `&d1=${year}-01-01&d2=${year}-12-31`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const response = await fetch(url);
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
totalCount: data.total_results,
|
|
43
|
+
top: data.results.map((result: any) => ({
|
|
44
|
+
id: result.user.id,
|
|
45
|
+
userName: result.user.login,
|
|
46
|
+
numObservations: result.observation_count,
|
|
47
|
+
iconUrl: result.user.icon_url
|
|
48
|
+
}))
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const getObservationSummary = async(taxonId: number, placeId: number, year: string | number) => {
|
|
53
|
+
let url = `${C.BASE_API_URL}/v1/observations?photos=true&per_page=1&taxon_id=${taxonId}&place_id=${placeId}&order=desc&order_by=observed_on`;
|
|
54
|
+
if (year !== "all") {
|
|
55
|
+
url += `&d1=${year}-01-01&d2=${year}-12-31`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const response = await fetch(url);
|
|
59
|
+
const data = await response.json();
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
totalCount: data.total_results
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type SeasonalityData = {
|
|
67
|
+
monthOfYear: {
|
|
68
|
+
[monthNum: string]: number
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const getSeasonalityData = async (taxonId: number, placeId: number, year: string | number): Promise<SeasonalityData> => {
|
|
73
|
+
let url = `${C.BASE_API_URL}/v1/observations/histogram?verifiable=true&taxon_id=${taxonId}&place_id=${placeId}&locale=en-US&date_field=observed&interval=month_of_year`;
|
|
74
|
+
if (year !== "all") {
|
|
75
|
+
url += `&d1=${year}-01-01&d2=${year}-12-31`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const response = await fetch(url);
|
|
79
|
+
const resp = await response.json();
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
monthOfYear: resp.results.month_of_year
|
|
83
|
+
};
|
|
84
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export type ConfigFile = {
|
|
2
|
+
taxa: TaxaConfig[];
|
|
3
|
+
places: PlaceConfig[];
|
|
4
|
+
features: {
|
|
5
|
+
[Feature.commonTaxa]?: {
|
|
6
|
+
numResults?: number;
|
|
7
|
+
};
|
|
8
|
+
[Feature.favourites]?: {
|
|
9
|
+
numResults?: number;
|
|
10
|
+
numYears?: number;
|
|
11
|
+
};
|
|
12
|
+
[Feature.recentObservations]?: {
|
|
13
|
+
numResults?: number;
|
|
14
|
+
numYears?: number;
|
|
15
|
+
};
|
|
16
|
+
[Feature.stats]?: {
|
|
17
|
+
numTopObservers?: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export enum DataSource {
|
|
23
|
+
autoLoad = "autoLoad",
|
|
24
|
+
dataProp = "dataProp",
|
|
25
|
+
url = "url"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export enum Feature {
|
|
29
|
+
commonTaxa = "commonTaxa",
|
|
30
|
+
favourites = "favourites",
|
|
31
|
+
recentObservations = "recentObservations",
|
|
32
|
+
stats = "stats"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// I know this is duplicated, but it's very possible they could be different in future
|
|
36
|
+
export enum Tab {
|
|
37
|
+
recent = "recent",
|
|
38
|
+
commonTaxa = "commonTaxa",
|
|
39
|
+
favourites = "favourites",
|
|
40
|
+
stats = "stats"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type BaseComponentProps = {
|
|
44
|
+
taxonId?: number;
|
|
45
|
+
placeId?: number;
|
|
46
|
+
filename?: string;
|
|
47
|
+
perPage?: number;
|
|
48
|
+
source?: DataSource;
|
|
49
|
+
data?: any;
|
|
50
|
+
dataUrl?: string;
|
|
51
|
+
className?: string;
|
|
52
|
+
components?: {
|
|
53
|
+
label?: any;
|
|
54
|
+
error?: any;
|
|
55
|
+
loader?: any;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type TaxaConfig = {
|
|
60
|
+
label: string;
|
|
61
|
+
short: string;
|
|
62
|
+
taxonId: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type PlaceConfig = {
|
|
66
|
+
label: string;
|
|
67
|
+
short: string;
|
|
68
|
+
placeId: number;
|
|
69
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {Feature, PlaceConfig, TaxaConfig} from "../typings";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Note that right now the source filenames aren't configurable.
|
|
5
|
+
*/
|
|
6
|
+
export const getSourceFile = (api: Feature, taxonInfo: TaxaConfig, placeInfo: PlaceConfig, year?: string | number): string => {
|
|
7
|
+
const yearStr = year === "all" ? "allyears" : year;
|
|
8
|
+
|
|
9
|
+
let filename = "";
|
|
10
|
+
if (api === Feature.recentObservations) {
|
|
11
|
+
filename = `${taxonInfo.short}-${placeInfo.short}-recent.json`;
|
|
12
|
+
} else if (api === Feature.commonTaxa) {
|
|
13
|
+
filename = `${taxonInfo.short}-${placeInfo.short}-${yearStr}-commonTaxa.json`;
|
|
14
|
+
} else if (api === Feature.favourites) {
|
|
15
|
+
filename = `${taxonInfo.short}-${placeInfo.short}-${yearStr}-favourites.json`;
|
|
16
|
+
} else if (api === Feature.stats) {
|
|
17
|
+
filename = `${taxonInfo.short}-${placeInfo.short}-${yearStr}-stats.json`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return filename;
|
|
21
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {format} from "date-fns";
|
|
2
|
+
|
|
3
|
+
export const getCurrentYear = () => new Date().getFullYear();
|
|
4
|
+
|
|
5
|
+
// not 100% this shows the date in the right timezone, but it's fine for BC
|
|
6
|
+
export const formatDate = (date: string, dateFormat = "MMM do, h:mm b"): any => {
|
|
7
|
+
let formattedDate = "";
|
|
8
|
+
try {
|
|
9
|
+
formattedDate = format(Date.parse(date), dateFormat);
|
|
10
|
+
} catch (e) {
|
|
11
|
+
console.log("Failed to parse date: ", date);
|
|
12
|
+
}
|
|
13
|
+
return formattedDate;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const numberWithCommas = (x: number) => (x || 0).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {render} from '@testing-library/react';
|
|
2
|
+
|
|
3
|
+
import { CommonTaxa } from '../common-taxa';
|
|
4
|
+
|
|
5
|
+
const defaultProps = {
|
|
6
|
+
year: 2022
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
describe('CommonTaxa', () => {
|
|
10
|
+
it('should render successfully', () => {
|
|
11
|
+
const {baseElement} = render(<CommonTaxa {...defaultProps} />);
|
|
12
|
+
expect(baseElement).toBeTruthy();
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, {useEffect, useState} from 'react';
|
|
2
|
+
import {numberWithCommas, C, CommonTaxData, getCommonTaxa, BaseComponentProps, DataSource} from "../../__shared";
|
|
3
|
+
import {Observation} from "../observation/observation";
|
|
4
|
+
import Loader from "../loader/loader";
|
|
5
|
+
import Error from "../error/error";
|
|
6
|
+
import {NoResults} from "../no-results/no-results";
|
|
7
|
+
import styles from "../shared/css/general.module.scss";
|
|
8
|
+
|
|
9
|
+
export type CommonTaxaProps = BaseComponentProps & {
|
|
10
|
+
year: string | number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const CommonTaxaLabel = (data: CommonTaxData) => (
|
|
14
|
+
<div className={styles.obsLabel}>
|
|
15
|
+
<h3>{data.taxonCommonName || data.taxonName}</h3>
|
|
16
|
+
<label className={styles.count}>{numberWithCommas(data.obsCount)}</label>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export const CommonTaxa = ({
|
|
21
|
+
year,
|
|
22
|
+
source,
|
|
23
|
+
taxonId,
|
|
24
|
+
placeId,
|
|
25
|
+
perPage = C.PER_PAGE,
|
|
26
|
+
data,
|
|
27
|
+
dataUrl,
|
|
28
|
+
components,
|
|
29
|
+
className
|
|
30
|
+
}: CommonTaxaProps) => {
|
|
31
|
+
const [taxa, setTaxa] = useState<any>(() => (source === DataSource.dataProp) ? data : []);
|
|
32
|
+
const [loading, setLoading] = useState(false);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (source !== DataSource.autoLoad) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!taxonId) {
|
|
39
|
+
console.error("Please supply a `taxonId` prop for the `autoLoad` source prop option.");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!placeId) {
|
|
43
|
+
console.error("Please supply a `placeId` prop for the `autoLoad` source prop option.");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
(async () => {
|
|
48
|
+
setLoading(true);
|
|
49
|
+
const resp = await getCommonTaxa({taxonId, placeId, year, perPage});
|
|
50
|
+
setTaxa(resp.results);
|
|
51
|
+
setLoading(false);
|
|
52
|
+
})();
|
|
53
|
+
}, [source, year, placeId, taxonId, perPage]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (source !== DataSource.url) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!dataUrl) {
|
|
61
|
+
console.error("Please supply a `dataUrl` prop for the `url` source prop option.");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
(async () => {
|
|
66
|
+
setLoading(true);
|
|
67
|
+
const obs = await fetch(dataUrl);
|
|
68
|
+
const json = await obs.json();
|
|
69
|
+
setTaxa(json.results);
|
|
70
|
+
setLoading(false);
|
|
71
|
+
})();
|
|
72
|
+
}, [source, dataUrl]);
|
|
73
|
+
|
|
74
|
+
const Load = components?.loader ? components.loader as any : Loader;
|
|
75
|
+
const Label = components?.label ? components.label as any : CommonTaxaLabel;
|
|
76
|
+
const ErrorMsg = components?.error ? components.error as any : Error;
|
|
77
|
+
|
|
78
|
+
let classes = styles.panel;
|
|
79
|
+
if (className) {
|
|
80
|
+
classes += ` ${className}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className={classes}>
|
|
85
|
+
<Load loading={loading}/>
|
|
86
|
+
{!loading && taxa.length === 0 && <NoResults/>}
|
|
87
|
+
<div className={styles.grid}>
|
|
88
|
+
{taxa.map((data: CommonTaxData) => (
|
|
89
|
+
<Observation
|
|
90
|
+
key={data.id}
|
|
91
|
+
imageUrl={data.imageUrl.replace(/square/, "medium")}
|
|
92
|
+
linkUrl={`${C.BASE_URL}/${data.id}`}>
|
|
93
|
+
<Label {...data} />
|
|
94
|
+
</Observation>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import styles from './error.module.scss';
|
|
2
|
+
|
|
3
|
+
/* eslint-disable-next-line */
|
|
4
|
+
export interface ErrorProps {
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const Error = (props: ErrorProps) => {
|
|
8
|
+
return (
|
|
9
|
+
<div className={styles['container']}>
|
|
10
|
+
<h1>Welcome to Error!</h1>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default Error;
|