@performant-software/core-data 1.2.0-beta.1 → 1.2.0-beta.11

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 (48) hide show
  1. package/build/index.js +1 -1
  2. package/build/index.js.map +1 -1
  3. package/build/main.css +17 -5
  4. package/package.json +15 -4
  5. package/postcss.config.js +6 -0
  6. package/src/components/MediaGallery.js +1 -1
  7. package/src/components/PlaceDetailsPanel.js +14 -1
  8. package/src/components/PlaceResultsList.css +7 -0
  9. package/src/components/PlaceResultsList.js +178 -0
  10. package/src/components/{PlaceDetailsPanel.css → RelatedItemsList.css} +5 -5
  11. package/src/components/RelatedItemsList.js +14 -3
  12. package/src/components/RelatedList.js +16 -2
  13. package/src/components/RelatedMedia.js +15 -1
  14. package/src/components/RelatedOrganizations.js +9 -2
  15. package/src/components/RelatedPeople.js +9 -2
  16. package/src/components/RelatedPlaces.js +9 -2
  17. package/src/components/RelatedTaxonomies.js +9 -2
  18. package/src/i18n/en.json +27 -0
  19. package/src/i18n/i18n.js +26 -0
  20. package/src/index.css +3 -0
  21. package/src/index.js +4 -0
  22. package/src/types/AnnotationPage.js +15 -0
  23. package/src/types/Feature.js +10 -0
  24. package/src/types/FeatureGeometry.js +6 -0
  25. package/src/types/Place.js +1 -1
  26. package/src/types/RelatedItems.js +1 -1
  27. package/src/types/typesense/Place.js +14 -0
  28. package/tailwind.config.js +19 -0
  29. package/types/components/MediaGallery.js.flow +1 -1
  30. package/types/components/PlaceDetailsPanel.js.flow +14 -1
  31. package/types/components/PlaceResultsList.js.flow +178 -0
  32. package/types/components/RelatedItemsList.js.flow +14 -3
  33. package/types/components/RelatedList.js.flow +16 -2
  34. package/types/components/RelatedMedia.js.flow +15 -1
  35. package/types/components/RelatedOrganizations.js.flow +9 -2
  36. package/types/components/RelatedPeople.js.flow +9 -2
  37. package/types/components/RelatedPlaces.js.flow +9 -2
  38. package/types/components/RelatedTaxonomies.js.flow +9 -2
  39. package/types/components/SearchResultsList.js.flow +160 -0
  40. package/types/i18n/i18n.js.flow +26 -0
  41. package/types/index.js.flow +4 -0
  42. package/types/types/AnnotationPage.js.flow +15 -0
  43. package/types/types/Feature.js.flow +10 -0
  44. package/types/types/FeatureGeometry.js.flow +6 -0
  45. package/types/types/Place.js.flow +1 -1
  46. package/types/types/RelatedItems.js.flow +1 -1
  47. package/types/types/typesense/Place.js.flow +14 -0
  48. package/webpack.config.js +31 -1
@@ -1,19 +1,33 @@
1
1
  // @flow
2
2
 
3
- import { AnnotationPage } from '@peripleo/peripleo';
4
3
  import React from 'react';
5
4
  import _ from 'underscore';
5
+ import type { AnnotationPage } from '../types/AnnotationPage';
6
6
 
