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

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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +0 -0
  3. package/build/index.js +2 -0
  4. package/build/index.js.map +1 -0
  5. package/build/main.css +133 -0
  6. package/index.js +1 -0
  7. package/package.json +29 -0
  8. package/src/components/LoadAnimation.css +40 -0
  9. package/src/components/LoadAnimation.js +24 -0
  10. package/src/components/MediaGallery.css +64 -0
  11. package/src/components/MediaGallery.js +80 -0
  12. package/src/components/PlaceDetailsPanel.css +37 -0
  13. package/src/components/PlaceDetailsPanel.js +113 -0
  14. package/src/components/PlaceMarker.js +59 -0
  15. package/src/components/RelatedItemsList.js +94 -0
  16. package/src/components/RelatedList.js +46 -0
  17. package/src/components/RelatedMedia.js +52 -0
  18. package/src/components/RelatedOrganizations.js +28 -0
  19. package/src/components/RelatedPeople.js +28 -0
  20. package/src/components/RelatedPlaces.js +28 -0
  21. package/src/components/RelatedTaxonomies.js +28 -0
  22. package/src/index.js +14 -0
  23. package/src/types/Annotation.js +13 -0
  24. package/src/types/MediaContent.js +13 -0
  25. package/src/types/Organization.js +8 -0
  26. package/src/types/Person.js +11 -0
  27. package/src/types/Place.js +9 -0
  28. package/src/types/RelatedItems.js +12 -0
  29. package/src/types/Taxonomy.js +7 -0
  30. package/src/types/UserDefinedField.js +6 -0
  31. package/types/components/LoadAnimation.js.flow +24 -0
  32. package/types/components/MediaGallery.js.flow +80 -0
  33. package/types/components/PlaceDetailsPanel.js.flow +113 -0
  34. package/types/components/PlaceMarker.js.flow +59 -0
  35. package/types/components/RelatedItemsList.js.flow +94 -0
  36. package/types/components/RelatedList.js.flow +46 -0
  37. package/types/components/RelatedMedia.js.flow +52 -0
  38. package/types/components/RelatedOrganizations.js.flow +28 -0
  39. package/types/components/RelatedPeople.js.flow +28 -0
  40. package/types/components/RelatedPlaces.js.flow +28 -0
  41. package/types/components/RelatedTaxonomies.js.flow +28 -0
  42. package/types/index.js.flow +14 -0
  43. package/types/types/Annotation.js.flow +13 -0
  44. package/types/types/MediaContent.js.flow +13 -0
  45. package/types/types/Organization.js.flow +8 -0
  46. package/types/types/Person.js.flow +11 -0
  47. package/types/types/Place.js.flow +9 -0
  48. package/types/types/RelatedItems.js.flow +12 -0
  49. package/types/types/Taxonomy.js.flow +7 -0
  50. package/types/types/UserDefinedField.js.flow +6 -0
  51. package/webpack.config.js +3 -0
