@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.
Files changed (60) hide show
  1. package/README.md +0 -0
  2. package/declaration.d.ts +2 -0
  3. package/dist/index.d.ts +214 -0
  4. package/dist/index.js +876 -0
  5. package/package.json +53 -0
  6. package/public/favicon.ico +0 -0
  7. package/public/index.html +20 -0
  8. package/public/logo192.png +0 -0
  9. package/public/logo512.png +0 -0
  10. package/public/manifest.json +25 -0
  11. package/public/robots.txt +3 -0
  12. package/rollup.config.js +36 -0
  13. package/src/__shared/api/common-taxa.ts +56 -0
  14. package/src/__shared/api/favourites.ts +61 -0
  15. package/src/__shared/api/index.ts +4 -0
  16. package/src/__shared/api/recent-observations.ts +43 -0
  17. package/src/__shared/api/summary.ts +84 -0
  18. package/src/__shared/constants.ts +4 -0
  19. package/src/__shared/index.ts +4 -0
  20. package/src/__shared/typings.ts +69 -0
  21. package/src/__shared/utils/configUtils.ts +21 -0
  22. package/src/__shared/utils/dateUtils.ts +14 -0
  23. package/src/__shared/utils/index.ts +3 -0
  24. package/src/__shared/utils/numberUtils.ts +1 -0
  25. package/src/components/common-taxa/__tests__/common-taxa.test.tsx +14 -0
  26. package/src/components/common-taxa/common-taxa.tsx +99 -0
  27. package/src/components/error/__tests__/error.test.tsx +9 -0
  28. package/src/components/error/error.module.scss +7 -0
  29. package/src/components/error/error.tsx +15 -0
  30. package/src/components/favourites/__tests__/favourites.test.tsx +13 -0
  31. package/src/components/favourites/favourites.tsx +98 -0
  32. package/src/components/hooks/hooks.tsx +24 -0
  33. package/src/components/loader/__tests__/loader.test.tsx +10 -0
  34. package/src/components/loader/loader.tsx +26 -0
  35. package/src/components/no-results/__tests__/no-results.test.tsx +9 -0
  36. package/src/components/no-results/no-results.tsx +9 -0
  37. package/src/components/observation/__tests__/observation.test.tsx +14 -0
  38. package/src/components/observation/observation.module.scss +20 -0
  39. package/src/components/observation/observation.tsx +29 -0
  40. package/src/components/recent-observations/__tests__/recent-observations.test.tsx +9 -0
  41. package/src/components/recent-observations/recent-observations.tsx +101 -0
  42. package/src/components/seasonality-graph/__tests__/seasonality-graph.test.tsx +9 -0
  43. package/src/components/seasonality-graph/seasonality-graph.module.scss +7 -0
  44. package/src/components/seasonality-graph/seasonality-graph.tsx +46 -0
  45. package/src/components/shared/css/general.module.scss +69 -0
  46. package/src/components/summary/ObserverList.tsx +18 -0
  47. package/src/components/summary/__tests__/summary.test.tsx +18 -0
  48. package/src/components/summary/summary.module.scss +88 -0
  49. package/src/components/summary/summary.tsx +83 -0
  50. package/src/components/tabs/__tests__/tabs.test.tsx +16 -0
  51. package/src/components/tabs/tabs.module.scss +28 -0
  52. package/src/components/tabs/tabs.tsx +44 -0
  53. package/src/components/taxon-panel/__tests__/taxon-panel.test.tsx +10 -0
  54. package/src/components/taxon-panel/taxon-panel.module.scss +7 -0
  55. package/src/components/taxon-panel/taxon-panel.tsx +104 -0
  56. package/src/components/year-dropdown/__tests__/year-dropdown.test.tsx +14 -0
  57. package/src/components/year-dropdown/year-dropdown.module.scss +3 -0
  58. package/src/components/year-dropdown/year-dropdown.tsx +33 -0
  59. package/src/index.ts +15 -0
  60. 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
+ }
@@ -0,0 +1,3 @@
1
+ # https://www.robotstxt.org/robotstxt.html
2
+ User-agent: *
3
+ Disallow:
@@ -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,4 @@
1
+ export * from "./common-taxa";
2
+ export * from "./favourites";
3
+ export * from "./recent-observations";
4
+ export * from "./summary";
@@ -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,4 @@
1
+ export const BASE_API_URL = 'https://api.inaturalist.org';
2
+ export const PER_PAGE = 100;
3
+ export const BASE_URL = "https://www.inaturalist.org"; // /people/name_here
4
+ export const ERROR_LOG_FILENAME = "inat-components-error.log";
@@ -0,0 +1,4 @@
1
+ export * from "./api";
2
+ export * from "./utils";
3
+ export * as C from "./constants";
4
+ export * from "./typings";
@@ -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,3 @@
1
+ export * from "./configUtils";
2
+ export * from "./dateUtils";
3
+ export * from "./numberUtils";
@@ -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,9 @@
1
+ import {render} from '@testing-library/react';
2
+ import Error from '../error';
3
+
4
+ describe('Error', () => {
5
+ it('should render successfully', () => {
6
+ const {baseElement} = render(<Error />);
7
+ expect(baseElement).toBeTruthy();
8
+ });
9
+ });
@@ -0,0 +1,7 @@
1
+ /*
2
+ * Replace this with your own classes
3
+ *
4
+ * e.g.
5
+ * .container {
6
+ * }
7
+ */
@@ -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;