@vonaffenfels/contentful-teasermanager 1.0.35 → 1.0.36
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/package.json +4 -5
- package/src/components/Contentful/Dialog/NewestArticles.js +153 -0
- package/src/components/Contentful/Dialog.js +44 -3
- package/src/components/Contentful/EntryEditor.js +15 -1
- package/src/components/Teasermanager/Timeline.js +4 -3
- package/src/hooks/useDebounce.js +21 -0
- package/src/index.js +6 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vonaffenfels/contentful-teasermanager",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.36",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"prepublish": "yarn run build",
|
|
6
6
|
"dev": "yarn run start",
|
|
@@ -16,11 +16,9 @@
|
|
|
16
16
|
"@babel/preset-env": "^7.13.15",
|
|
17
17
|
"@babel/preset-react": "^7.13.13",
|
|
18
18
|
"@contentful/app-sdk": "^3.33.0",
|
|
19
|
+
"@contentful/f36-components": "^4.50.2",
|
|
19
20
|
"@contentful/field-editor-single-line": "^0.14.1",
|
|
20
21
|
"@contentful/field-editor-test-utils": "^0.11.1",
|
|
21
|
-
"@contentful/forma-36-fcss": "^0.3.1",
|
|
22
|
-
"@contentful/forma-36-react-components": "^3.88.3",
|
|
23
|
-
"@contentful/forma-36-tokens": "^0.10.1",
|
|
24
22
|
"@svgr/webpack": "^5.5.0",
|
|
25
23
|
"babel-loader": "^8.2.2",
|
|
26
24
|
"classnames": "^2.3.2",
|
|
@@ -59,6 +57,7 @@
|
|
|
59
57
|
"@babel/preset-env": "^7.13.15",
|
|
60
58
|
"@babel/preset-react": "^7.13.13",
|
|
61
59
|
"@contentful/app-sdk": "^3.33.0",
|
|
60
|
+
"@contentful/f36-components": "^4.50.2",
|
|
62
61
|
"@contentful/field-editor-single-line": "^0.14.1",
|
|
63
62
|
"@contentful/field-editor-test-utils": "^0.11.1",
|
|
64
63
|
"@contentful/forma-36-fcss": "^0.3.1",
|
|
@@ -99,7 +98,7 @@
|
|
|
99
98
|
"@vonaffenfels/slate-editor": "^1.0.35",
|
|
100
99
|
"webpack": "5.88.2"
|
|
101
100
|
},
|
|
102
|
-
"gitHead": "
|
|
101
|
+
"gitHead": "bef8a736da079b4fe1c79e5490d2bbca3c518772",
|
|
103
102
|
"publishConfig": {
|
|
104
103
|
"access": "public"
|
|
105
104
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {EntityList, Pagination, Spinner, Select, Autocomplete, TextInput} from "@contentful/f36-components";
|
|
2
|
+
import {getContentfulClient} from "../../../lib/contentfulClient";
|
|
3
|
+
import {useEffect, useState} from "react";
|
|
4
|
+
import format from "date-fns/format"
|
|
5
|
+
import useDebounce from "../../../hooks/useDebounce";
|
|
6
|
+
import classNames from "classnames";
|
|
7
|
+
|
|
8
|
+
export const NewestArticles = ({
|
|
9
|
+
slotState = {},
|
|
10
|
+
portals = {},
|
|
11
|
+
sdk,
|
|
12
|
+
onEntryClick = () => alert("onEntryClick missing"),
|
|
13
|
+
getArticleThumbnailUrl = (article) => alert("getArticleThumbnailUrl missing"),
|
|
14
|
+
}) => {
|
|
15
|
+
const {portal, slotId} = sdk.parameters.invocation;
|
|
16
|
+
const contentfulClient = getContentfulClient();
|
|
17
|
+
const limit = 25;
|
|
18
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
19
|
+
const [authorSearchQuery, setAuthorSearchQuery] = useState("");
|
|
20
|
+
const [selectedPortal, setSelectedPortal] = useState(portal);
|
|
21
|
+
const [loading, setLoading] = useState(true);
|
|
22
|
+
const [data, setData] = useState({items: [], total: 0});
|
|
23
|
+
const [page, setPage] = useState(0);
|
|
24
|
+
const [selectedAuthor, setSelectedAuthor] = useState(null);
|
|
25
|
+
const [authors, setAuthors] = useState([]);
|
|
26
|
+
const [authorsLoading, setAuthorsLoading] = useState(false);
|
|
27
|
+
|
|
28
|
+
const debouncedSearch = useDebounce(searchQuery, 500);
|
|
29
|
+
const debouncedAuthorSearch = useDebounce(authorSearchQuery, 500);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
setPage(0);
|
|
33
|
+
}, [searchQuery, selectedAuthor, selectedPortal]);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
setLoading(true);
|
|
37
|
+
const params = {
|
|
38
|
+
limit: limit,
|
|
39
|
+
skip: page * limit,
|
|
40
|
+
content_type: "article",
|
|
41
|
+
order: "-fields.teaserDate",
|
|
42
|
+
"fields.portal": portal,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (searchQuery) {
|
|
46
|
+
params["fields.title[match]"] = searchQuery;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (selectedAuthor) {
|
|
50
|
+
params["fields.authors.sys.id"] = selectedAuthor;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
contentfulClient.getEntries(params).then((response) => {
|
|
54
|
+
setLoading(false);
|
|
55
|
+
setData(response);
|
|
56
|
+
});
|
|
57
|
+
}, [portal, debouncedSearch, selectedAuthor, limit, page]);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
setAuthorsLoading(true);
|
|
61
|
+
const params = {
|
|
62
|
+
limit: 100,
|
|
63
|
+
content_type: "author",
|
|
64
|
+
order: "fields.name",
|
|
65
|
+
"fields.portal": portal,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (debouncedAuthorSearch) {
|
|
69
|
+
params["fields.name[match]"] = debouncedAuthorSearch
|
|
70
|
+
} else {
|
|
71
|
+
setSelectedAuthor(null);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
contentfulClient.getEntries(params).then((response) => {
|
|
75
|
+
setAuthors(response?.items || []);
|
|
76
|
+
setAuthorsLoading(false);
|
|
77
|
+
});
|
|
78
|
+
}, [debouncedAuthorSearch, portal]);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (selectedAuthor) {
|
|
82
|
+
if (!authors.find(author => author.sys.id === selectedAuthor)) {
|
|
83
|
+
setSelectedAuthor(null);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}, [authors, selectedAuthor]);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className="p-4 flex flex-col space-y-4">
|
|
90
|
+
<div className="flex space-x-2">
|
|
91
|
+
<div className="flex-grow">
|
|
92
|
+
<TextInput
|
|
93
|
+
width={"full"}
|
|
94
|
+
placeholder={"Suche nach Titel"}
|
|
95
|
+
onChange={(e) => setSearchQuery(e.target.value)}/>
|
|
96
|
+
</div>
|
|
97
|
+
<div className="flex-grow-0">
|
|
98
|
+
<Autocomplete
|
|
99
|
+
isLoading={authorsLoading}
|
|
100
|
+
onSelectItem={item => setSelectedAuthor(item?.sys?.id)}
|
|
101
|
+
items={authors}
|
|
102
|
+
closeAfterSelect={true}
|
|
103
|
+
itemToString={(item) => item.fields.name.de}
|
|
104
|
+
renderItem={(item) => `${item.fields.name.de}`}
|
|
105
|
+
onInputValueChange={setAuthorSearchQuery}
|
|
106
|
+
|
|
107
|
+
placeholder={'Autor'}
|
|
108
|
+
noMatchesMessage={'Kein Autor gefunden'}
|
|
109
|
+
>
|
|
110
|
+
{(options) => {
|
|
111
|
+
return options.map(option => <span key={option.value}>{option.label}</span>)
|
|
112
|
+
}}
|
|
113
|
+
</Autocomplete>
|
|
114
|
+
</div>
|
|
115
|
+
<div className="flex-grow-0">
|
|
116
|
+
<Select value={selectedPortal} onChange={e => setSelectedPortal(e.target.value)}>
|
|
117
|
+
{Object.keys(portals).sort().map(optionValue => <Select.Option
|
|
118
|
+
key={optionValue}
|
|
119
|
+
value={optionValue}>{portals[optionValue]}</Select.Option>)}
|
|
120
|
+
</Select>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<EntityList>
|
|
124
|
+
{loading && <Spinner/>}
|
|
125
|
+
{!loading && data.items.map((entry) => {
|
|
126
|
+
return <div
|
|
127
|
+
key={entry.sys.id}
|
|
128
|
+
className={classNames({
|
|
129
|
+
"border-2 border-red-300": Object.keys(slotState || {}).find(slot => slotState[slot] === entry.sys.id && slot !== slotId),
|
|
130
|
+
"border-2 border-green-300": Object.keys(slotState || {}).find(slot => slotState[slot] === entry.sys.id && slot === slotId),
|
|
131
|
+
})}
|
|
132
|
+
>
|
|
133
|
+
<EntityList.Item
|
|
134
|
+
isLoading={loading}
|
|
135
|
+
entityType={format(new Date(entry.fields.teaserDate?.de || entry.fields.date?.de), "dd.MM.yyyy HH:mm")}
|
|
136
|
+
title={entry.fields.title?.de}
|
|
137
|
+
onClick={() => onEntryClick(entry)}
|
|
138
|
+
thumbnailUrl={getArticleThumbnailUrl(entry)}
|
|
139
|
+
description={""}
|
|
140
|
+
status={entry.sys.publishedAt ? "published" : "draft"}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
})}
|
|
144
|
+
</EntityList>
|
|
145
|
+
<Pagination
|
|
146
|
+
activePage={page}
|
|
147
|
+
onPageChange={setPage}
|
|
148
|
+
itemsPerPage={limit}
|
|
149
|
+
isLastPage={data.total / limit <= page + 1}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -1,7 +1,48 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, {useEffect, useState} from 'react';
|
|
2
|
+
import {NewestArticles} from "./Dialog/NewestArticles";
|
|
2
3
|
|
|
3
|
-
const Dialog = ({sdk}) => {
|
|
4
|
-
|
|
4
|
+
const Dialog = ({sdk, portals, getArticleThumbnailUrl}) => {
|
|
5
|
+
const [slotState, setSlotState] = useState({});
|
|
6
|
+
|
|
7
|
+
const selectEntry = (entry) => {
|
|
8
|
+
sdk.close(entry);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const loadSlotStateForPage = async (date) => {
|
|
12
|
+
try {
|
|
13
|
+
const apiRoot = sdk.parameters.instance.apiRoot;
|
|
14
|
+
const portal = sdk.parameters.invocation.portal;
|
|
15
|
+
const pageId = sdk.parameters.invocation.entryId;
|
|
16
|
+
const date = sdk.parameters.invocation.date;
|
|
17
|
+
const teasermanagerUrl = `${apiRoot}/api/findStateForPage?project=${portal}&page=${pageId}&date=${date}`;
|
|
18
|
+
const response = await fetch(teasermanagerUrl).then(res => res.json());
|
|
19
|
+
|
|
20
|
+
if (response?.message) {
|
|
21
|
+
console.error(response.message);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return response?.data || {};
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.error(e);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
loadSlotStateForPage().then(setSlotState);
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
return <div style={{backgroundColor: "#FFFFFF", minHeight: "100vh"}}>
|
|
35
|
+
<NewestArticles sdk={sdk} onEntryClick={selectEntry} slotState={slotState} getArticleThumbnailUrl={getArticleThumbnailUrl} portals={portals}/>
|
|
36
|
+
{/*
|
|
37
|
+
<Tabs defaultTab="newest">
|
|
38
|
+
<Tabs.List>
|
|
39
|
+
<Tabs.Tab panelId="newest">Neuste Artikel</Tabs.Tab>
|
|
40
|
+
</Tabs.List>
|
|
41
|
+
<Tabs.Panel id="newest">
|
|
42
|
+
</Tabs.Panel>
|
|
43
|
+
</Tabs>
|
|
44
|
+
*/}
|
|
45
|
+
</div>
|
|
5
46
|
};
|
|
6
47
|
|
|
7
48
|
export default Dialog;
|
|
@@ -26,7 +26,21 @@ const Entry = ({sdk}) => {
|
|
|
26
26
|
}, []);
|
|
27
27
|
|
|
28
28
|
const onSlotClick = (slotId, currentDate) => {
|
|
29
|
-
return sdk.dialogs.
|
|
29
|
+
return sdk.dialogs.openCurrentApp({
|
|
30
|
+
title: "Artikel wählen",
|
|
31
|
+
width: "fullWidth",
|
|
32
|
+
position: "top",
|
|
33
|
+
allowHeightOverflow: true,
|
|
34
|
+
minHeight: "500px",
|
|
35
|
+
shouldCloseOnOverlayClick: true,
|
|
36
|
+
shouldCloseOnEscapePress: true,
|
|
37
|
+
parameters: {
|
|
38
|
+
portal,
|
|
39
|
+
date: currentDate,
|
|
40
|
+
slotId: slotId,
|
|
41
|
+
entryId: sdk.entry.getSys().id,
|
|
42
|
+
},
|
|
43
|
+
}).then(async (entry) => {
|
|
30
44
|
if (!entry) {
|
|
31
45
|
return;
|
|
32
46
|
}
|
|
@@ -12,7 +12,6 @@ export const Timeline = ({
|
|
|
12
12
|
loadTimelineStateForPage,
|
|
13
13
|
}) => {
|
|
14
14
|
const [timelineState, setTimelineState] = useState(null);
|
|
15
|
-
|
|
16
15
|
const stepsInRange = (24 * 60) / 15 + 1; // must be uneven so we have a center ;)
|
|
17
16
|
|
|
18
17
|
useEffect(() => {
|
|
@@ -27,6 +26,8 @@ export const Timeline = ({
|
|
|
27
26
|
const leftDate = currentDate && subtract(currentDate, {minutes: stepsInRange / 2 * 15});
|
|
28
27
|
if (leftDate) {
|
|
29
28
|
leftDate.setMinutes((Math.round(currentDate.getMinutes() / 15) * 15) % 60);
|
|
29
|
+
leftDate.setSeconds(0);
|
|
30
|
+
leftDate.setMilliseconds(0);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
const handleDateChange = (e) => {
|
|
@@ -105,13 +106,13 @@ export const Timeline = ({
|
|
|
105
106
|
<div className={styles.timeline}>
|
|
106
107
|
{!!leftDate && Array(stepsInRange).fill(0).map((_, index) => {
|
|
107
108
|
const dotDate = new Date(leftDate.getTime() + (15 * 60 * 1000 * (index + 1)));
|
|
108
|
-
const stateChanged = timelineState?.[
|
|
109
|
+
const stateChanged = timelineState?.[dotDate.toISOString()] || 0;
|
|
109
110
|
|
|
110
111
|
return <div
|
|
111
112
|
key={index}
|
|
112
113
|
className={classNames(styles.timelineDot, {
|
|
113
114
|
[styles.timelineDotActive]: dotDate.getTime() === currentDate.getTime(),
|
|
114
|
-
[styles.timelineDotChanged]:
|
|
115
|
+
[styles.timelineDotChanged]: !!stateChanged,
|
|
115
116
|
})}
|
|
116
117
|
onClick={e => {
|
|
117
118
|
e.preventDefault();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React, {useState, useEffect} from "react";
|
|
2
|
+
|
|
3
|
+
export default function useDebounce(value, delay) {
|
|
4
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
5
|
+
|
|
6
|
+
useEffect(
|
|
7
|
+
() => {
|
|
8
|
+
const handler = setTimeout(() => {
|
|
9
|
+
setDebouncedValue(value);
|
|
10
|
+
}, delay);
|
|
11
|
+
|
|
12
|
+
return () => {
|
|
13
|
+
clearTimeout(handler);
|
|
14
|
+
};
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
[value],
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return debouncedValue;
|
|
21
|
+
}
|
package/src/index.js
CHANGED
|
@@ -5,9 +5,6 @@ import {
|
|
|
5
5
|
init,
|
|
6
6
|
locations,
|
|
7
7
|
} from '@contentful/app-sdk';
|
|
8
|
-
import '@contentful/forma-36-react-components/dist/styles.css';
|
|
9
|
-
import '@contentful/forma-36-fcss/dist/styles.css';
|
|
10
|
-
import '@contentful/forma-36-tokens/dist/css/index.css';
|
|
11
8
|
|
|
12
9
|
import EntryEditor from './components/Contentful/EntryEditor';
|
|
13
10
|
import Config from './components/Contentful/ConfigScreen';
|
|
@@ -19,10 +16,13 @@ export const BaseContentfulApp = (props) => {
|
|
|
19
16
|
init((sdk) => {
|
|
20
17
|
const rootContainer = document.getElementById('root');
|
|
21
18
|
|
|
22
|
-
|
|
19
|
+
if (props.initPortal && sdk.parameters?.invocation?.portal) {
|
|
20
|
+
props.initPortal(sdk.parameters.invocation.portal);
|
|
21
|
+
}
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
initContentfulClient(sdk);
|
|
25
24
|
|
|
25
|
+
const RendererWrapperComponent = props.RendererWrapperComponent || (({children}) => <div>{children}</div>);
|
|
26
26
|
const ComponentLocationSettings = [
|
|
27
27
|
{
|
|
28
28
|
location: locations.LOCATION_APP_CONFIG,
|
|
@@ -34,7 +34,7 @@ export const BaseContentfulApp = (props) => {
|
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
location: locations.LOCATION_DIALOG,
|
|
37
|
-
component: <Dialog sdk={sdk}/>,
|
|
37
|
+
component: <Dialog sdk={sdk} portals={props.portals} getArticleThumbnailUrl={props.getArticleThumbnailUrl}/>,
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
location: locations.LOCATION_PAGE,
|