@vonaffenfels/contentful-teasermanager 1.1.68 → 1.1.70
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/.babelrc +19 -19
- package/.nvmrc +1 -1
- package/package.json +3 -3
- package/postcss.config.js +6 -6
- package/src/components/Contentful/ConfigScreen.js +154 -154
- package/src/components/Contentful/Dialog/NewestArticles.js +188 -178
- package/src/components/Contentful/Dialog.js +58 -58
- package/src/components/Contentful/EntryEditor.js +139 -139
- package/src/components/Contentful/Page.js +9 -9
- package/src/components/NoAccess.js +12 -12
- package/src/components/Teasermanager/Timeline.js +179 -179
- package/src/components/Teasermanager/Timeline.module.css +89 -89
- package/src/components/Teasermanager.js +237 -237
- package/src/components/Teasermanager.module.css +105 -105
- package/src/dev.js +5 -5
- package/src/hooks/useDebounce.js +20 -20
- package/src/hooks/useOnScreen.js +23 -23
- package/src/icons/remove.svg +7 -7
- package/src/index.html +11 -11
- package/src/index.js +51 -51
- package/src/lib/contentfulClient.js +12 -12
- package/src/lib/runLoaders.js +46 -46
- package/src/scss/index.scss +11 -11
- package/tailwind.config.js +5 -5
- package/webpack.config.dev.js +25 -25
- package/webpack.config.js +174 -174
- package/dist/TestStory.js +0 -94
- package/dist/TestStory2.js +0 -94
- package/dist/TestStory3.js +0 -94
- package/dist/index.html +0 -12
- package/dist/index.js +0 -81197
|
@@ -1,179 +1,189 @@
|
|
|
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 [enabledPortals, setEnabledPortals] = useState([portal]);
|
|
21
|
-
const [selectedPortal, setSelectedPortal] = useState(portal);
|
|
22
|
-
const [loading, setLoading] = useState(true);
|
|
23
|
-
const [data, setData] = useState({items: [], total: 0});
|
|
24
|
-
const [page, setPage] = useState(0);
|
|
25
|
-
const [selectedAuthor, setSelectedAuthor] = useState(null);
|
|
26
|
-
const [authors, setAuthors] = useState([]);
|
|
27
|
-
const [authorsLoading, setAuthorsLoading] = useState(false);
|
|
28
|
-
|
|
29
|
-
const debouncedSearch = useDebounce(searchQuery, 500);
|
|
30
|
-
const debouncedAuthorSearch = useDebounce(authorSearchQuery, 500);
|
|
31
|
-
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
sdk.space.getContentType("article").then(articleType => {
|
|
34
|
-
let enabledPortals = articleType.fields.find(v => v.id === "portal")?.validations?.find(v => !!v.in)?.in;
|
|
35
|
-
|
|
36
|
-
if (enabledPortals) {
|
|
37
|
-
setEnabledPortals(enabledPortals);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
}, []);
|
|
41
|
-
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
setPage(0);
|
|
44
|
-
}, [searchQuery, selectedAuthor, selectedPortal]);
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
setLoading(true);
|
|
48
|
-
const params = {
|
|
49
|
-
limit: limit,
|
|
50
|
-
skip: page * limit,
|
|
51
|
-
content_type: "article",
|
|
52
|
-
order: "-fields.teaserDate",
|
|
53
|
-
"fields.portal": selectedPortal,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
{
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
{
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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 [enabledPortals, setEnabledPortals] = useState([portal]);
|
|
21
|
+
const [selectedPortal, setSelectedPortal] = useState(portal);
|
|
22
|
+
const [loading, setLoading] = useState(true);
|
|
23
|
+
const [data, setData] = useState({items: [], total: 0});
|
|
24
|
+
const [page, setPage] = useState(0);
|
|
25
|
+
const [selectedAuthor, setSelectedAuthor] = useState(null);
|
|
26
|
+
const [authors, setAuthors] = useState([]);
|
|
27
|
+
const [authorsLoading, setAuthorsLoading] = useState(false);
|
|
28
|
+
|
|
29
|
+
const debouncedSearch = useDebounce(searchQuery, 500);
|
|
30
|
+
const debouncedAuthorSearch = useDebounce(authorSearchQuery, 500);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
sdk.space.getContentType("article").then(articleType => {
|
|
34
|
+
let enabledPortals = articleType.fields.find(v => v.id === "portal")?.validations?.find(v => !!v.in)?.in;
|
|
35
|
+
|
|
36
|
+
if (enabledPortals) {
|
|
37
|
+
setEnabledPortals(enabledPortals);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setPage(0);
|
|
44
|
+
}, [searchQuery, selectedAuthor, selectedPortal]);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
setLoading(true);
|
|
48
|
+
const params = {
|
|
49
|
+
limit: limit,
|
|
50
|
+
skip: page * limit,
|
|
51
|
+
content_type: "article",
|
|
52
|
+
order: "-fields.teaserDate",
|
|
53
|
+
"fields.portal": selectedPortal,
|
|
54
|
+
select: [
|
|
55
|
+
"fields.title",
|
|
56
|
+
"fields.authors",
|
|
57
|
+
"fields.teaserDate",
|
|
58
|
+
"fields.image",
|
|
59
|
+
"fields.date",
|
|
60
|
+
"fields.portal",
|
|
61
|
+
"sys.publishedAt",
|
|
62
|
+
"sys.id",
|
|
63
|
+
].join(","),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (searchQuery) {
|
|
67
|
+
params["fields.title[match]"] = searchQuery;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (selectedAuthor) {
|
|
71
|
+
params["fields.authors.sys.id"] = selectedAuthor;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
contentfulClient.getEntries(params).then((response) => {
|
|
75
|
+
setLoading(false);
|
|
76
|
+
setData(response);
|
|
77
|
+
});
|
|
78
|
+
}, [debouncedSearch, selectedAuthor,selectedPortal, limit, page]);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
setAuthorsLoading(true);
|
|
82
|
+
const params = {
|
|
83
|
+
limit: 100,
|
|
84
|
+
content_type: "author",
|
|
85
|
+
order: "fields.name",
|
|
86
|
+
"fields.portal": portal,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (debouncedAuthorSearch) {
|
|
90
|
+
params["fields.name[match]"] = debouncedAuthorSearch
|
|
91
|
+
} else {
|
|
92
|
+
setSelectedAuthor(null);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
contentfulClient.getEntries(params).then((response) => {
|
|
96
|
+
setAuthors(response?.items || []);
|
|
97
|
+
setAuthorsLoading(false);
|
|
98
|
+
});
|
|
99
|
+
}, [debouncedAuthorSearch, portal]);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (selectedAuthor) {
|
|
103
|
+
if (!authors.find(author => author.sys.id === selectedAuthor)) {
|
|
104
|
+
setSelectedAuthor(null);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}, [authors, selectedAuthor]);
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className="p-4 flex flex-col space-y-4">
|
|
111
|
+
<div className="flex space-x-2">
|
|
112
|
+
<div className="flex-grow">
|
|
113
|
+
<TextInput
|
|
114
|
+
width={"full"}
|
|
115
|
+
placeholder={"Suche nach Titel"}
|
|
116
|
+
onChange={(e) => setSearchQuery(e.target.value)}/>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="flex-grow-0">
|
|
119
|
+
<Autocomplete
|
|
120
|
+
isLoading={authorsLoading}
|
|
121
|
+
onSelectItem={item => setSelectedAuthor(item?.sys?.id)}
|
|
122
|
+
items={authors}
|
|
123
|
+
closeAfterSelect={true}
|
|
124
|
+
itemToString={(item) => item.fields?.name?.de}
|
|
125
|
+
renderItem={(item) => `${item.fields?.name?.de || "Unbekannter Name"}`}
|
|
126
|
+
onInputValueChange={setAuthorSearchQuery}
|
|
127
|
+
|
|
128
|
+
placeholder={'Autor'}
|
|
129
|
+
noMatchesMessage={'Kein Autor gefunden'}
|
|
130
|
+
>
|
|
131
|
+
{(options) => {
|
|
132
|
+
return options.map(option => <span key={option.value}>{option.label}</span>)
|
|
133
|
+
}}
|
|
134
|
+
</Autocomplete>
|
|
135
|
+
</div>
|
|
136
|
+
<div className="flex-grow-0">
|
|
137
|
+
<Select value={selectedPortal} onChange={e => setSelectedPortal(e.target.value)}>
|
|
138
|
+
{Object.keys(portals).filter(v => enabledPortals.includes(v)).sort().map(optionValue => <Select.Option
|
|
139
|
+
key={optionValue}
|
|
140
|
+
value={optionValue}>{portals[optionValue]}</Select.Option>)}
|
|
141
|
+
</Select>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
<EntityList>
|
|
145
|
+
{loading && <Spinner/>}
|
|
146
|
+
{!loading && data.items.map((entry) => {
|
|
147
|
+
const isCurrentlySelectedInOtherSlot = !! Object.keys(slotState || {}).find(slot => slotState[slot] === entry.sys.id && slot !== slotId);
|
|
148
|
+
const isCurrentlySelectedInSlot = !!Object.keys(slotState || {}).find(slot => slotState[slot] === entry.sys.id && slot === slotId);
|
|
149
|
+
|
|
150
|
+
let status = "draft";
|
|
151
|
+
if (entry.sys.publishedAt) {
|
|
152
|
+
status = "published";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let description = "";
|
|
156
|
+
if (isCurrentlySelectedInSlot) {
|
|
157
|
+
description = "Aktuell ausgewählt";
|
|
158
|
+
} else if (isCurrentlySelectedInOtherSlot) {
|
|
159
|
+
description = "Aktuell in anderem Teaser ausgewählt"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return <div
|
|
163
|
+
key={entry.sys.id}
|
|
164
|
+
className={classNames({
|
|
165
|
+
"border-2 border-red-300": isCurrentlySelectedInSlot,
|
|
166
|
+
"border-2 border-green-300": isCurrentlySelectedInOtherSlot,
|
|
167
|
+
})}
|
|
168
|
+
>
|
|
169
|
+
<EntityList.Item
|
|
170
|
+
isLoading={loading}
|
|
171
|
+
entityType={format(new Date(entry.fields.teaserDate?.de || entry.fields.date?.de), "dd.MM.yyyy HH:mm")}
|
|
172
|
+
title={entry.fields.title?.de}
|
|
173
|
+
onClick={() => onEntryClick(entry)}
|
|
174
|
+
thumbnailUrl={getArticleThumbnailUrl(entry)}
|
|
175
|
+
description={description}
|
|
176
|
+
status={status}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
})}
|
|
180
|
+
</EntityList>
|
|
181
|
+
<Pagination
|
|
182
|
+
activePage={page}
|
|
183
|
+
onPageChange={setPage}
|
|
184
|
+
itemsPerPage={limit}
|
|
185
|
+
isLastPage={data.total / limit <= page + 1}
|
|
186
|
+
/>
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
179
189
|
}
|
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
useEffect, useState,
|
|
3
|
-
} from 'react';
|
|
4
|
-
import {NewestArticles} from "./Dialog/NewestArticles";
|
|
5
|
-
import {NoAccess} from "../NoAccess";
|
|
6
|
-
|
|
7
|
-
const Dialog = ({
|
|
8
|
-
sdk,
|
|
9
|
-
portals,
|
|
10
|
-
getArticleThumbnailUrl,
|
|
11
|
-
}) => {
|
|
12
|
-
const [slotState, setSlotState] = useState({});
|
|
13
|
-
|
|
14
|
-
const selectEntry = (entry) => {
|
|
15
|
-
sdk.close(entry);
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const loadSlotStateForPage = async () => {
|
|
19
|
-
try {
|
|
20
|
-
const apiRoot = sdk.parameters.instance.apiRoot;
|
|
21
|
-
const portal = sdk.parameters.invocation.portal;
|
|
22
|
-
const pageId = sdk.parameters.invocation.entryId;
|
|
23
|
-
const date = sdk.parameters.invocation.date;
|
|
24
|
-
const teasermanagerUrl = `${apiRoot}/api/findStateForPage?project=${portal}&page=${pageId}&date=${date.toISOString()}`;
|
|
25
|
-
const response = await fetch(teasermanagerUrl).then(res => res.json());
|
|
26
|
-
|
|
27
|
-
if (response?.message) {
|
|
28
|
-
console.error(response.message);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return response?.data || {};
|
|
32
|
-
} catch (e) {
|
|
33
|
-
console.error(e);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
loadSlotStateForPage().then(setSlotState);
|
|
39
|
-
}, [sdk.parameters.invocation]);
|
|
40
|
-
|
|
41
|
-
return <div style={{
|
|
42
|
-
backgroundColor: "#FFFFFF",
|
|
43
|
-
minHeight: "100vh",
|
|
44
|
-
}}>
|
|
45
|
-
<NewestArticles sdk={sdk} onEntryClick={selectEntry} slotState={slotState} getArticleThumbnailUrl={getArticleThumbnailUrl} portals={portals}/>
|
|
46
|
-
{/*
|
|
47
|
-
<Tabs defaultTab="newest">
|
|
48
|
-
<Tabs.List>
|
|
49
|
-
<Tabs.Tab panelId="newest">Neuste Artikel</Tabs.Tab>
|
|
50
|
-
</Tabs.List>
|
|
51
|
-
<Tabs.Panel id="newest">
|
|
52
|
-
</Tabs.Panel>
|
|
53
|
-
</Tabs>
|
|
54
|
-
*/}
|
|
55
|
-
</div>;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
export default Dialog;
|
|
1
|
+
import React, {
|
|
2
|
+
useEffect, useState,
|
|
3
|
+
} from 'react';
|
|
4
|
+
import {NewestArticles} from "./Dialog/NewestArticles";
|
|
5
|
+
import {NoAccess} from "../NoAccess";
|
|
6
|
+
|
|
7
|
+
const Dialog = ({
|
|
8
|
+
sdk,
|
|
9
|
+
portals,
|
|
10
|
+
getArticleThumbnailUrl,
|
|
11
|
+
}) => {
|
|
12
|
+
const [slotState, setSlotState] = useState({});
|
|
13
|
+
|
|
14
|
+
const selectEntry = (entry) => {
|
|
15
|
+
sdk.close(entry);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const loadSlotStateForPage = async () => {
|
|
19
|
+
try {
|
|
20
|
+
const apiRoot = sdk.parameters.instance.apiRoot;
|
|
21
|
+
const portal = sdk.parameters.invocation.portal;
|
|
22
|
+
const pageId = sdk.parameters.invocation.entryId;
|
|
23
|
+
const date = sdk.parameters.invocation.date;
|
|
24
|
+
const teasermanagerUrl = `${apiRoot}/api/findStateForPage?project=${portal}&page=${pageId}&date=${date.toISOString()}`;
|
|
25
|
+
const response = await fetch(teasermanagerUrl).then(res => res.json());
|
|
26
|
+
|
|
27
|
+
if (response?.message) {
|
|
28
|
+
console.error(response.message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return response?.data || {};
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error(e);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
loadSlotStateForPage().then(setSlotState);
|
|
39
|
+
}, [sdk.parameters.invocation]);
|
|
40
|
+
|
|
41
|
+
return <div style={{
|
|
42
|
+
backgroundColor: "#FFFFFF",
|
|
43
|
+
minHeight: "100vh",
|
|
44
|
+
}}>
|
|
45
|
+
<NewestArticles sdk={sdk} onEntryClick={selectEntry} slotState={slotState} getArticleThumbnailUrl={getArticleThumbnailUrl} portals={portals}/>
|
|
46
|
+
{/*
|
|
47
|
+
<Tabs defaultTab="newest">
|
|
48
|
+
<Tabs.List>
|
|
49
|
+
<Tabs.Tab panelId="newest">Neuste Artikel</Tabs.Tab>
|
|
50
|
+
</Tabs.List>
|
|
51
|
+
<Tabs.Panel id="newest">
|
|
52
|
+
</Tabs.Panel>
|
|
53
|
+
</Tabs>
|
|
54
|
+
*/}
|
|
55
|
+
</div>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default Dialog;
|