@performant-software/core-data 1.2.0-beta.2 → 1.2.0-beta.21
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/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/main.css +17 -5
- package/package.json +13 -3
- package/postcss.config.js +6 -0
- package/src/components/MediaGallery.js +1 -1
- package/src/components/PlaceDetailsPanel.js +14 -1
- package/src/components/PlaceMarker.js +6 -0
- package/src/components/PlaceResultsList.css +7 -0
- package/src/components/PlaceResultsList.js +178 -0
- package/src/components/{PlaceDetailsPanel.css → RelatedItemsList.css} +5 -5
- package/src/components/RelatedItemsList.js +14 -3
- package/src/components/RelatedList.js +15 -1
- package/src/components/RelatedMedia.js +14 -0
- package/src/components/RelatedOrganizations.js +8 -1
- package/src/components/RelatedPeople.js +8 -1
- package/src/components/RelatedPlaces.js +8 -1
- package/src/components/RelatedTaxonomies.js +8 -1
- package/src/i18n/en.json +27 -0
- package/src/i18n/i18n.js +26 -0
- package/src/index.css +3 -0
- package/src/index.js +4 -0
- package/src/types/Feature.js +10 -0
- package/src/types/typesense/Place.js +14 -0
- package/tailwind.config.js +19 -0
- package/types/components/MediaGallery.js.flow +1 -1
- package/types/components/PlaceDetailsPanel.js.flow +14 -1
- package/types/components/PlaceMarker.js.flow +6 -0
- package/types/components/PlaceResultsList.js.flow +178 -0
- package/types/components/RelatedItemsList.js.flow +14 -3
- package/types/components/RelatedList.js.flow +15 -1
- package/types/components/RelatedMedia.js.flow +14 -0
- package/types/components/RelatedOrganizations.js.flow +8 -1
- package/types/components/RelatedPeople.js.flow +8 -1
- package/types/components/RelatedPlaces.js.flow +8 -1
- package/types/components/RelatedTaxonomies.js.flow +8 -1
- package/types/components/SearchResultsList.js.flow +160 -0
- package/types/i18n/i18n.js.flow +26 -0
- package/types/index.js.flow +4 -0
- package/types/types/Feature.js.flow +10 -0
- package/types/types/typesense/Place.js.flow +14 -0
- package/webpack.config.js +31 -1
|
@@ -4,25 +4,36 @@ import * as Accordion from '@radix-ui/react-accordion';
|
|
|
4
4
|
import { AlertCircle, ChevronDown } from 'lucide-react';
|
|
5
5
|
import React from 'react';
|
|
6
6
|
import _ from 'underscore';
|
|
7
|
+
import i18n from '../i18n/i18n';
|
|
7
8
|
import LoadAnimation from './LoadAnimation';
|
|
9
|
+
import type { RelatedItems } from '../types/RelatedItems';
|
|
8
10
|
import RelatedMedia from './RelatedMedia';
|
|
9
11
|
import RelatedOrganizations from './RelatedOrganizations';
|
|
10
12
|
import RelatedPeople from './RelatedPeople';
|
|
11
13
|
import RelatedPlaces from './RelatedPlaces';
|
|
12
14
|
import RelatedTaxonomies from './RelatedTaxonomies';
|
|
13
|
-
import
|
|
15
|
+
import './RelatedItemsList.css';
|
|
14
16
|
|
|
15
17
|
type Props = {
|
|
18
|
+
/**
|
|
19
|
+
* A list of related items.
|
|
20
|
+
*/
|
|
16
21
|
items: Array<RelatedItems>
|
|
17
22
|
};
|
|
18
23
|
|
|
24
|
+
/**
|
|
25
|
+
* This component renders the passed list of related items in an accordion fashion.
|
|
26
|
+
*/
|
|
19
27
|
const RelatedItemsList = (props: Props) => {
|
|
20
28
|
if (_.isEmpty(props.items)) {
|
|
21
29
|
return null;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
return (
|
|
25
|
-
<Accordion.Root
|
|
33
|
+
<Accordion.Root
|
|
34
|
+
className='related-items-list'
|
|
35
|
+
type='multiple'
|
|
36
|
+
>
|
|
26
37
|
{ _.map(props.items, ({ data, error, ...conf }) => (
|
|
27
38
|
<Accordion.Item
|
|
28
39
|
key={conf.endpoint}
|
|
@@ -37,7 +48,7 @@ const RelatedItemsList = (props: Props) => {
|
|
|
37
48
|
{ conf.ui_label }
|
|
38
49
|
{ data && (
|
|
39
50
|
<span className='ml-1'>
|
|
40
|
-
({ data.items
|
|
51
|
+
{ i18n.t('RelatedItemsList.labels.count', { count: data.items?.length })}
|
|
41
52
|
</span>
|
|
42
53
|
)}
|
|
43
54
|
{ error && (
|
|
@@ -9,11 +9,25 @@ type Item = {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
type Props = {
|
|
12
|
+
/**
|
|
13
|
+
* An annotation page containing the list of records to render.
|
|
14
|
+
*/
|
|
12
15
|
data: AnnotationPage<Item>,
|
|
13
|
-
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A message to display when the list is empty.
|
|
19
|
+
*/
|
|
20
|
+
emptyMessage: string,
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Render function used to determine how to present the passed item.
|
|
24
|
+
*/
|
|
14
25
|
renderItem: (item: Item) => JSX.Element
|
|
15
26
|
};
|
|
16
27
|
|
|
28
|
+
/**
|
|
29
|
+
* This component is a helper component used to structure the lists for the other `Related*` comnponents.
|
|
30
|
+
*/
|
|
17
31
|
const RelatedList = (props: Props) => {
|
|
18
32
|
const { items } = props.data;
|
|
19
33
|
|
|
@@ -8,14 +8,28 @@ import type { MediaContent } from '../types/MediaContent';
|
|
|
8
8
|
import MediaGallery from './MediaGallery';
|
|
9
9
|
|
|
10
10
|
type Props = {
|
|
11
|
+
/**
|
|
12
|
+
* The annotation page containing the media content to render.
|
|
13
|
+
*/
|
|
11
14
|
data: AnnotationPage<MediaContent>,
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Media thumbnail height.
|
|
18
|
+
*/
|
|
12
19
|
thumbnailHeight?: number,
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Media thumbnail width.
|
|
23
|
+
*/
|
|
13
24
|
thumbnailWidth?: number
|
|
14
25
|
};
|
|
15
26
|
|
|
16
27
|
const DEFAULT_THUMBNAIL_HEIGHT = 80;
|
|
17
28
|
const DEFAULT_THUMBNAIL_WIDTH = 80;
|
|
18
29
|
|
|
30
|
+
/**
|
|
31
|
+
* This component renders the related Core Data media records as well as a gallery viewer.
|
|
32
|
+
*/
|
|
19
33
|
const RelatedMedia = (props: Props) => {
|
|
20
34
|
const [showGallery, setShowGallery] = useState<MediaContent>();
|
|
21
35
|
|
|
@@ -2,18 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
import { Building2 } from 'lucide-react';
|
|
4
4
|
import React from 'react';
|
|
5
|
+
import i18n from '../i18n/i18n';
|
|
5
6
|
import type { AnnotationPage } from '../types/AnnotationPage';
|
|
6
7
|
import type { Organization } from '../types/Organization';
|
|
7
8
|
import RelatedList from './RelatedList';
|
|
8
9
|
|
|
9
10
|
type Props = {
|
|
11
|
+
/**
|
|
12
|
+
* The annotation page containing the Core Data organizations to render.
|
|
13
|
+
*/
|
|
10
14
|
data: AnnotationPage<Organization>
|
|
11
15
|
};
|
|
12
16
|
|
|
17
|
+
/**
|
|
18
|
+
* This component renders the related Core Data organizations records.
|
|
19
|
+
*/
|
|
13
20
|
const RelatedOrganizations = (props: Props) => (
|
|
14
21
|
<RelatedList
|
|
15
22
|
data={props.data}
|
|
16
|
-
emptyMessage={'
|
|
23
|
+
emptyMessage={i18n.t('RelatedOrganizations.labels.empty')}
|
|
17
24
|
renderItem={(organization) => (
|
|
18
25
|
<>
|
|
19
26
|
<Building2
|
|
@@ -2,18 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
import { UserCircle } from 'lucide-react';
|
|
4
4
|
import React from 'react';
|
|
5
|
+
import i18n from '../i18n/i18n';
|
|
5
6
|
import type { AnnotationPage } from '../types/AnnotationPage';
|
|
6
7
|
import type { Person } from '../types/Person';
|
|
7
8
|
import RelatedList from './RelatedList';
|
|
8
9
|
|
|
9
10
|
type Props = {
|
|
11
|
+
/**
|
|
12
|
+
* The annotation page containing the Core Data people to render.
|
|
13
|
+
*/
|
|
10
14
|
data: AnnotationPage<Person>
|
|
11
15
|
};
|
|
12
16
|
|
|
17
|
+
/**
|
|
18
|
+
* This component renders the related Core Data organizations.
|
|
19
|
+
*/
|
|
13
20
|
const RelatedPeople = (props: Props) => (
|
|
14
21
|
<RelatedList
|
|
15
22
|
data={props.data}
|
|
16
|
-
emptyMessage={'
|
|
23
|
+
emptyMessage={i18n.t('RelatedPeople.labels.empty')}
|
|
17
24
|
renderItem={(person) => (
|
|
18
25
|
<>
|
|
19
26
|
<UserCircle
|
|
@@ -2,18 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
import { MapPin } from 'lucide-react';
|
|
4
4
|
import React from 'react';
|
|
5
|
+
import i18n from '../i18n/i18n';
|
|
5
6
|
import type { AnnotationPage } from '../types/AnnotationPage';
|
|
6
7
|
import type { Place } from '../types/Place';
|
|
7
8
|
import RelatedList from './RelatedList';
|
|
8
9
|
|
|
9
10
|
type Props = {
|
|
11
|
+
/**
|
|
12
|
+
* The annotation page containing the Core Data places to render.
|
|
13
|
+
*/
|
|
10
14
|
data: AnnotationPage<Place>
|
|
11
15
|
};
|
|
12
16
|
|
|
17
|
+
/**
|
|
18
|
+
* This component renders the related Core Data places.
|
|
19
|
+
*/
|
|
13
20
|
const RelatedPlaces = (props: Props) => (
|
|
14
21
|
<RelatedList
|
|
15
22
|
data={props.data}
|
|
16
|
-
emptyMessage={'
|
|
23
|
+
emptyMessage={i18n.t('RelatedPlaces.labels.empty')}
|
|
17
24
|
renderItem={(place) => (
|
|
18
25
|
<>
|
|
19
26
|
<MapPin
|
|
@@ -2,18 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
import { ListTree } from 'lucide-react';
|
|
4
4
|
import React from 'react';
|
|
5
|
+
import i18n from '../i18n/i18n';
|
|
5
6
|
import type { AnnotationPage } from '../types/AnnotationPage';
|
|
6
7
|
import RelatedList from './RelatedList';
|
|
7
8
|
import type { Taxonomy } from '../types/Taxonomy';
|
|
8
9
|
|
|
9
10
|
type Props = {
|
|
11
|
+
/**
|
|
12
|
+
* The annotation page containing the Core Data taxonomies to render.
|
|
13
|
+
*/
|
|
10
14
|
data: AnnotationPage<Taxonomy>
|
|
11
15
|
};
|
|
12
16
|
|
|
17
|
+
/**
|
|
18
|
+
* This component renders the related Core Data taxonomies.
|
|
19
|
+
*/
|
|
13
20
|
const RelatedTaxonomies = (props: Props) => (
|
|
14
21
|
<RelatedList
|
|
15
22
|
data={props.data}
|
|
16
|
-
emptyMessage={'
|
|
23
|
+
emptyMessage={i18n.t('RelatedTaxonomies.labels.empty')}
|
|
17
24
|
renderItem={(taxonomy) => (
|
|
18
25
|
<>
|
|
19
26
|
<ListTree
|
package/src/i18n/en.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"RelatedItemsList": {
|
|
3
|
+
"labels": {
|
|
4
|
+
"count": "({{count}})"
|
|
5
|
+
}
|
|
6
|
+
},
|
|
7
|
+
"RelatedOrganizations": {
|
|
8
|
+
"labels": {
|
|
9
|
+
"empty": "No related organizations"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"RelatedPeople": {
|
|
13
|
+
"labels": {
|
|
14
|
+
"empty": "No related people"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"RelatedPlaces": {
|
|
18
|
+
"labels": {
|
|
19
|
+
"empty": "No related places"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"RelatedTaxonomies": {
|
|
23
|
+
"labels": {
|
|
24
|
+
"empty": "No related taxonomies"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/i18n/i18n.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import i18next from 'i18next';
|
|
4
|
+
|
|
5
|
+
import en from './en.json';
|
|
6
|
+
|
|
7
|
+
const resources = {
|
|
8
|
+
en: {
|
|
9
|
+
translation: en
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const i18n = i18next.createInstance();
|
|
14
|
+
|
|
15
|
+
i18n
|
|
16
|
+
.init({
|
|
17
|
+
debug: true,
|
|
18
|
+
fallbackLng: 'en',
|
|
19
|
+
lng: 'en',
|
|
20
|
+
interpolation: {
|
|
21
|
+
escapeValue: false,
|
|
22
|
+
},
|
|
23
|
+
resources
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export default i18n;
|
package/src/index.css
ADDED
package/src/index.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
|
|
3
|
+
// CSS
|
|
4
|
+
import './index.css';
|
|
5
|
+
|
|
3
6
|
// Components
|
|
4
7
|
export { default as LoadAnimation } from './components/LoadAnimation';
|
|
5
8
|
export { default as MediaGallery } from './components/MediaGallery';
|
|
6
9
|
export { default as PlaceDetailsPanel } from './components/PlaceDetailsPanel';
|
|
7
10
|
export { default as PlaceMarker } from './components/PlaceMarker';
|
|
11
|
+
export { default as PlaceResultsList } from './components/PlaceResultsList';
|
|
8
12
|
export { default as RelatedItemsList } from './components/RelatedItemsList';
|
|
9
13
|
export { default as RelatedList } from './components/RelatedList';
|
|
10
14
|
export { default as RelatedMedia } from './components/RelatedMedia';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
content: [
|
|
4
|
+
'./src/index.css',
|
|
5
|
+
'./src/**/*.js'
|
|
6
|
+
],
|
|
7
|
+
theme: {
|
|
8
|
+
extend: {
|
|
9
|
+
colors: {
|
|
10
|
+
muted: '#0005119e'
|
|
11
|
+
},
|
|
12
|
+
fontFamily: {
|
|
13
|
+
sans: ['-apple-system', 'Roboto', 'sans-serif'],
|
|
14
|
+
'dm-display': ['DM Serif Display'],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
plugins: []
|
|
19
|
+
};
|
|
@@ -5,7 +5,6 @@ import Viewer from '@samvera/clover-iiif/viewer';
|
|
|
5
5
|
import { Image, X } from 'lucide-react';
|
|
6
6
|
import React from 'react';
|
|
7
7
|
import type { MediaContent } from '../types/MediaContent';
|
|
8
|
-
|
|
9
8
|
import './MediaGallery.css';
|
|
10
9
|
|
|
11
10
|
type Props = {
|
|
@@ -30,6 +29,7 @@ type Props = {
|
|
|
30
29
|
*/
|
|
31
30
|
const MediaGallery = (props: Props) => (
|
|
32
31
|
<Dialog.Root
|
|
32
|
+
className='media-gallery'
|
|
33
33
|
onOpenChange={props.onClose}
|
|
34
34
|
open={Boolean(props.defaultItem)}
|
|
35
35
|
>
|
|
@@ -6,14 +6,27 @@ import _ from 'underscore';
|
|
|
6
6
|
import type { Place } from '../types/Place';
|
|
7
7
|
import type { RelatedItems } from '../types/RelatedItems';
|
|
8
8
|
import RelatedItemsList from './RelatedItemsList';
|
|
9
|
-
import './PlaceDetailsPanel.css';
|
|
10
9
|
|
|
11
10
|
type Props = {
|
|
11
|
+
/**
|
|
12
|
+
* The place record to render.
|
|
13
|
+
*/
|
|
12
14
|
place?: Place,
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A list of items related to the place.
|
|
18
|
+
*/
|
|
13
19
|
related: Array<RelatedItems>,
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Callback fired when the panel is closed.
|
|
23
|
+
*/
|
|
14
24
|
onClose: () => void
|
|
15
25
|
};
|
|
16
26
|
|
|
27
|
+
/**
|
|
28
|
+
* This component renders a side panel for the passed Core Data place record.
|
|
29
|
+
*/
|
|
17
30
|
const PlaceDetailsPanel = (props: Props) => {
|
|
18
31
|
const el = useRef<HTMLElement>(null);
|
|
19
32
|
|
|
@@ -4,6 +4,11 @@ import { LocationMarker } from '@performant-software/geospatial';
|
|
|
4
4
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
5
5
|
|
|
6
6
|
type Props = {
|
|
7
|
+
/**
|
|
8
|
+
* TODO: Comment me.
|
|
9
|
+
*/
|
|
10
|
+
map?: any,
|
|
11
|
+
|
|
7
12
|
/**
|
|
8
13
|
* The URL of the Core Data place record.
|
|
9
14
|
*/
|
|
@@ -52,6 +57,7 @@ const PlaceMarker = (props: Props) => {
|
|
|
52
57
|
return (
|
|
53
58
|
<LocationMarker
|
|
54
59
|
data={place}
|
|
60
|
+
map={props.map}
|
|
55
61
|
/>
|
|
56
62
|
);
|
|
57
63
|
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import React, { useMemo } from 'react';
|
|
4
|
+
import { Highlight } from 'react-instantsearch';
|
|
5
|
+
import AutoSizer from 'react-virtualized-auto-sizer';
|
|
6
|
+
import { FixedSizeList } from 'react-window';
|
|
7
|
+
import _ from 'underscore';
|
|
8
|
+
import type { Feature } from '../types/Feature';
|
|
9
|
+
import type { Place } from '../types/typesense/Place';
|
|
10
|
+
import './PlaceResultsList.css';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Converts the passed place result to a feature.
|
|
14
|
+
*
|
|
15
|
+
* @param result
|
|
16
|
+
*
|
|
17
|
+
* @returns {{
|
|
18
|
+
* geometry: {coordinates: number[], type: string},
|
|
19
|
+
* id: number,
|
|
20
|
+
* type:
|
|
21
|
+
* string,
|
|
22
|
+
* properties: {
|
|
23
|
+
* ccode: *[],
|
|
24
|
+
* record_id: string,
|
|
25
|
+
* names: *,
|
|
26
|
+
* name: string,
|
|
27
|
+
* id: string,
|
|
28
|
+
* title: string,
|
|
29
|
+
* type: string,
|
|
30
|
+
* uuid: string
|
|
31
|
+
* }
|
|
32
|
+
* }}
|
|
33
|
+
*/
|
|
34
|
+
const toFeature = (result: Place) => ({
|
|
35
|
+
id: parseInt(result.record_id, 10),
|
|
36
|
+
type: 'Feature',
|
|
37
|
+
properties: {
|
|
38
|
+
id: result.record_id,
|
|
39
|
+
ccode: [],
|
|
40
|
+
title: result.name,
|
|
41
|
+
uuid: result.uuid,
|
|
42
|
+
record_id: result.record_id,
|
|
43
|
+
name: result.name,
|
|
44
|
+
names: _.map(result.names, (toponym) => ({ toponym })),
|
|
45
|
+
type: result.type
|
|
46
|
+
},
|
|
47
|
+
geometry: {
|
|
48
|
+
type: 'Point',
|
|
49
|
+
coordinates: result.coordinates.slice().reverse()
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
type HitComponentProps = {
|
|
54
|
+
hit: any,
|
|
55
|
+
isHovered: boolean,
|
|
56
|
+
onClick: () => void
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const HitComponent = (props: HitComponentProps) => {
|
|
60
|
+
const { hit } = props;
|
|
61
|
+
|
|
62
|
+
const className = useMemo(() => {
|
|
63
|
+
const classNames = [
|
|
64
|
+
'h-[5.5em]',
|
|
65
|
+
'border-b',
|
|
66
|
+
'flex',
|
|
67
|
+
'flex-col',
|
|
68
|
+
'justify-start'
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
if (props.isHovered) {
|
|
72
|
+
classNames.add('bg-teal-700/30');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return classNames.join(' ');
|
|
76
|
+
}, [props.isHovered]);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
className={className}
|
|
81
|
+
>
|
|
82
|
+
<button
|
|
83
|
+
className='py-2 px-3 flex-grow text-left inline-flex flex-col'
|
|
84
|
+
onClick={props.onClick}
|
|
85
|
+
type='button'
|
|
86
|
+
>
|
|
87
|
+
<Highlight
|
|
88
|
+
attribute='name'
|
|
89
|
+
className='line-clamp-2'
|
|
90
|
+
hit={hit}
|
|
91
|
+
/>
|
|
92
|
+
<p
|
|
93
|
+
className='text-muted text-xs line-clamp-1'
|
|
94
|
+
>
|
|
95
|
+
<Highlight
|
|
96
|
+
hit={hit}
|
|
97
|
+
attribute='names'
|
|
98
|
+
/>
|
|
99
|
+
</p>
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
type Props = {
|
|
106
|
+
/**
|
|
107
|
+
* An array of search results representing `/typesense/Place` objects.
|
|
108
|
+
*/
|
|
109
|
+
hits: Array<Place>,
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The object that is currently occupying the hover state.
|
|
113
|
+
*/
|
|
114
|
+
hover?: Feature<{ id: string }>,
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Callback fired when the hover item is changed.
|
|
118
|
+
*/
|
|
119
|
+
onHoverChange: (hover?: Feature<{ id: string }>) => void,
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Callback fired when a search result is clicked.
|
|
123
|
+
*/
|
|
124
|
+
onClick: (result: Place) => void
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
type RowProps = {
|
|
128
|
+
index: number,
|
|
129
|
+
style: any
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* This component renders a list of search results returned from a Core Data Typesense index.
|
|
134
|
+
*/
|
|
135
|
+
const PlaceResultsList = (props: Props) => {
|
|
136
|
+
const {
|
|
137
|
+
hits,
|
|
138
|
+
hover,
|
|
139
|
+
onClick,
|
|
140
|
+
onHoverChange
|
|
141
|
+
} = props;
|
|
142
|
+
|
|
143
|
+
const Row = ({ index, style }: RowProps) => {
|
|
144
|
+
const hit = hits[index];
|
|
145
|
+
const id = parseInt(hit.record_id, 10);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div
|
|
149
|
+
style={style}
|
|
150
|
+
onPointerEnter={() => onHoverChange(hover?.id === id ? hover : toFeature(hit))}
|
|
151
|
+
onPointerLeave={() => onHoverChange(undefined)}
|
|
152
|
+
>
|
|
153
|
+
<HitComponent
|
|
154
|
+
hit={hit}
|
|
155
|
+
isHovered={hover?.id === parseInt(hit?.record_id, 10)}
|
|
156
|
+
onClick={() => onClick(hit)}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<AutoSizer>
|
|
164
|
+
{({ height, width }) => (
|
|
165
|
+
<FixedSizeList
|
|
166
|
+
height={height}
|
|
167
|
+
itemCount={hits.length}
|
|
168
|
+
width={width}
|
|
169
|
+
itemSize={88}
|
|
170
|
+
>
|
|
171
|
+
{ Row }
|
|
172
|
+
</FixedSizeList>
|
|
173
|
+
)}
|
|
174
|
+
</AutoSizer>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export default PlaceResultsList;
|
|
@@ -4,25 +4,36 @@ import * as Accordion from '@radix-ui/react-accordion';
|
|
|
4
4
|
import { AlertCircle, ChevronDown } from 'lucide-react';
|
|
5
5
|
import React from 'react';
|
|
6
6
|
import _ from 'underscore';
|
|
7
|
+
import i18n from '../i18n/i18n';
|
|
7
8
|
import LoadAnimation from './LoadAnimation';
|
|
9
|
+
import type { RelatedItems } from '../types/RelatedItems';
|
|
8
10
|
import RelatedMedia from './RelatedMedia';
|
|
9
11
|
import RelatedOrganizations from './RelatedOrganizations';
|
|
10
12
|
import RelatedPeople from './RelatedPeople';
|
|
11
13
|
import RelatedPlaces from './RelatedPlaces';
|
|
12
14
|
import RelatedTaxonomies from './RelatedTaxonomies';
|
|
13
|
-
import
|
|
15
|
+
import './RelatedItemsList.css';
|
|
14
16
|
|
|
15
17
|
type Props = {
|
|
18
|
+
/**
|
|
19
|
+
* A list of related items.
|
|
20
|
+
*/
|
|
16
21
|
items: Array<RelatedItems>
|
|
17
22
|
};
|
|
18
23
|
|
|
24
|
+
/**
|
|
25
|
+
* This component renders the passed list of related items in an accordion fashion.
|
|
26
|
+
*/
|
|
19
27
|
const RelatedItemsList = (props: Props) => {
|
|
20
28
|
if (_.isEmpty(props.items)) {
|
|
21
29
|
return null;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
return (
|
|
25
|
-
<Accordion.Root
|
|
33
|
+
<Accordion.Root
|
|
34
|
+
className='related-items-list'
|
|
35
|
+
type='multiple'
|
|
36
|
+
>
|
|
26
37
|
{ _.map(props.items, ({ data, error, ...conf }) => (
|
|
27
38
|
<Accordion.Item
|
|
28
39
|
key={conf.endpoint}
|
|
@@ -37,7 +48,7 @@ const RelatedItemsList = (props: Props) => {
|
|
|
37
48
|
{ conf.ui_label }
|
|
38
49
|
{ data && (
|
|
39
50
|
<span className='ml-1'>
|
|
40
|
-
({ data.items
|
|
51
|
+
{ i18n.t('RelatedItemsList.labels.count', { count: data.items?.length })}
|
|
41
52
|
</span>
|
|
42
53
|
)}
|
|
43
54
|
{ error && (
|
|
@@ -9,11 +9,25 @@ type Item = {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
type Props = {
|
|
12
|
+
/**
|
|
13
|
+
* An annotation page containing the list of records to render.
|
|
14
|
+
*/
|
|
12
15
|
data: AnnotationPage<Item>,
|
|
13
|
-
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A message to display when the list is empty.
|
|
19
|
+
*/
|
|
20
|
+
emptyMessage: string,
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Render function used to determine how to present the passed item.
|
|
24
|
+
*/
|
|
14
25
|
renderItem: (item: Item) => JSX.Element
|
|
15
26
|
};
|
|
16
27
|
|
|
28
|
+
/**
|
|
29
|
+
* This component is a helper component used to structure the lists for the other `Related*` comnponents.
|
|
30
|
+
*/
|
|
17
31
|
const RelatedList = (props: Props) => {
|
|
18
32
|
const { items } = props.data;
|
|
19
33
|
|
|
@@ -8,14 +8,28 @@ import type { MediaContent } from '../types/MediaContent';
|
|
|
8
8
|
import MediaGallery from './MediaGallery';
|
|
9
9
|
|
|
10
10
|
type Props = {
|
|
11
|
+
/**
|
|
12
|
+
* The annotation page containing the media content to render.
|
|
13
|
+
*/
|
|
11
14
|
data: AnnotationPage<MediaContent>,
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Media thumbnail height.
|
|
18
|
+
*/
|
|
12
19
|
thumbnailHeight?: number,
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Media thumbnail width.
|
|
23
|
+
*/
|
|
13
24
|
thumbnailWidth?: number
|
|
14
25
|
};
|
|
15
26
|
|
|
16
27
|
const DEFAULT_THUMBNAIL_HEIGHT = 80;
|
|
17
28
|
const DEFAULT_THUMBNAIL_WIDTH = 80;
|
|
18
29
|
|
|
30
|
+
/**
|
|
31
|
+
* This component renders the related Core Data media records as well as a gallery viewer.
|
|
32
|
+
*/
|
|
19
33
|
const RelatedMedia = (props: Props) => {
|
|
20
34
|
const [showGallery, setShowGallery] = useState<MediaContent>();
|
|
21
35
|
|