@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,113 @@
1
+ // @flow
2
+
3
+ import { Image, X } from 'lucide-react';
4
+ import React, { useMemo, useRef } from 'react';
5
+ import _ from 'underscore';
6
+ import type { Place } from '../types/Place';
7
+ import type { RelatedItems } from '../types/RelatedItems';
8
+ import RelatedItemsList from './RelatedItemsList';
9
+ import './PlaceDetailsPanel.css';
10
+
11
+ type Props = {
12
+ place?: Place,
13
+ related: Array<RelatedItems>,
14
+ onClose: () => void
15
+ };
16
+
17
+ const PlaceDetailsPanel = (props: Props) => {
18
+ const el = useRef<HTMLElement>(null);
19
+
20
+ /**
21
+ * Returns the first image for the passed related items.
22
+ */
23
+ const firstImage = useMemo(() => {
24
+ const images = props.related.find((i) => i.endpoint === 'media_contents' && !_.isEmpty(i.data?.items));
25
+ return images ? (images.data && images.data.items[0].body) : undefined;
26
+ }, [props.related]);
27
+
28
+ /**
29
+ * Sets the user defined field values.
30
+ *
31
+ * @type {UserDefinedField[]|*[]}
32
+ */
33
+ const userDefined = useMemo(() => (
34
+ props.place?.user_defined ? Object.values(props.place.user_defined) : []
35
+ ), [props.place]);
36
+
37
+ return (
38
+ <aside
39
+ className='flex flex-col absolute z-10 h-full w-[280px] bg-white/80 backdrop-blur shadow overflow-y-auto'
40
+ ref={el}
41
+ >
42
+ <button
43
+ className='absolute top-2 right-2 p-1.5 rounded-full z-10 bg-slate-200/60 hover:bg-slate-200 focus:outline-2 focus:outline-offset-2 focus:outline-teal-700'
44
+ onClick={props.onClose}
45
+ type='button'
46
+ >
47
+ <X className='h-4 w-4' />
48
+ </button>
49
+ { props.place && (
50
+ <>
51
+ { firstImage && (
52
+ <div
53
+ className='relative w-full h-[200px] flex-grow-0 flex-shrink-0 bg-muted/20 z-0'
54
+ >
55
+ <div
56
+ className='absolute top-0 left-0 w-full h-full flex justify-center items-center'
57
+ >
58
+ <Image
59
+ className='h-20 w-20 text-gray-400'
60
+ strokeWidth={1}
61
+ />
62
+ </div>
63
+ <div
64
+ className='absolute top-0 left-0 w-full h-full flex justify-center items-center'
65
+ >
66
+ <img
67
+ className='object-cover h-full w-full'
68
+ src={firstImage.content_url}
69
+ alt={firstImage.title}
70
+ />
71
+ </div>
72
+ </div>
73
+ )}
74
+ <div
75
+ className='p-3'
76
+ >
77
+ <h1
78
+ className='pr-6 font-medium'
79
+ >
80
+ { props.place.properties.title }
81
+ </h1>
82
+ <ol
83
+ className='text-sm mt-4 leading-6 overflow-hidden'
84
+ >
85
+ { _.map(userDefined, ({ label, value }) => (
86
+ <li
87
+ key={label}
88
+ className='mb-2'
89
+ >
90
+ <div
91
+ className='text-muted'
92
+ >
93
+ { label }
94
+ </div>
95
+ <div
96
+ className='font-medium overflow-hidden text-ellipsis'
97
+ >
98
+ { value }
99
+ </div>
100
+ </li>
101
+ ))}
102
+ </ol>
103
+ </div>
104
+ <RelatedItemsList
105
+ items={props.related}
106
+ />
107
+ </>
108
+ )}
109
+ </aside>
110
+ );
111
+ };
112
+
113
+ export default PlaceDetailsPanel;
@@ -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;
@@ -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,3 @@
1
+ const { configure } = require('@performant-software/webpack-config');
2
+
3
+ module.exports = configure(__dirname);