7
7
  type Item = {
8
8
  id: string
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
- emptyMessage?: string,
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
 
@@ -1,21 +1,35 @@
1
1
  // @flow
2
2
 
3
- import { AnnotationPage } from '@peripleo/peripleo';
4
3
  import { Thumbnail } from '@samvera/clover-iiif/primitives';
5
4
  import React, { useState } from 'react';
6
5
  import _ from 'underscore';
6
+ import type { AnnotationPage } from '../types/AnnotationPage';
7
7
  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
 
@@ -1,19 +1,26 @@
1
1
  // @flow
2
2
 
3
- import { AnnotationPage } from '@peripleo/peripleo';
4
3
  import { Building2 } from 'lucide-react';
5
4
  import React from 'react';
5
+ import i18n from '../i18n/i18n';
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={'No related organization'}
23
+ emptyMessage={i18n.t('RelatedOrganizations.labels.empty')}
17
24
  renderItem={(organization) => (
18
25
  <>
19
26
  <Building2
@@ -1,19 +1,26 @@
1
1
  // @flow
2
2
 
3
- import { AnnotationPage } from '@peripleo/peripleo';
4
3
  import { UserCircle } from 'lucide-react';
5
4
  import React from 'react';
5
+ import i18n from '../i18n/i18n';
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={'No related people'}
23
+ emptyMessage={i18n.t('RelatedPeople.labels.empty')}
17
24
  renderItem={(person) => (
18
25
  <>
19
26
  <UserCircle
@@ -1,19 +1,26 @@
1
1
  // @flow
2
2
 
3
- import { AnnotationPage } from '@peripleo/peripleo';
4
3
  import { MapPin } from 'lucide-react';
5
4
  import React from 'react';
5
+ import i18n from '../i18n/i18n';
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={'No related places'}
23
+ emptyMessage={i18n.t('RelatedPlaces.labels.empty')}
17
24
  renderItem={(place) => (
18
25
  <>
19
26
  <MapPin
@@ -1,19 +1,26 @@
1
1
  // @flow
2
2
 
3
- import { AnnotationPage } from '@peripleo/peripleo';
4
3
  import { ListTree } from 'lucide-react';
5
4
  import React from 'react';
5
+ import i18n from '../i18n/i18n';
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={'No related taxonomies'}
23
+ emptyMessage={i18n.t('RelatedTaxonomies.labels.empty')}
17
24
  renderItem={(taxonomy) => (
18
25
  <>
19
26
  <ListTree
@@ -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
+ }
@@ -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
@@ -0,0 +1,3 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
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,15 @@
1
+ // @flow
2
+
3
+ import type { Annotation } from './Annotation';
4
+
5
+ export type AnnotationPage = {
6
+ '@context': 'http://www.w3.org/ns/anno.jsonld';
7
+ id: string;
8
+ type: 'AnnotationPage';
9
+ partOf: {
10
+ id: string;
11
+ label: string;
12
+ total: number;
13
+ };
14
+ items: Annotation<T>[];
15
+ };
@@ -0,0 +1,10 @@
1
+ // @flow
2
+
3
+ import type { FeatureGeometry } from './FeatureGeometry';
4
+
5
+ export type Feature = {
6
+ id: number,
7
+ type: 'Feature',
8
+ properties: any,
9
+ geometry: FeatureGeometry
10
+ };
@@ -0,0 +1,6 @@
1
+ // @flow
2
+
3
+ export type FeatureGeometry = {
4
+ type: 'Point' | 'Polygon' | 'Polyline' | 'GeometryCollection';
5
+ coordinates: number[] | number[][] | number[][][];
6
+ };
@@ -1,7 +1,7 @@
1
1
  // @flow
2
2
 
3
- import type { FeatureGeometry } from '@peripleo/peripleo';
4
3
  import type { Annotation } from './Annotation';
4
+ import type { FeatureGeometry } from './FeatureGeometry';
5
5
 
6
6
  export type Place = Annotation & {
7
7
  type: 'Place',
@@ -1,7 +1,7 @@
1
1
  // @flow
2
2
 
3
- import { AnnotationPage } from '@peripleo/peripleo';
4
3
  import type { Annotation } from './Annotation';
4
+ import type { AnnotationPage } from './AnnotationPage';
5
5
 
6
6
  export type RelatedItems = {
7
7
  endpoint: string,
@@ -0,0 +1,14 @@
1
+ // @flow
2
+
3
+ export type Place = {
4
+ uuid: string;
5
+ record_id: string;
6
+ type: string;
7
+ name: string;
8
+ names: string[];
9
+ coordinates: number[];
10
+ geometry: {
11
+ type: 'Point' | 'GeometryCollection',
12
+ coordinates: [ number, number ];
13
+ }
14
+ };
@@ -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
 
@@ -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 type { RelatedItems } from '../types/RelatedItems';
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 type='multiple'>
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.length })
51
+ { i18n.t('RelatedItemsList.labels.count', { count: data.items?.length })}
41
52
  </span>
42
53
  )}
43
54
  { error && (
@@ -1,19 +1,33 @@
1
1
  // @flow
2
2
 
3
- import { AnnotationPage } from '@peripleo/peripleo';
4
3
  import React from 'react';
5
4
  import _ from 'underscore';
5
+ import type { AnnotationPage } from '../types/AnnotationPage';
6
6
 
7
7
  type Item = {
8
8
  id: string
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
- emptyMessage?: string,
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