@vonaffenfels/contentful-teasermanager 1.0.35 → 1.0.37

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vonaffenfels/contentful-teasermanager",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
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": "8531e3bdf7affdda47f286a5786cd7778044f1fc",
101
+ "gitHead": "d615f4feaf8cd8d3f04c0f102887cac6f5e0659f",
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
- return <div>DIALOG</div>
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.selectSingleEntry({contentTypes: ["article"]}).then(async (entry) => {
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,16 @@ 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?.[format(dotDate, "dd.MM.yyyy HH:mm")] || 0;
109
+ const stateChanged = timelineState?.[dotDate.toISOString()] || 0;
110
+ const currentDateCopy = new Date(currentDate);
111
+ currentDateCopy.setMilliseconds(0);
112
+ currentDateCopy.setSeconds(0);
109
113
 
110
114
  return <div
111
115
  key={index}
112
116
  className={classNames(styles.timelineDot, {
113
- [styles.timelineDotActive]: dotDate.getTime() === currentDate.getTime(),
114
- [styles.timelineDotChanged]: timelineState?.[format(dotDate, "dd.MM.yyyy HH:mm")],
117
+ [styles.timelineDotActive]: dotDate.getTime() === currentDateCopy.getTime(),
118
+ [styles.timelineDotChanged]: !!stateChanged,
115
119
  })}
116
120
  onClick={e => {
117
121
  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
- initContentfulClient(sdk);
19
+ if (props.initPortal && sdk.parameters?.invocation?.portal) {
20
+ props.initPortal(sdk.parameters.invocation.portal);
21
+ }
23
22
 
24
- const RendererWrapperComponent = props?.RendererWrapperComponent || (({children}) => <div>{children}</div>);
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,