@@ -0,0 +1,59 @@
1
+ // @flow
2
+
3
+ import { LocationMarker } from '@performant-software/geospatial';
4
+ import React, { useCallback, useEffect, useState } from 'react';
5
+
6
+ type Props = {
7
+ /**
8
+ * The URL of the Core Data place record.
9
+ */
10
+ url: string
11
+ };
12
+
13
+ /**
14
+ * This component renders a map marker for a given Core Data Place record.
15
+ */
16
+ const PlaceMarker = (props: Props) => {
17
+ const [place, setPlace] = useState();
18
+
19
+ /**
20
+ * Converts the passed data to a feature collection and sets it on the state.
21
+ *
22
+ * @type {(function(*): void)|*}
23
+ */
24
+ const onLoad = useCallback((data) => {
25
+ const featureCollection = {
26
+ type: 'FeatureCollection',
27
+ features: [{
28
+ ...data,
29
+ properties: {
30
+ ...data.properties,
31
+ record_id: data.record_id
32
+ }
33
+ }]
34
+ };
35
+
36
+ setPlace(featureCollection);
37
+ }, []);
38
+
39
+ /**
40
+ * Fetch the place record from the passed URL.
41
+ */
42
+ useEffect(() => {
43
+ fetch(props.url)
44
+ .then((res) => res.json())
45
+ .then(onLoad);
46
+ }, [props.url]);
47
+
48
+ if (!place) {
49
+ return null;
50
+ }
51
+
52
+ return (
53
+ <LocationMarker
54
+ data={place}
55
+ />
56
+ );
57
+ };
58
+
59
+ export default PlaceMarker;
@@ -0,0 +1,94 @@
1
+ // @flow
2
+
3
+ import * as Accordion from '@radix-ui/react-accordion';
4
+ import { AlertCircle, ChevronDown } from 'lucide-react';
5
+ import React from 'react';
6
+ import _ from 'underscore';
7
+ import LoadAnimation from './LoadAnimation';
8
+ import RelatedMedia from './RelatedMedia';
9
+ import RelatedOrganizations from './RelatedOrganizations';
10
+ import RelatedPeople from './RelatedPeople';
11
+ import RelatedPlaces from './RelatedPlaces';
12
+ import RelatedTaxonomies from './RelatedTaxonomies';
13
+ import type { RelatedItems } from '../types/RelatedItems';
14
+
15
+ type Props = {
16
+ items: Array<RelatedItems>
17
+ };
18
+
19
+ const RelatedItemsList = (props: Props) => {
20
+ if (_.isEmpty(props.items)) {
21
+ return null;
22
+ }
23
+
24
+ return (
25
+ <Accordion.Root type='multiple'>
26
+ { _.map(props.items, ({ data, error, ...conf }) => (
27
+ <Accordion.Item
28
+ key={conf.endpoint}
29
+ value={conf.endpoint}
30
+ >
31
+ <Accordion.Header>
32
+ <Accordion.Trigger
33
+ className='accordion-trigger border-black/20 border border-t border-b-0 border-l-0 border-r-0 border-solid
34
+ rounded-none w-full flex justify-between items-center px-3 py-3 text-sm'
35
+ >
36
+ <div>
37
+ { conf.ui_label }
38
+ { data && (
39
+ <span className='ml-1'>
40
+ ({ data.items.length })
41
+ </span>
42
+ )}
43
+ { error && (
44
+ <AlertCircle
45
+ className='inline ml-1.5 h-4 w-4 text-red-600 align-text-bottom'
46
+ />
47
+ )}
48
+ { !(data || error) && (
49
+ <LoadAnimation
50
+ className='text-muted/60 ml-4'
51
+ />
52
+ )}
53
+ </div>
54
+ <ChevronDown
55
+ className='accordion-chevron h-4 w-4'
56
+ />
57
+ </Accordion.Trigger>
58
+ </Accordion.Header>
59
+ <Accordion.Content
60
+ className='accordion-content text-sm leading-6'
61
+ >
62
+ { conf.endpoint === 'media_contents' && (
63
+ <RelatedMedia
64
+ data={data}
65
+ />
66
+ )}
67
+ { conf.endpoint === 'organizations' && (
68
+ <RelatedOrganizations
69
+ data={data}
70
+ />
71
+ )}
72
+ { conf.endpoint === 'people' && (
73
+ <RelatedPeople
74
+ data={data}
75
+ />
76
+ )}
77
+ { conf.endpoint === 'places' && (
78
+ <RelatedPlaces
79
+ data={data}
80
+ />
81
+ )}
82
+ { conf.endpoint === 'taxonomies' && (
83
+ <RelatedTaxonomies
84
+ data={data}
85
+ />
86
+ )}
87
+ </Accordion.Content>
88
+ </Accordion.Item>
89
+ ))}
90
+ </Accordion.Root>
91
+ );
92
+ };
93
+
94
+ export default RelatedItemsList;
@@ -0,0 +1,46 @@
1
+ // @flow
2
+
3
+ import { AnnotationPage } from '@peripleo/peripleo';
4
+ import React from 'react';
5
+ import _ from 'underscore';
6
+
7
+ type Item = {
8
+ id: string
9
+ };
10
+
11
+ type Props = {
12
+ data: AnnotationPage<Item>,
13
+ emptyMessage?: string,
14
+ renderItem: (item: Item) => JSX.Element
15
+ };
16
+
17
+ const RelatedList = (props: Props) => {
18
+ const { items } = props.data;
19
+
20
+ if (_.isEmpty(items)) {
21
+ return (
22
+ <div
23
+ className='pt-6 pl-3 pr-6 pb-8 flex items-center justify-center text-muted/50 italic'
24
+ >
25
+ { props.emptyMessage }
26
+ </div>
27
+ );
28
+ }
29
+
30
+ return (
31
+ <ul
32
+ className='p-3 pt-1 pb-4'
33
+ >
34
+ { _.map(items, (item) => (
35
+ <li
36
+ key={item.id}
37
+ className='flex items-center'
38
+ >
39
+ { props.renderItem(item) }
40
+ </li>
41
+ ))}
42
+ </ul>
43
+ );
44
+ };
45
+
46
+ export default RelatedList;
@@ -0,0 +1,52 @@
1
+ // @flow
2
+
3
+ import { AnnotationPage } from '@peripleo/peripleo';
4
+ import { Thumbnail } from '@samvera/clover-iiif/primitives';
5
+ import React, { useState } from 'react';
6
+ import _ from 'underscore';
7
+ import type { MediaContent } from '../types/MediaContent';
8
+ import MediaGallery from './MediaGallery';
9
+
10
+ type Props = {
11
+ data: AnnotationPage<MediaContent>,
12
+ thumbnailHeight?: number,
13
+ thumbnailWidth?: number
14
+ };
15
+
16
+ const DEFAULT_THUMBNAIL_HEIGHT = 80;
17
+ const DEFAULT_THUMBNAIL_WIDTH = 80;
18
+
19
+ const RelatedMedia = (props: Props) => {
20
+ const [showGallery, setShowGallery] = useState<MediaContent>();
21
+
22
+ return (
23
+ <div
24
+ className='p-3 pb-4 grid grid-cols-3 gap-1'
25
+ >
26
+ { _.map(props.data?.items, (item) => (
27
+ <Thumbnail
28
+ key={item.body.id}
29
+ className='rounded shadow cursor-pointer'
30
+ onClick={() => setShowGallery(item.body)}
31
+ thumbnail={[{
32
+ id: item.body.content_thumbnail_url,
33
+ type: 'Image',
34
+ width: props.thumbnailWidth,
35
+ height: props.thumbnailHeight
36
+ }]}
37
+ />
38
+ ))}
39
+ <MediaGallery
40
+ defaultItem={showGallery}
41
+ onClose={() => setShowGallery(undefined)}
42
+ />
43
+ </div>
44
+ );
45
+ };
46
+
47
+ RelatedMedia.defaultProps = {
48
+ thumbnailHeight: DEFAULT_THUMBNAIL_HEIGHT,
49
+ thumbnailWidth: DEFAULT_THUMBNAIL_WIDTH
50
+ };
51
+
52
+ export default RelatedMedia;
@@ -0,0 +1,28 @@
1
+ // @flow
2
+
3
+ import { AnnotationPage } from '@peripleo/peripleo';
4
+ import { Building2 } from 'lucide-react';
5
+ import React from 'react';
6
+ import type { Organization } from '../types/Organization';
7
+ import RelatedList from './RelatedList';
8
+
9
+ type Props = {
10
+ data: AnnotationPage<Organization>
11
+ };
12
+
13
+ const RelatedOrganizations = (props: Props) => (
14
+ <RelatedList
15
+ data={props.data}
16
+ emptyMessage={'No related organization'}
17
+ renderItem={(organization) => (
18
+ <>
19
+ <Building2
20
+ className='h-4 w-4 mr-1.5'
21
+ />
22
+ { organization.body.title }
23
+ </>
24
+ )}
25
+ />
26
+ );
27
+
28
+ export default RelatedOrganizations;
@@ -0,0 +1,28 @@
1
+ // @flow
2
+
3
+ import { AnnotationPage } from '@peripleo/peripleo';
4
+ import { UserCircle } from 'lucide-react';
5
+ import React from 'react';
6
+ import type { Person } from '../types/Person';
7
+ import RelatedList from './RelatedList';
8
+
9
+ type Props = {
10
+ data: AnnotationPage<Person>
11
+ };
12
+
13
+ const RelatedPeople = (props: Props) => (
14
+ <RelatedList
15
+ data={props.data}
16
+ emptyMessage={'No related people'}
17
+ renderItem={(person) => (
18
+ <>
19
+ <UserCircle
20
+ className='h-4 w-4 mr-1.5'
21
+ />
22
+ { person.body.title }
23
+ </>
24
+ )}
25
+ />
26
+ );
27
+
28
+ export default RelatedPeople;
@@ -0,0 +1,28 @@
1
+ // @flow
2
+
3
+ import { AnnotationPage } from '@peripleo/peripleo';
4
+ import { MapPin } from 'lucide-react';
5
+ import React from 'react';
6
+ import type { Place } from '../types/Place';
7
+ import RelatedList from './RelatedList';
8
+
9
+ type Props = {
10
+ data: AnnotationPage<Place>
11
+ };
12
+
13
+ const RelatedPlaces = (props: Props) => (
14
+ <RelatedList
15
+ data={props.data}
16
+ emptyMessage={'No related places'}
17
+ renderItem={(place) => (
18
+ <>
19
+ <MapPin
20
+ className='h-4 w-4 mr-0.5 inline-block mb-0.5'
21
+ />
22
+ { place.body.title }
23
+ </>
24
+ )}
25
+ />
26
+ );
27
+
28
+ export default RelatedPlaces;
@@ -0,0 +1,28 @@
1
+ // @flow
2
+
3
+ import { AnnotationPage } from '@peripleo/peripleo';
4
+ import { ListTree } from 'lucide-react';
5
+ import React from 'react';
6
+ import RelatedList from './RelatedList';
7
+ import type { Taxonomy } from '../types/Taxonomy';
8
+
9
+ type Props = {
10
+ data: AnnotationPage<Taxonomy>
11
+ };
12
+
13
+ const RelatedTaxonomies = (props: Props) => (
14
+ <RelatedList
15
+ data={props.data}
16
+ emptyMessage={'No related taxonomies'}
17
+ renderItem={(taxonomy) => (
18
+ <>
19
+ <ListTree
20
+ className='h-4 w-4 mr-1.5'
21
+ />
22
+ { taxonomy.body.title }
23
+ </>
24
+ )}
25
+ />
26
+ );
27
+
28
+ export default RelatedTaxonomies;
package/src/index.js ADDED
@@ -0,0 +1,14 @@
1
+ // @flow
2
+
3
+ // Components
4
+ export { default as LoadAnimation } from './components/LoadAnimation';
5
+ export { default as MediaGallery } from './components/MediaGallery';
6
+ export { default as PlaceDetailsPanel } from './components/PlaceDetailsPanel';
7
+ export { default as PlaceMarker } from './components/PlaceMarker';
8
+ export { default as RelatedItemsList } from './components/RelatedItemsList';
9
+ export { default as RelatedList } from './components/RelatedList';
10
+ export { default as RelatedMedia } from './components/RelatedMedia';
11
+ export { default as RelatedOrganization } from './components/RelatedOrganizations';
12
+ export { default as RelatedPeople } from './components/RelatedPeople';
13
+ export { default as RelatedPlaces } from './components/RelatedPlaces';
14
+ export { default as RelatedTaxonomies } from './components/RelatedTaxonomies';
@@ -0,0 +1,13 @@
1
+ // @flow
2
+
3
+ import type { UserDefinedField } from './UserDefinedField';
4
+
5
+ export type Annotation = {
6
+ id: string,
7
+ record_id: string,
8
+ uuid: string,
9
+ title: string,
10
+ user_defined: {
11
+ [key: string]: UserDefinedField
12
+ }
13
+ };
@@ -0,0 +1,13 @@
1
+ // @flow
2
+
3
+ import type { Annotation } from './Annotation';
4
+
5
+ export type MediaContent = Annotation & {
6
+ content_download_url: string,
7
+ content_iiif_url: string,
8
+ content_preview_url: string,
9
+ content_thumbnail_url: string,
10
+ content_url: string,
11
+ manifest_url: string,
12
+ type: 'MediaContent'
13
+ };
@@ -0,0 +1,8 @@
1
+ // @flow
2
+
3
+ import type { Annotation } from './Annotation';
4
+
5
+ export type Organization = Annotation & {
6
+ type: 'Organization',
7
+ biography: string
8
+ };
@@ -0,0 +1,11 @@
1
+ // @flow
2
+
3
+ import type { Annotation } from './Annotation';
4
+
5
+ export type Person = Annotation & {
6
+ id: string,
7
+ type: 'Person',
8
+ record_id: string,
9
+ title: string,
10
+ biography: string
11
+ };
@@ -0,0 +1,9 @@
1
+ // @flow
2
+
3
+ import type { FeatureGeometry } from '@peripleo/peripleo';
4
+ import type { Annotation } from './Annotation';
5
+
6
+ export type Place = Annotation & {
7
+ type: 'Place',
8
+ geometry: FeatureGeometry
9
+ };
@@ -0,0 +1,12 @@
1
+ // @flow
2
+
3
+ import { AnnotationPage } from '@peripleo/peripleo';
4
+ import type { Annotation } from './Annotation';
5
+
6
+ export type RelatedItems = {
7
+ endpoint: string,
8
+ ui_label: string,
9
+ default_open?: boolean,
10
+ data?: AnnotationPage<Annotation>,
11
+ error?: Error
12
+ };
@@ -0,0 +1,7 @@
1
+ // @flow
2
+
3
+ import type { Annotation } from './Annotation';
4
+
5
+ export type Taxonomy = Annotation & {
6
+ type: 'Taxonomy'
7
+ };
@@ -0,0 +1,6 @@
1
+ // @flow
2
+
3
+ export type UserDefinedField = {
4
+ label: string,
5
+ value: string
6
+ };
@@ -0,0 +1,24 @@
1
+ // @flow
2
+
3
+ import React from 'react';
4
+ import './LoadAnimation.css';
5
+
6
+ type Props = {
7
+ /**
8
+ * Additional class name to apply to the element.
9
+ */
10
+ className?: string
11
+ };
12
+
13
+ /**
14
+ * This component renders a basic loading animation.
15
+ */
16
+ const LoadAnimation = (props: Props) => {
17
+ const className = `${props.className || ''} loader three-dots`.trim();
18
+
19
+ return (
20
+ <span className={className} />
21
+ );
22
+ };
23
+
24
+ export default LoadAnimation;
@@ -0,0 +1,80 @@
1
+ // @flow
2
+
3
+ import * as Dialog from '@radix-ui/react-dialog';
4
+ import Viewer from '@samvera/clover-iiif/viewer';
5
+ import { Image, X } from 'lucide-react';
6
+ import React from 'react';
7
+ import type { MediaContent } from '../types/MediaContent';
8
+
9
+ import './MediaGallery.css';
10
+
11
+ type Props = {
12
+ /**
13
+ * The MediaContent record contain the IIIF manifest URL.
14
+ */
15
+ defaultItem: MediaContent,
16
+
17
+ /**
18
+ * Callback fired when the dialog is closed.
19
+ */
20
+ onClose: () => void,
21
+
22
+ /**
23
+ * Title text to display at the top of the dialog.
24
+ */
25
+ title?: string
26
+ };
27
+
28
+ /**
29
+ * This component renders a IIIF Viewer for the passed MediaContent record.
30
+ */
31
+ const MediaGallery = (props: Props) => (
32
+ <Dialog.Root
33
+ onOpenChange={props.onClose}
34
+ open={Boolean(props.defaultItem)}
35
+ >
36
+ <Dialog.Portal>
37
+ <Dialog.Overlay
38
+ className='dialog-overlay'
39
+ />
40
+ <Dialog.Content
41
+ className='dialog-content'
42
+ >
43
+ <Dialog.Title
44
+ className='dialog-title flex items-center'
45
+ >
46
+ <Image
47
+ className='h-4 w-4 mr-1.5'
48
+ />
49
+ { props.title }
50
+ </Dialog.Title>
51
+ <div
52
+ className='pt-6 pb-2 text-sm w-full text-muted min-h-20'
53
+ >
54
+ { Boolean(props.defaultItem) && (
55
+ <Viewer
56
+ iiifContent={props.defaultItem.manifest_url}
57
+ options={{
58
+ informationPanel: {
59
+ open: false
60
+ }
61
+ }}
62
+ />
63
+ )}
64
+ </div>
65
+ <Dialog.Close
66
+ asChild
67
+ >
68
+ <button
69
+ className='dialog-close rounded-full'
70
+ type='button'
71
+ >
72
+ <X className='h-7 w-7 p-1.5' />
73
+ </button>
74
+ </Dialog.Close>
75
+ </Dialog.Content>
76
+ </Dialog.Portal>
77
+ </Dialog.Root>
78
+ );
79
+
80
+ export default MediaGallery;