@eeacms/volto-cca-policy 0.2.96 → 0.2.97

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/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ ### [0.2.97](https://github.com/eea/volto-cca-policy/compare/0.2.96...0.2.97) - 27 January 2025
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat(widget): Add image download option for PromotionalImageWidget - refs #283317 [kreafox - [`cc9b1ab`](https://github.com/eea/volto-cca-policy/commit/cc9b1ab4b79fcc92ea3d555cbc9b9f17298a147c)]
12
+
13
+ #### :nail_care: Enhancements
14
+
15
+ - change(view): remove last modified date from DB items, refactoring, cleanup - refs #283449 [kreafox - [`bb3b60a`](https://github.com/eea/volto-cca-policy/commit/bb3b60aae64ac21d22ad391f60a40a9b226ee1fe)]
16
+
17
+ #### :hammer_and_wrench: Others
18
+
19
+ - test: Fix issue reported by sonarqube [kreafox - [`78dca60`](https://github.com/eea/volto-cca-policy/commit/78dca60618339183dc08bb70cb96e2e35d370b5c)]
20
+ - Add PromotionalImageWidget tests - refs #283317 [kreafox - [`8d74e37`](https://github.com/eea/volto-cca-policy/commit/8d74e37bebd468feaa93b2e468e742a18f7deefa)]
7
21
  ### [0.2.96](https://github.com/eea/volto-cca-policy/compare/0.2.95...0.2.96) - 24 January 2025
8
22
 
9
23
  #### :nail_care: Enhancements
@@ -1816,13 +1830,10 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
1816
1830
  - Refs #260715 rast-block wip [Tripon Eugen - [`f19d54e`](https://github.com/eea/volto-cca-policy/commit/f19d54e0b9a6a86bf344eb85b6a1cda7f3de91bf)]
1817
1831
  - Refs #260715 rast-block wip [Tripon Eugen - [`2828537`](https://github.com/eea/volto-cca-policy/commit/2828537b6c084cd1a82162d552fb4ef025b71f9f)]
1818
1832
  - Refs #260715 rast-block updates [Tripon Eugen - [`1e803e5`](https://github.com/eea/volto-cca-policy/commit/1e803e5bd3d3fb7558f261c76c68866be7beb8b5)]
1819
- - test: [JENKINS] Use java17 for sonarqube scanner [valentinab25 - [`0a15e1b`](https://github.com/eea/volto-cca-policy/commit/0a15e1b2ad081233685e80d5b3c60a8663f6b896)]
1820
- - test: [JENKINS] Run cypress in started frontend container [valentinab25 - [`9554e44`](https://github.com/eea/volto-cca-policy/commit/9554e44c92a621a52b2adb5a4830fb084ee5734b)]
1821
1833
  ### [0.1.49](https://github.com/eea/volto-cca-policy/compare/0.1.48...0.1.49) - 15 November 2023
1822
1834
 
1823
1835
  #### :house: Internal changes
1824
1836
 
1825
- - chore: [JENKINS] Refactor automated testing [valentinab25 - [`7b820a6`](https://github.com/eea/volto-cca-policy/commit/7b820a6369c2ddd5203b1a4abe352cb4bb43db7a)]
1826
1837
  - chore: husky, lint-staged use fixed versions [valentinab25 - [`f0a8061`](https://github.com/eea/volto-cca-policy/commit/f0a8061c275c236deb00087c23fac9860a073106)]
1827
1838
 
1828
1839
  #### :hammer_and_wrench: Others
@@ -1839,9 +1850,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
1839
1850
  - Refs #259267 - jenkins test [Tripon Eugen - [`cacd31e`](https://github.com/eea/volto-cca-policy/commit/cacd31e7b1afe0983674ed5c7632d2e1d7fa752e)]
1840
1851
  - Refs #259267 - jenkins [Tripon Eugen - [`5b3affe`](https://github.com/eea/volto-cca-policy/commit/5b3affee8401239de10097884c1b7f2349d15ec0)]
1841
1852
  - Refs #259267 - add When, lead image and title to files [Tripon Eugen - [`2cedb23`](https://github.com/eea/volto-cca-policy/commit/2cedb237f898af9057e13fba94b615ef71077204)]
1842
- - test: [JENKINS] Add cpu limit on cypress docker [valentinab25 - [`4d607a5`](https://github.com/eea/volto-cca-policy/commit/4d607a576e9d0a5c34e48c41b409e7df616ee3d6)]
1843
- - test: [JENKINS] Increase shm-size to cypress docker [valentinab25 - [`b7f74d5`](https://github.com/eea/volto-cca-policy/commit/b7f74d53513a6edbfbca5cb6d19687929bb1e5db)]
1844
- - test: [JENKINS] Improve cypress time [valentinab25 - [`db65617`](https://github.com/eea/volto-cca-policy/commit/db656173391f65157098d95d388c25f6429753d8)]
1845
1853
  - Refs #259267 - cca event blocks attachments and check not mandatoty fields [Tripon Eugen - [`3138e5a`](https://github.com/eea/volto-cca-policy/commit/3138e5afb5bfbdbed14e27ed457b16867b7fa414)]
1846
1854
  - Refs #256681 - Fix error in CCA Event view menu. ([React Intl] An id must be provided to format a message.) [GhitaB - [`517eeb8`](https://github.com/eea/volto-cca-policy/commit/517eeb817264a47bbfd6b9b7d22aaf22d44ed224)]
1847
1855
  - Refs #161485 - Fix ECDE name conflict. [GhitaB - [`8bfd99f`](https://github.com/eea/volto-cca-policy/commit/8bfd99ff68bb82a04d1c0ed625fa514fcf46289e)]
@@ -2058,7 +2066,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
2058
2066
 
2059
2067
  #### :house: Internal changes
2060
2068
 
2061
- - chore: [JENKINS] Remove alpha testing version [valentinab25 - [`ad1ced0`](https://github.com/eea/volto-cca-policy/commit/ad1ced0971ba116c13a3b5fcc039172cc915c919)]
2062
2069
 
2063
2070
  #### :hammer_and_wrench: Others
2064
2071
 
@@ -2539,7 +2546,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
2539
2546
  #### :hammer_and_wrench: Others
2540
2547
 
2541
2548
  - Refs #158294 - Update supported languages list. [GhitaB - [`0a4f91f`](https://github.com/eea/volto-cca-policy/commit/0a4f91f39b7edc367bd4c127d6a8f273c7788361)]
2542
- - Add Sonarqube tag using cca-frontend addons list [EEA Jenkins - [`8f1f9ce`](https://github.com/eea/volto-cca-policy/commit/8f1f9ce6c22805670cc0800d3c779b6d619d0f31)]
2543
2549
  ### [0.1.1](https://github.com/eea/volto-cca-policy/compare/0.1.0...0.1.1) - 13 December 2022
2544
2550
 
2545
2551
  #### :hammer_and_wrench: Others
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-cca-policy",
3
- "version": "0.2.96",
3
+ "version": "0.2.97",
4
4
  "description": "@eeacms/volto-cca-policy: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -34,6 +34,7 @@ describe('AdaptationOptionView', () => {
34
34
  },
35
35
  ],
36
36
  websites: ['https://my-website.com'],
37
+ cca_published: '2022-06-24T12:52:50+00:00',
37
38
  source: {
38
39
  'content-type': 'text/html',
39
40
  data: '<p>Some text</p>',
@@ -26,6 +26,7 @@ describe('CaseStudyView', () => {
26
26
  const content = {
27
27
  title: 'CaseStudyView',
28
28
  cca_gallery: [],
29
+ cca_published: '2022-06-24T12:52:50+00:00',
29
30
  geochars:
30
31
  '{\r\n "geoElements":{"element":"GLOBAL",\r\n "macrotrans":null,"biotrans":null,"countries":[],\r\n "subnational":[],"city":""}}',
31
32
  };
@@ -37,6 +37,7 @@ describe('DatabaseItemView', () => {
37
37
  '<p>Nam commodo suscipit quam. Praesent egestas neque eu enim. Quisque rutrum.</p>',
38
38
  encoding: 'utf-8',
39
39
  },
40
+ publication_date: '2022-06-24',
40
41
  geochars:
41
42
  '{\r\n "geoElements":{"element":"GLOBAL",\r\n "macrotrans":null,"biotrans":null,"countries":[],\r\n "subnational":[],"city":""}}',
42
43
  keywords: ['keyword 1', 'keyword 2'],
@@ -25,6 +25,7 @@ describe('ProjectView', () => {
25
25
  it('should render the component', () => {
26
26
  const content = {
27
27
  title: 'My ProjectView',
28
+ cca_published: '2022-06-24T12:52:50+00:00',
28
29
  geochars:
29
30
  '{\r\n "geoElements":{"element":"GLOBAL",\r\n "macrotrans":null,"biotrans":null,"countries":[],\r\n "subnational":[],"city":""}}',
30
31
  };
@@ -0,0 +1,238 @@
1
+ // Original: https://github.com/plone/volto/blob/16.x.x/src/components/manage/Widgets/FileWidget.jsx
2
+
3
+ /**
4
+ * FileWidget component.
5
+ * @module components/manage/Widgets/FileWidget
6
+ */
7
+
8
+ import React from 'react';
9
+ import PropTypes from 'prop-types';
10
+ import { Button, Image, Dimmer } from 'semantic-ui-react';
11
+ import { readAsDataURL } from 'promise-file-reader';
12
+ import { injectIntl } from 'react-intl';
13
+ import deleteSVG from '@plone/volto/icons/delete.svg';
14
+ import { Icon, FormFieldWrapper } from '@plone/volto/components';
15
+ import loadable from '@loadable/component';
16
+ import { flattenToAppURL, validateFileUploadSize } from '@plone/volto/helpers';
17
+ import { defineMessages, useIntl } from 'react-intl';
18
+
19
+ const imageMimetypes = [
20
+ 'image/png',
21
+ 'image/jpeg',
22
+ 'image/webp',
23
+ 'image/jpg',
24
+ 'image/gif',
25
+ 'image/svg+xml',
26
+ ];
27
+ const Dropzone = loadable(() => import('react-dropzone'));
28
+
29
+ const messages = defineMessages({
30
+ releaseDrag: {
31
+ id: 'Drop files here ...',
32
+ defaultMessage: 'Drop files here ...',
33
+ },
34
+ editFile: {
35
+ id: 'Drop file here to replace the existing file',
36
+ defaultMessage: 'Drop file here to replace the existing file',
37
+ },
38
+ fileDrag: {
39
+ id: 'Drop file here to upload a new file',
40
+ defaultMessage: 'Drop file here to upload a new file',
41
+ },
42
+ replaceFile: {
43
+ id: 'Replace existing file',
44
+ defaultMessage: 'Replace existing file',
45
+ },
46
+ addNewFile: {
47
+ id: 'Choose a file',
48
+ defaultMessage: 'Choose a file',
49
+ },
50
+ });
51
+
52
+ /**
53
+ * FileWidget component class.
54
+ * @function FileWidget
55
+ * @returns {string} Markup of the component.
56
+ *
57
+ * To use it, in schema properties, declare a field like:
58
+ *
59
+ * ```jsx
60
+ * {
61
+ * title: "File",
62
+ * widget: 'file',
63
+ * }
64
+ * ```
65
+ * or:
66
+ *
67
+ * ```jsx
68
+ * {
69
+ * title: "File",
70
+ * type: 'object',
71
+ * }
72
+ * ```
73
+ *
74
+ */
75
+ const FileWidget = (props) => {
76
+ const { id, value, onChange, isDisabled } = props;
77
+ const [fileType, setFileType] = React.useState(false);
78
+ const intl = useIntl();
79
+
80
+ React.useEffect(() => {
81
+ if (value && imageMimetypes.includes(value['content-type'])) {
82
+ setFileType(true);
83
+ }
84
+ }, [value]);
85
+
86
+ const imgsrc = value?.download
87
+ ? `${flattenToAppURL(value?.download)}?id=${Date.now()}`
88
+ : value?.data
89
+ ? `data:${value['content-type']};${value.encoding},${value.data}`
90
+ : null;
91
+
92
+ /**
93
+ * Drop handler
94
+ * @method onDrop
95
+ * @param {array} files File objects
96
+ * @returns {undefined}
97
+ */
98
+ const onDrop = (files) => {
99
+ const file = files[0];
100
+ if (!validateFileUploadSize(file, intl.formatMessage)) return;
101
+ readAsDataURL(file).then((data) => {
102
+ const fields = data.match(/^data:(.*);(.*),(.*)$/);
103
+ onChange(id, {
104
+ data: fields[3],
105
+ encoding: fields[2],
106
+ 'content-type': fields[1],
107
+ filename: file.name,
108
+ });
109
+ });
110
+
111
+ let reader = new FileReader();
112
+ reader.onload = function () {
113
+ const fields = reader.result.match(/^data:(.*);(.*),(.*)$/);
114
+ if (imageMimetypes.includes(fields[1])) {
115
+ setFileType(true);
116
+ let imagePreview = document.getElementById(`field-${id}-image`);
117
+ imagePreview.src = reader.result;
118
+ } else {
119
+ setFileType(false);
120
+ }
121
+ };
122
+ reader.readAsDataURL(files[0]);
123
+ };
124
+
125
+ return (
126
+ <FormFieldWrapper {...props}>
127
+ <Dropzone onDrop={onDrop}>
128
+ {({ getRootProps, getInputProps, isDragActive }) => (
129
+ <div className="file-widget-dropzone" {...getRootProps()}>
130
+ {isDragActive && <Dimmer active></Dimmer>}
131
+ {fileType ? (
132
+ <Image
133
+ className="image-preview"
134
+ id={`field-${id}-image`}
135
+ size="small"
136
+ src={imgsrc}
137
+ />
138
+ ) : (
139
+ <div className="dropzone-placeholder">
140
+ {isDragActive ? (
141
+ <p className="dropzone-text">
142
+ {intl.formatMessage(messages.releaseDrag)}
143
+ </p>
144
+ ) : value ? (
145
+ <p className="dropzone-text">
146
+ {intl.formatMessage(messages.editFile)}
147
+ </p>
148
+ ) : (
149
+ <p className="dropzone-text">
150
+ {intl.formatMessage(messages.fileDrag)}
151
+ </p>
152
+ )}
153
+ </div>
154
+ )}
155
+
156
+ <label className="label-file-widget-input">
157
+ {value
158
+ ? intl.formatMessage(messages.replaceFile)
159
+ : intl.formatMessage(messages.addNewFile)}
160
+ </label>
161
+ <input
162
+ {...getInputProps({
163
+ type: 'file',
164
+ style: { display: 'none' },
165
+ })}
166
+ id={`field-${id}`}
167
+ name={id}
168
+ type="file"
169
+ disabled={isDisabled}
170
+ />
171
+ </div>
172
+ )}
173
+ </Dropzone>
174
+
175
+ {value && value.download && (
176
+ <a
177
+ className="image-download-btn"
178
+ href={flattenToAppURL(value.download)}
179
+ download={value.filename || 'download'}
180
+ >
181
+ Download image
182
+ </a>
183
+ )}
184
+ <div className="field-file-name">
185
+ {value && value.filename}
186
+ {value && (
187
+ <Button
188
+ type="button"
189
+ icon
190
+ basic
191
+ className="delete-button"
192
+ aria-label="delete file"
193
+ disabled={isDisabled}
194
+ onClick={() => {
195
+ onChange(id, null);
196
+ setFileType(false);
197
+ }}
198
+ >
199
+ <Icon name={deleteSVG} size="20px" />
200
+ </Button>
201
+ )}
202
+ </div>
203
+ </FormFieldWrapper>
204
+ );
205
+ };
206
+
207
+ /**
208
+ * Property types.
209
+ * @property {Object} propTypes Property types.
210
+ * @static
211
+ */
212
+ FileWidget.propTypes = {
213
+ id: PropTypes.string.isRequired,
214
+ title: PropTypes.string.isRequired,
215
+ description: PropTypes.string,
216
+ required: PropTypes.bool,
217
+ error: PropTypes.arrayOf(PropTypes.string),
218
+ value: PropTypes.shape({
219
+ '@type': PropTypes.string,
220
+ title: PropTypes.string,
221
+ }),
222
+ onChange: PropTypes.func.isRequired,
223
+ wrapped: PropTypes.bool,
224
+ };
225
+
226
+ /**
227
+ * Default properties.
228
+ * @property {Object} defaultProps Default properties.
229
+ * @static
230
+ */
231
+ FileWidget.defaultProps = {
232
+ description: null,
233
+ required: false,
234
+ error: [],
235
+ value: null,
236
+ };
237
+
238
+ export default injectIntl(FileWidget);
@@ -0,0 +1,91 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-intl-redux';
3
+ import { render, waitFor } from '@testing-library/react';
4
+ import configureStore from 'redux-mock-store';
5
+
6
+ import FileWidget from './PromotionalImageWidget';
7
+
8
+ jest.spyOn(global.Date, 'now').mockImplementation(() => '0');
9
+
10
+ const mockStore = configureStore();
11
+
12
+ describe('FileWidget', () => {
13
+ test('renders an empty file widget component', async () => {
14
+ const store = mockStore({
15
+ intl: {
16
+ locale: 'en',
17
+ messages: {},
18
+ },
19
+ });
20
+
21
+ const { container } = render(
22
+ <Provider store={store}>
23
+ <FileWidget
24
+ id="my-field"
25
+ title="My field"
26
+ fieldSet="default"
27
+ onChange={() => {}}
28
+ />
29
+ </Provider>,
30
+ );
31
+
32
+ await waitFor(() => {});
33
+ expect(container).toMatchSnapshot();
34
+ });
35
+ test('renders a file widget component with value', async () => {
36
+ const store = mockStore({
37
+ intl: {
38
+ locale: 'en',
39
+ messages: {},
40
+ },
41
+ });
42
+
43
+ const { container } = render(
44
+ <Provider store={store}>
45
+ <FileWidget
46
+ id="my-field"
47
+ title="My field"
48
+ fieldSet="default"
49
+ onChange={() => {}}
50
+ value={{
51
+ download: 'http://myfile',
52
+ 'content-type': 'image/png',
53
+ filename: 'myfile',
54
+ encoding: '',
55
+ }}
56
+ />
57
+ </Provider>,
58
+ );
59
+
60
+ await waitFor(() => {});
61
+ expect(container).toMatchSnapshot();
62
+ });
63
+ test('renders a file widget component with value in raw data', async () => {
64
+ const store = mockStore({
65
+ intl: {
66
+ locale: 'en',
67
+ messages: {},
68
+ },
69
+ });
70
+
71
+ const { container } = render(
72
+ <Provider store={store}>
73
+ <FileWidget
74
+ id="my-field"
75
+ title="My field"
76
+ fieldSet="default"
77
+ onChange={() => {}}
78
+ value={{
79
+ data: 'oiweurtksdgfjaslfqw9523563456',
80
+ 'content-type': 'image/png',
81
+ filename: 'myfile',
82
+ encoding: 'base64',
83
+ }}
84
+ />
85
+ </Provider>,
86
+ );
87
+
88
+ await waitFor(() => {});
89
+ expect(container).toMatchSnapshot();
90
+ });
91
+ });
@@ -542,4 +542,5 @@ export const EU_COUNTRIES = [
542
542
  'SK',
543
543
  'TR',
544
544
  'XK',
545
+ 'GB',
545
546
  ];
@@ -254,64 +254,34 @@ export const ReferenceInfo = (props) => {
254
254
  ) : null;
255
255
  };
256
256
 
257
- export const PublishedModifiedInfo = (props) => {
258
- const { content } = props;
259
-
260
- const cca_modif = content.cca_last_modified;
261
- const cca_publ = content.cca_published;
257
+ export const PublishedModifiedInfo = ({ content }) => {
258
+ const { cca_published, publication_date } = content;
262
259
 
263
- let published = null;
264
- let modified = null;
265
260
  const dateFormatOptions = {
266
261
  year: 'numeric',
267
262
  month: 'short',
268
263
  day: 'numeric',
269
264
  };
270
- if (cca_modif !== undefined) {
271
- modified = new Date(cca_modif).toLocaleString('default', dateFormatOptions);
272
- } else {
273
- modified = new Date(content.modification_date).toLocaleString(
274
- 'default',
275
- dateFormatOptions,
276
- );
277
- }
278
- if (cca_publ !== undefined) {
279
- published = new Date(cca_publ).toLocaleString('default', dateFormatOptions);
280
- } else {
281
- published = new Date(content.publication_date).toLocaleString(
282
- 'default',
283
- dateFormatOptions,
284
- );
285
- }
286
265
 
287
- // TODO fix wrong information for some cases. Test for each content type.
288
- return (
289
- <div className="published-modified-info">
266
+ const published = new Date(cca_published || publication_date).toLocaleString(
267
+ 'default',
268
+ dateFormatOptions,
269
+ );
270
+
271
+ return published ? (
272
+ <div className="published-info">
290
273
  <p>
291
- <span>
292
- <strong>
293
- <FormattedMessage
294
- id="Published in Climate-ADAPT"
295
- defaultMessage="Published in Climate-ADAPT"
296
- />
297
- </strong>
298
- &nbsp;
299
- {published}
300
- </span>
301
- <span> &nbsp; - &nbsp; </span>
302
- <span>
303
- <strong>
304
- <FormattedMessage
305
- id="Last Modified in Climate-ADAPT"
306
- defaultMessage="Last Modified in Climate-ADAPT"
307
- />
308
- </strong>
309
- &nbsp;
310
- {modified}
311
- </span>
274
+ <strong>
275
+ <FormattedMessage
276
+ id="Published in Climate-ADAPT"
277
+ defaultMessage="Published in Climate-ADAPT"
278
+ />
279
+ </strong>
280
+ {': '}
281
+ {published}
312
282
  </p>
313
283
  </div>
314
- );
284
+ ) : null;
315
285
  };
316
286
 
317
287
  export const DocumentsList = (props) => {
package/src/index.js CHANGED
@@ -21,6 +21,7 @@ import DatabaseItemView from './components/theme/Views/DatabaseItemView';
21
21
 
22
22
  import GeocharsWidget from './components/theme/Widgets/GeocharsWidget';
23
23
  import GeolocationWidget from './components/theme/Widgets/GeolocationWidget';
24
+ import PromotionalImageWidget from './components/theme/Widgets/PromotionalImageWidget';
24
25
  import MigrationButtons from './components/MigrationButtons';
25
26
  import HealthHorizontalCardItem from './components/Result/HealthHorizontalCardItem';
26
27
  import ClusterHorizontalCardItem from './components/Result/ClusterHorizontalCardItem';
@@ -396,6 +397,7 @@ const applyConfig = (config) => {
396
397
  // Custom widgets
397
398
  config.widgets.id.geochars = GeocharsWidget;
398
399
  config.widgets.id.geolocation = GeolocationWidget;
400
+ config.widgets.id.promotional_image = PromotionalImageWidget;
399
401
 
400
402
  if (config.widgets.views?.widget) {
401
403
  config.widgets.views.id.rast_steps = RASTWidgetView;
@@ -199,3 +199,20 @@ body.subsite {
199
199
  iframe {
200
200
  border: none;
201
201
  }
202
+
203
+ .label-file-widget-input {
204
+ font-weight: bold;
205
+ }
206
+
207
+ .image-download-btn {
208
+ display: inline-block;
209
+ padding: 7px 23px;
210
+ border: 2px solid #007eb1;
211
+ margin-top: 10px;
212
+ border-radius: 10px;
213
+ color: #007eb1;
214
+ cursor: pointer;
215
+ opacity: 0.8;
216
+ max-width: max-content;
217
+ font-weight: bold;
218
+ }
@@ -38,7 +38,7 @@ div.geochars-field {
38
38
  float: right;
39
39
  }
40
40
 
41
- .published-modified-info {
41
+ .published-info {
42
42
  margin: 2em 0;
43
43
  font-size: 0.8em;
44
44
  }