Skip to content

Commit 403d9f5

Browse files
committed
WIP PDF reports
1 parent 607bcd0 commit 403d9f5

19 files changed

Lines changed: 375 additions & 17 deletions

‎fixtures/observations.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"today": "2014-03-11",
3131
"village": "Parikowaro Naawa",
3232
"start": "2014-03-11T00:00:00.000Z",
33-
"notes": "Laboris qui tempor dolore exercitation adipisicing. In officia eu irure occaecat deserunt mollit do nulla. Id dolor deserunt laborum est.",
33+
"notes": "Laboris qui tempor dolore exercitation adipisicing. In officia eu irure occaecat deserunt mollit do nulla. Id dolor deserunt laborum est.\nLaboris qui tempor dolore exercitation adipisicing.",
3434
"categoryId": "fishing"
3535
}
3636
},

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"build": "npm run build:translations & npm run build:esm & npm run build:cjs",
1717
"extract-messages": "node scripts/extract-messages.js",
1818
"prepublish": "npm run build",
19-
"storybook": "BABEL_ENV=cjs start-storybook -p 6006 --ci",
19+
"storybook": "BABEL_ENV=cjs start-storybook -p 6006 --ci --static-dir static",
2020
"build-storybook": "build-storybook",
2121
"test": "jest"
2222
},

‎src/ReportView/DetailsTable.js‎

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import FormattedFieldname from '../internal/FormattedFieldname'
1313
import { get } from '../utils/get_set'
1414

1515
import type { Field, JSONObject, Primitive } from '../types'
16+
import { isEmptyValue } from '../utils/helpers'
1617

1718
const styles = {
1819
root: {
@@ -232,12 +233,4 @@ const DetailsTable = ({ fields = [], tags = {}, width }: Props) => {
232233
)
233234
}
234235

235-
function isEmptyValue(value) {
236-
return (
237-
(typeof value === 'string' && value.length === 0) ||
238-
value === undefined ||
239-
value === null
240-
)
241-
}
242-
243236
export default DetailsTable

‎src/ReportViewPDF/ReportView.js‎

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
// @flow
2+
3+
import React from 'react'
4+
import {
5+
Page,
6+
Text,
7+
View,
8+
Document,
9+
StyleSheet,
10+
Font
11+
} from '@react-pdf/renderer'
12+
import type { Observation } from 'mapeo-schema'
13+
import { FormattedTime, IntlProvider } from 'react-intl'
14+
15+
// import ReportFeature from './ReportFeature'
16+
// import ReportPageContent from './ReportPageContent'
17+
import { defaultGetPreset, isEmptyValue } from '../utils/helpers'
18+
import { get } from '../utils/get_set'
19+
20+
import type {
21+
PaperSize,
22+
CameraOptions,
23+
CommonViewContentProps,
24+
PresetWithAdditionalFields,
25+
Primitive
26+
} from '../types'
27+
import FormattedFieldname from '../internal/FormattedFieldname'
28+
import FormattedValue from '../internal/FormattedValue'
29+
import FormattedLocation from '../internal/FormattedLocation'
30+
31+
export type ReportViewPDFProps = {
32+
/** Called with
33+
* [CameraOptions](https://docs.mapbox.com/mapbox-gl-js/api/#cameraoptions)
34+
* with properties `center`, `zoom`, `bearing`, `pitch` */
35+
onMapMove?: CameraOptions => any,
36+
/** Initial position of the map - an object with properties `center`, `zoom`,
37+
* `bearing`, `pitch`. If this is not set then the map will by default zoom to
38+
* the bounds of the observations. If you are going to unmount and re-mount
39+
* this component (e.g. within tabs) then you will want to use onMove to store
40+
* the position in state, and pass it as initialPosition for when the map
41+
* re-mounts. */
42+
initialMapPosition?: $Shape<CameraOptions>,
43+
/** Mapbox access token */
44+
mapboxAccessToken: string,
45+
/** Mapbox style url */
46+
mapStyle?: any
47+
}
48+
49+
type Props = {
50+
...$Exact<ReportViewPDFProps>,
51+
...$Exact<CommonViewContentProps>,
52+
/** Paper size for report */
53+
paperSize?: PaperSize,
54+
/** Render for printing (for screen display only visible observations are
55+
* rendered, for performance reasons) */
56+
print?: boolean
57+
}
58+
59+
Font.register({
60+
family: 'SourceSansPro',
61+
fonts: [
62+
{ src: 'fonts/SourceSansPro-Regular.ttf' }, // font-style: normal, font-weight: normal
63+
{ src: 'fonts/SourceSansPro-Italic.ttf', fontStyle: 'italic' },
64+
{ src: 'fonts/SourceSansPro-Bold.ttf', fontWeight: 700 },
65+
{
66+
src: 'fonts/SourceSansPro-BoldItalic.ttf',
67+
fontStyle: 'italic',
68+
fontWeight: 700
69+
},
70+
{ src: 'fonts/SourceSansPro-Light.ttf', fontWeight: 300 },
71+
{
72+
src: 'fonts/SourceSansPro-LightItalic.ttf',
73+
fontStyle: 'italic',
74+
fontWeight: 300
75+
}
76+
]
77+
})
78+
79+
const FeaturePage = ({
80+
observation,
81+
preset = {}
82+
}: {
83+
observation: Observation,
84+
preset?: PresetWithAdditionalFields
85+
}) => {
86+
const coords =
87+
typeof observation.lon === 'number' && typeof observation.lat === 'number'
88+
? {
89+
longitude: observation.lon,
90+
latitude: observation.lat
91+
}
92+
: undefined
93+
const createdAt =
94+
typeof observation.created_at === 'string'
95+
? new Date(observation.created_at)
96+
: undefined
97+
const fields = preset.fields.concat(preset.additionalFields)
98+
const tags = observation.tags || {}
99+
const note = tags.note || tags.notes
100+
return (
101+
<Page size="A4" style={styles.page}>
102+
<View style={styles.pageContent}>
103+
<View style={styles.columnLeft}>
104+
<Text style={styles.presetName}>{preset.name || 'Observation'}</Text>
105+
{createdAt && (
106+
<Text style={styles.createdAt}>
107+
<Text style={styles.createdAtLabel}>Registrado: </Text>
108+
<FormattedTime
109+
key="time"
110+
value={createdAt}
111+
year="numeric"
112+
month="long"
113+
day="2-digit"
114+
/>
115+
</Text>
116+
)}
117+
{coords && (
118+
<Text style={styles.location}>
119+
<Text style={styles.locationLabel}>Ubicación: </Text>
120+
<FormattedLocation {...coords} />
121+
</Text>
122+
)}
123+
{note &&
124+
note.split('\n').map((para, idx) => (
125+
<Text key={idx} style={styles.description}>
126+
{para}
127+
</Text>
128+
))}
129+
<Text style={styles.details}>Detalles</Text>
130+
{fields.map(field => {
131+
const value: Primitive | Array<Primitive> = get(tags, field.key)
132+
if (isEmptyValue(value)) return null
133+
return (
134+
<View key={field.id} style={styles.field} wrap={false}>
135+
<Text style={styles.fieldLabel}>
136+
<FormattedFieldname field={field} />
137+
</Text>
138+
<Text style={styles.fieldValue}>
139+
<FormattedValue field={field} value={value} />
140+
</Text>
141+
</View>
142+
)
143+
})}
144+
</View>
145+
<View style={styles.columnRight}>
146+
<View style={styles.map} />
147+
{new Array(4).fill(null).map((att, i) => (
148+
<View
149+
key={i}
150+
style={[
151+
styles.image,
152+
{ height: Math.random() > 0.5 ? '70mm' : '50mm' }
153+
]}
154+
wrap={false}
155+
/>
156+
))}
157+
</View>
158+
</View>
159+
</Page>
160+
)
161+
}
162+
163+
export const PdfContext = React.createContext<boolean>(false)
164+
165+
const ReportViewPDF = ({
166+
observations,
167+
getPreset = defaultGetPreset,
168+
getMedia,
169+
paperSize = 'a4',
170+
mapboxAccessToken,
171+
mapStyle
172+
}: Props) => {
173+
return (
174+
<PdfContext.Provider value={true}>
175+
<IntlProvider>
176+
<Document>
177+
{observations.slice(0, 1).map(observation => (
178+
<FeaturePage
179+
key={observation.id}
180+
observation={observation}
181+
preset={getPreset(observation)}
182+
/>
183+
))}
184+
</Document>
185+
</IntlProvider>
186+
</PdfContext.Provider>
187+
)
188+
}
189+
190+
export default ReportViewPDF
191+
192+
const styles = StyleSheet.create({
193+
page: {
194+
backgroundColor: 'white',
195+
padding: '15mm',
196+
flexDirection: 'row'
197+
},
198+
pageContent: {
199+
flex: 1,
200+
flexDirection: 'row',
201+
fontFamily: 'SourceSansPro'
202+
},
203+
columnLeft: {
204+
flex: 2,
205+
paddingRight: 12,
206+
lineHeight: 1.2
207+
},
208+
columnRight: {
209+
// backgroundColor: 'aqua',
210+
flex: 1
211+
},
212+
presetName: {
213+
fontWeight: 700
214+
},
215+
createdAt: {
216+
fontSize: 12
217+
},
218+
createdAtLabel: {
219+
fontSize: 12,
220+
color: 'grey'
221+
},
222+
location: {
223+
fontSize: 12,
224+
marginBottom: 6
225+
},
226+
locationLabel: {
227+
fontSize: 12,
228+
color: 'grey'
229+
},
230+
map: {
231+
height: '60mm',
232+
borderStyle: 'solid',
233+
borderWidth: 1,
234+
borderColor: 'black',
235+
marginBottom: 12,
236+
backgroundColor: '#8E918B'
237+
},
238+
image: {
239+
height: '50mm',
240+
borderStyle: 'solid',
241+
borderWidth: 1,
242+
borderColor: 'black',
243+
marginBottom: 12,
244+
backgroundColor: '#C8D8E3'
245+
},
246+
description: {
247+
marginBottom: 6,
248+
fontSize: 12
249+
},
250+
details: {
251+
fontWeight: 'bold',
252+
fontSize: 14,
253+
marginBottom: 3,
254+
marginTop: 12
255+
},
256+
field: {
257+
marginBottom: 6
258+
},
259+
fieldLabel: {
260+
fontSize: 9,
261+
marginBottom: 1,
262+
color: '#333333'
263+
},
264+
fieldValue: {
265+
fontSize: 12
266+
}
267+
})
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/* eslint-disable react-hooks/rules-of-hooks */
2+
// @flow
3+
import React from 'react'
4+
import { PDFViewer } from '@react-pdf/renderer'
5+
import { withKnobs, boolean } from '@storybook/addon-knobs'
6+
import { action } from '@storybook/addon-actions'
7+
// import { linkTo } from '@storybook/addon-links'
8+
9+
import ReportView from './ReportView'
10+
import { defaultGetPreset } from '../utils/helpers'
11+
import { useIntl } from 'react-intl'
12+
13+
const exampleObservations = require('../../fixtures/observations.json')
14+
15+
const imageBaseUrl =
16+
'https://images.digital-democracy.org/mapfilter-sample/sample-'
17+
18+
const getMedia = ({ id }) => ({
19+
src: imageBaseUrl + ((parseInt(id, 16) % 17) + 1) + '.jpg',
20+
type: 'image'
21+
})
22+
23+
export default {
24+
title: 'ReportView/PDF',
25+
decorators: [
26+
withKnobs,
27+
(storyFn: any) => (
28+
<div style={{ width: '100vw', height: '100vh', display: 'flex' }}>
29+
<PDFViewer width="100%" height="100%">
30+
{storyFn()}
31+
</PDFViewer>
32+
</div>
33+
)
34+
]
35+
}
36+
37+
export const withoutImages = () => (
38+
<ReportView
39+
getPreset={defaultGetPreset}
40+
mapboxAccessToken="pk.eyJ1IjoiZ21hY2xlbm5hbiIsImEiOiJSaWVtd2lRIn0.ASYMZE2HhwkAw4Vt7SavEg"
41+
observations={exampleObservations}
42+
onClick={action('click')}
43+
getMedia={() => {}}
44+
/>
45+
)
46+
47+
export const images = () => (
48+
<ReportView
49+
getPreset={defaultGetPreset}
50+
mapboxAccessToken="pk.eyJ1IjoiZ21hY2xlbm5hbiIsImEiOiJSaWVtd2lRIn0.ASYMZE2HhwkAw4Vt7SavEg"
51+
observations={exampleObservations}
52+
onClick={action('click')}
53+
getMedia={getMedia}
54+
/>
55+
)
56+
57+
export const customFields = () => (
58+
<ReportView
59+
getPreset={defaultGetPreset}
60+
mapboxAccessToken="pk.eyJ1IjoiZ21hY2xlbm5hbiIsImEiOiJSaWVtd2lRIn0.ASYMZE2HhwkAw4Vt7SavEg"
61+
observations={exampleObservations}
62+
onClick={action('click')}
63+
getMedia={getMedia}
64+
getFields={obs => [
65+
{
66+
id: 'myField',
67+
key: 'caption',
68+
label: 'Image caption',
69+
type: 'text',
70+
appearance: 'multiline'
71+
}
72+
]}
73+
/>
74+
)
75+
76+
export const printView = () => {
77+
return (
78+
<ReportView
79+
getPreset={defaultGetPreset}
80+
mapboxAccessToken="pk.eyJ1IjoiZ21hY2xlbm5hbiIsImEiOiJSaWVtd2lRIn0.ASYMZE2HhwkAw4Vt7SavEg"
81+
observations={exampleObservations.slice(0, 50)}
82+
onClick={action('click')}
83+
getMedia={getMedia}
84+
print={boolean('Print', false)}
85+
/>
86+
)
87+
}

0 commit comments

Comments
 (0)