@vonaffenfels/contentful-teasermanager 1.2.18 → 1.2.22

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.
Files changed (36) hide show
  1. package/.babelrc +19 -19
  2. package/.nvmrc +1 -1
  3. package/dist/TestStory.js +94 -0
  4. package/dist/TestStory2.js +94 -0
  5. package/dist/TestStory3.js +94 -0
  6. package/dist/index.html +11 -11
  7. package/dist/index.js +99285 -44296
  8. package/package.json +3 -3
  9. package/postcss.config.js +6 -6
  10. package/src/components/Contentful/ConfigScreen.js +154 -154
  11. package/src/components/Contentful/Dialog/LogicEditor.js +602 -533
  12. package/src/components/Contentful/Dialog/NewestArticles.js +198 -198
  13. package/src/components/Contentful/Dialog.js +87 -87
  14. package/src/components/Contentful/EntryEditor.js +196 -196
  15. package/src/components/Contentful/Page.js +9 -9
  16. package/src/components/NoAccess.js +12 -12
  17. package/src/components/Teasermanager/Timeline.js +179 -179
  18. package/src/components/Teasermanager/Timeline.module.css +89 -89
  19. package/src/components/Teasermanager.js +269 -269
  20. package/src/components/Teasermanager.module.css +137 -137
  21. package/src/dev.js +5 -5
  22. package/src/hooks/useDebounce.js +20 -20
  23. package/src/hooks/useOnScreen.js +23 -23
  24. package/src/icons/remove.svg +7 -7
  25. package/src/index.html +11 -11
  26. package/src/index.js +51 -51
  27. package/src/lib/contentfulClient.js +12 -12
  28. package/src/lib/runLoaders.js +46 -46
  29. package/src/queryFromLogic.js +33 -33
  30. package/src/scss/index.scss +11 -11
  31. package/tailwind.config.js +5 -5
  32. package/webpack.config.dev.js +25 -25
  33. package/webpack.config.js +174 -174
  34. package/dist/_base_slate-editor_src_dev_testComponents_TestStory2_js.js +0 -94
  35. package/dist/_base_slate-editor_src_dev_testComponents_TestStory3_js.js +0 -94
  36. package/dist/_base_slate-editor_src_dev_testComponents_TestStory_js.js +0 -94
@@ -1,534 +1,603 @@
1
- import {
2
- TextInput,
3
- Pill,
4
- SectionHeading,
5
- Checkbox,
6
- Button,
7
- DragHandle,
8
- } from "@contentful/f36-components";
9
- import {
10
- DndContext, useDraggable, useDroppable,
11
- PointerSensor, useSensor, useSensors,
12
- } from '@dnd-kit/core';
13
- import contentfulResolveResponse from "contentful-resolve-response";
14
- import {
15
- useEffect, useState,
16
- } from "react";
17
- import useDebounce from "../../../hooks/useDebounce";
18
- import {getContentfulClient} from "../../../lib/contentfulClient";
19
- import {PlusIcon} from "@contentful/f36-icons";
20
- import {CSS} from "@dnd-kit/utilities";
21
- import {queryFromLogic} from "../../../queryFromLogic";
22
- import {useQuery} from "@tanstack/react-query";
23
- export const LogicEditor = (props) => {
24
- const [selectedTags, setSelectedTags] = useState([]);
25
-
26
- useEffect(() => {
27
- setSelectedTags(props?.logicData?.tags || []);
28
- }, [props?.logicData?.tags]);
29
-
30
- const handleDragEnd = (event) => {
31
- const {
32
- over,
33
- active,
34
- } = event;
35
-
36
- if (over) {
37
- const draggedTagIndex = selectedTags.findIndex(tag => tag.id === active.id);
38
- if (draggedTagIndex !== -1) {
39
- const updatedTags = [...selectedTags];
40
- updatedTags[draggedTagIndex] = {
41
- ...updatedTags[draggedTagIndex],
42
- type: over.id, // "and" or "or"
43
- };
44
- setSelectedTags(updatedTags);
45
- }
46
- }
47
- };
48
-
49
- return <DndContext onDragEnd={handleDragEnd}>
50
- <LogicEditorInner {...props} selectedTags={selectedTags} setSelectedTags={setSelectedTags} />
51
- </DndContext>;
52
- };
53
-
54
- const LogicEditorInner = ({
55
- onSave = () => alert("onSave missing"),
56
- sdk,
57
- logicData,
58
- selectedTags,
59
- setSelectedTags,
60
- }) => {
61
- const contentfulClient = getContentfulClient();
62
- const [loading, setLoading] = useState(false);
63
- const [tags, setTags] = useState([]);
64
- const [categories, setCategories] = useState([]);
65
- const [searchQuery, setSearchQuery] = useState("");
66
- const [addTimepoint, setAddTimepoint] = useState(7);
67
- const debouncedSearch = useDebounce(searchQuery, 500);
68
- const [selectedTimepoints, setSelectedTimepoints] = useState([]);
69
- const {
70
- portal,
71
- locale = "de",
72
- } = sdk.parameters.invocation;
73
-
74
- const {data: articles} = useQuery({
75
- enabled: !!selectedTags.length && !!portal && !!contentfulClient,
76
- queryKey: ["articles", selectedTags, portal],
77
- queryFn: async () => {
78
- const query = queryFromLogic({
79
- tags: selectedTags,
80
- project: portal,
81
- });
82
-
83
- try {
84
- const result = await contentfulClient.getEntries({
85
- content_type: "article",
86
- locale: "de",
87
- order: "-fields.date",
88
- limit: 10,
89
- ...query,
90
- select: [
91
- "fields.title",
92
- "fields.slug",
93
- "fields.date",
94
- "sys.id",
95
- ].join(","),
96
- });
97
-
98
- return result;
99
- } catch (e) {
100
- console.error(e);
101
- return {items: []};
102
- }
103
- },
104
- });
105
-
106
- useEffect(() => {
107
- if (logicData?.timepoints) {
108
- setSelectedTimepoints(logicData?.timepoints);
109
- } else {
110
- setSelectedTimepoints([7, 12, 18]);
111
- }
112
- }, [logicData]);
113
- const {
114
- setNodeRef: setOrNodeRef,
115
- isOver: isOrOver,
116
- node: orNode,
117
- } = useDroppable({id: 'or'});
118
- const {
119
- setNodeRef: setAndNodeRef,
120
- isOver: isAndOver,
121
- node: andNode,
122
- } = useDroppable({id: "and"});
123
- const {
124
- setNodeRef: setExcludeNodeRef,
125
- isOver: isExcludeOver,
126
- node: excludeNode,
127
- } = useDroppable({id: "exclude"});
128
-
129
- useEffect(() => {
130
- setLoading(true);
131
- const params = {
132
- limit: 100,
133
- skip: 0,
134
- content_type: "tags",
135
- locale,
136
- order: "-sys.updatedAt",
137
- "fields.portal": portal,
138
- select: [
139
- "fields.title",
140
- "fields.isCategory",
141
- "fields.hidden",
142
- "sys.publishedAt",
143
- "sys.id",
144
- ].join(","),
145
- };
146
- const paramsNavigation = {
147
- limit: 1,
148
- content_type: "navigation",
149
- include: 2,
150
- locale,
151
- "fields.portal": portal,
152
- "fields.type": "main",
153
- };
154
-
155
- if (debouncedSearch) {
156
- params["fields.title[match]"] = debouncedSearch;
157
- }
158
-
159
- Promise.all([
160
- contentfulClient.getEntries(params),
161
- contentfulClient.getEntries(paramsNavigation).then(async (response) => {
162
- if (!response?.items?.[0]) {
163
- return [];
164
- }
165
-
166
- const references = await sdk.cma.entry.references({entryId: response.items[0].sys.id}, {include: 10});
167
- const resolved = contentfulResolveResponse(references);
168
- const allCategories = {};
169
-
170
- for (const category of resolved) {
171
- if (!category?.fields?.children?.de) {
172
- continue;
173
- }
174
-
175
- const children = category.fields.children.de || [];
176
-
177
- for (const child of children) {
178
- getAllTagsFromCategory(child, allCategories);
179
- }
180
- }
181
-
182
- return Object.values(allCategories).filter((c) => c.tags.length > 0);
183
- }),
184
- ]).then(([tags, categories]) => {
185
- setLoading(false);
186
- setTags(tags.items);
187
- setCategories(categories);
188
- });
189
- }, [debouncedSearch, portal, locale]);
190
-
191
- const andTags = selectedTags.filter((t) => t.type === "and");
192
- const orTags = selectedTags.filter((t) => t.type === "or");
193
- const excludeTags = selectedTags.filter((t) => t.type === "exclude");
194
-
195
- return <div className="flex flex-col gap-2 p-4">
196
- <div className="z-10 flex w-full flex-col gap-4 border-b border-gray-200 bg-white py-4">
197
- <div className="flex w-full flex-row items-center gap-2" style={{minHeight: 36}}>
198
- <SectionHeading style={{marginBottom: 0}}>Gewählte Zeitpunkte:</SectionHeading>
199
- <div className="flex flex-row items-center gap-2">
200
- {selectedTimepoints.map((timepoint) => {
201
- return <Pill
202
- key={timepoint}
203
- draggable={false}
204
- testId="pill-item"
205
- variant="active"
206
- label={`${timepoint} Uhr`}
207
- onClose={() => {
208
- setSelectedTimepoints(selectedTimepoints.filter((t) => t !== timepoint));
209
- }}
210
- />;
211
- })}
212
- <TextInput
213
- type="number"
214
- min={1}
215
- max={24}
216
- value={addTimepoint}
217
- onChange={(e) => {
218
- setAddTimepoint(e.target.value);
219
- }}
220
- />
221
- <SectionHeading style={{marginBottom: 0}}>Uhr</SectionHeading>
222
- <Button
223
- variant="secondary"
224
- onClick={() => {
225
- if (selectedTimepoints.includes(addTimepoint)) {
226
- return;
227
- }
228
-
229
- setSelectedTimepoints([...selectedTimepoints, addTimepoint]);
230
- }}
231
- >
232
- <PlusIcon />
233
- </Button>
234
- </div>
235
- </div>
236
- <div className="flex w-full flex-row items-center gap-2" style={{minHeight: 36}}>
237
- <SectionHeading style={{marginBottom: 0}}>Gewählte Kategorien & Tags:</SectionHeading>
238
- <div className="grid w-full grid-cols-5 gap-4">
239
- <div className="flex w-full flex-col gap-2">
240
- <div>
241
- <SectionHeading style={{marginBottom: 0}}>Oder</SectionHeading>
242
- </div>
243
- <div
244
- ref={setOrNodeRef}
245
- style={{
246
- minHeight: 36,
247
- backgroundColor: isOrOver ? "rgba(0, 255, 0, 0.3)" : "transparent",
248
- }}
249
- className="flex h-full w-full flex-row flex-wrap gap-2"
250
- >
251
- {orTags.map((tag) => {
252
- return <DraggablePill
253
- key={tag.id}
254
- id={tag.id}
255
- variant="active"
256
- label={(`${tag.label}`)}
257
- onClose={() => {
258
- setSelectedTags(selectedTags.filter((t) => t.id !== tag.id));
259
- }}
260
- />;
261
- })}
262
- </div>
263
- </div>
264
- <div className="flex w-full flex-col gap-2">
265
- <SectionHeading style={{marginBottom: 0}}>Und</SectionHeading>
266
- <div
267
- ref={setAndNodeRef}
268
- style={{
269
- minHeight: 36,
270
- backgroundColor: isAndOver ? "rgba(0, 255, 0, 0.3)" : "transparent",
271
- }}
272
- className="flex h-full w-full flex-row flex-wrap gap-2"
273
- >
274
- {andTags.map((tag) => {
275
- return <DraggablePill
276
- key={tag.id}
277
- id={tag.id}
278
- testId={tag.id}
279
- variant="active"
280
- label={(`${tag.label}`)}
281
- onClose={() => {
282
- setSelectedTags(selectedTags.filter((t) => t.id !== tag.id));
283
- }}
284
- />;
285
- })}
286
- </div>
287
- </div>
288
- <div className="flex w-full flex-col gap-2">
289
- <SectionHeading style={{marginBottom: 0}}>Ausschließen</SectionHeading>
290
- <div
291
- ref={setExcludeNodeRef}
292
- style={{
293
- minHeight: 36,
294
- backgroundColor: isExcludeOver ? "rgba(0, 255, 0, 0.3)" : "transparent",
295
- }}
296
- className="flex h-full w-full flex-row flex-wrap gap-2"
297
- >
298
- {excludeTags.map((tag) => {
299
- return <DraggablePill
300
- key={tag.id}
301
- id={tag.id}
302
- testId={tag.id}
303
- variant="active"
304
- label={(`${tag.label}`)}
305
- onClose={() => {
306
- setSelectedTags(selectedTags.filter((t) => t.id !== tag.id));
307
- }}
308
- />;
309
- })}
310
- </div>
311
- </div>
312
- <div className="col-span-2 flex w-full flex-col gap-4">
313
- <div>
314
- <SectionHeading style={{marginBottom: 0}}>Artikel</SectionHeading>
315
- </div>
316
- <div className="flex flex-col gap-4">
317
- <table className="w-full max-w-full table-auto gap-4 text-left">
318
- <thead>
319
- <tr>
320
- <th>Datum</th>
321
- <th className="pl-4">Titel</th>
322
- </tr>
323
- </thead>
324
- <tbody>
325
- {articles?.items?.map((article) => {
326
- return <tr key={article.sys.id}>
327
- <td className="whitespace-nowrap">
328
- {new Date(article.fields.date.de).toLocaleString("de-DE", {
329
- hour: "2-digit",
330
- minute: "2-digit",
331
- day: "2-digit",
332
- month: "2-digit",
333
- year: "numeric",
334
- })}
335
- </td>
336
- <td className="truncate whitespace-nowrap pl-4">
337
- {article.fields.title.de}
338
- </td>
339
- </tr>;
340
- })}
341
- </tbody>
342
- </table>
343
- </div>
344
- </div>
345
- </div>
346
- </div>
347
- <div className="flex w-full flex-row gap-2">
348
- <TextInput
349
- placeholder="Suche nach Kategorie oder Tag"
350
- value={searchQuery}
351
- onChange={(e) => setSearchQuery(e.target.value)}
352
- />
353
- </div>
354
- </div>
355
- <div className="grid grid-cols-2 gap-4 overflow-y-auto">
356
- <div className="flex flex-col gap-4">
357
- <div>
358
- <SectionHeading style={{marginBottom: 0}}>Kategorien</SectionHeading>
359
- </div>
360
- <div className="grid grid-cols-3 gap-4">
361
- {categories?.map((category) => {
362
- return (
363
- <Checkbox
364
- key={category.id}
365
- isChecked={selectedTags.some((t) => category.tags.some((c) => c.id === t.id))}
366
- onChange={(e) => {
367
- let newTags = [...selectedTags];
368
-
369
- if (e.target.checked) {
370
- for (const tag of category.tags) {
371
- if (!selectedTags.find((t) => t.id === tag.id)) {
372
- newTags.push({
373
- label: tag.label,
374
- id: tag.id,
375
- type: "or",
376
- });
377
- }
378
- }
379
- } else {
380
- for (const tag of category.tags) {
381
- newTags = newTags.filter((t) => t.id !== tag.id);
382
- }
383
- }
384
-
385
- setSelectedTags(newTags);
386
- }}
387
- >
388
- {category.title} ({selectedTags.filter((t) =>
389
- category.tags.some((c) => c.id === t.id)).length}/{category.tags.length})
390
- </Checkbox>
391
- );
392
- })}
393
- </div>
394
- </div>
395
- <div className="flex flex-col gap-4">
396
- <div>
397
- <SectionHeading style={{marginBottom: 0}}>Tags</SectionHeading>
398
- </div>
399
- <div className="grid grid-cols-3 gap-4">
400
- {tags?.map((tag) => {
401
- return (
402
- <Checkbox
403
- key={tag.sys.id}
404
- isChecked={!!selectedTags.find((t) => t.id === tag.sys.id)}
405
- onChange={(e) => {
406
- if (e.target.checked) {
407
- setSelectedTags([...selectedTags, {
408
- label: tag.fields.title[locale],
409
- id: tag.sys.id,
410
- type: "or",
411
- }]);
412
- } else {
413
- setSelectedTags(selectedTags.filter((t) => t.id !== tag.sys.id));
414
- }
415
- }}
416
- >
417
- {tag.fields.title[locale]}
418
- </Checkbox>
419
- );
420
- })}
421
- </div>
422
- </div>
423
- </div>
424
- <div className="sticky bottom-0 flex w-full flex-row justify-between gap-2 border-t border-gray-200 bg-white py-4">
425
- <Button
426
- variant="positive"
427
- type="button"
428
- onClick={() => onSave({
429
- tags: selectedTags,
430
- timepoints: selectedTimepoints,
431
- })}>Speichern</Button>
432
- <Button
433
- variant="negative"
434
- type="button"
435
- onClick={() => onSave({
436
- tags: [],
437
- timepoints: [],
438
- })}>Logik löschen</Button>
439
- </div>
440
- </div>;
441
- };
442
-
443
- function getAllTagsFromCategory(categoryNode, allCategories = {}, parentCategoryTitle = null) {
444
- if (!categoryNode || !categoryNode.fields?.children) {
445
- return allCategories;
446
- }
447
-
448
- const children = categoryNode.fields.children.de || [];
449
- const title = parentCategoryTitle ? `${parentCategoryTitle} > ${categoryNode.fields.title.de}` : categoryNode.fields.title.de;
450
-
451
- allCategories[categoryNode.sys.id] = {
452
- title,
453
- tags: [],
454
- id: categoryNode.sys.id,
455
- };
456
-
457
- if (categoryNode?.fields?.tags?.de) {
458
- for (const tag of categoryNode.fields.tags.de) {
459
- if (!tag?.fields?.title) {
460
- continue;
461
- }
462
-
463
- if (!allCategories[categoryNode.sys.id].tags.find((c) => c.id === tag.sys.id)) {
464
- allCategories[categoryNode.sys.id].tags.push({
465
- label: tag.fields.title.de,
466
- id: tag.sys.id,
467
- });
468
- }
469
- }
470
- }
471
-
472
- for (const child of children) {
473
- if (!child || !child.fields) {
474
- continue;
475
- }
476
-
477
- if (child?.fields?.tags?.de) {
478
- for (const tag of child.fields.tags.de) {
479
- if (!allCategories[categoryNode.sys.id].tags.find((c) => c.id === tag.sys.id)) {
480
- if (!tag?.fields?.title) {
481
- continue;
482
- }
483
-
484
- allCategories[categoryNode.sys.id].tags.push({
485
- label: tag.fields.title.de,
486
- id: tag.sys.id,
487
- });
488
- }
489
- }
490
- }
491
-
492
- getAllTagsFromCategory(child, allCategories, title);
493
- }
494
-
495
- allCategories[categoryNode.sys.id].tags = [...new Set(allCategories[categoryNode.sys.id].tags)];
496
-
497
- return allCategories;
498
- }
499
-
500
-
501
- function DraggablePill({
502
- id, ...props
503
- }) {
504
- const {
505
- attributes,
506
- listeners,
507
- setNodeRef,
508
- transform,
509
- transition,
510
- } = useDraggable({id});
511
- const style = {
512
- transform: CSS.Translate.toString(transform),
513
- transition,
514
- };
515
-
516
- return (
517
- <div>
518
- <Pill
519
- dragHandleComponent={
520
- <DragHandle
521
- label="Reorder item"
522
- variant="transparent"
523
- {...attributes}
524
- {...listeners}
525
- />
526
- }
527
- isDraggable
528
- ref={setNodeRef}
529
- style={style}
530
- {...props}
531
- />
532
- </div>
533
- );
1
+ import {
2
+ TextInput,
3
+ Pill,
4
+ SectionHeading,
5
+ Checkbox,
6
+ Button,
7
+ DragHandle,
8
+ } from "@contentful/f36-components";
9
+ import {
10
+ DndContext, useDraggable, useDroppable,
11
+ PointerSensor, useSensor, useSensors,
12
+ } from '@dnd-kit/core';
13
+ import contentfulResolveResponse from "contentful-resolve-response";
14
+ import {
15
+ useEffect, useState,
16
+ } from "react";
17
+ import useDebounce from "../../../hooks/useDebounce";
18
+ import {getContentfulClient} from "../../../lib/contentfulClient";
19
+ import {PlusIcon} from "@contentful/f36-icons";
20
+ import {CSS} from "@dnd-kit/utilities";
21
+ import {queryFromLogic} from "../../../queryFromLogic";
22
+ import {useQuery} from "@tanstack/react-query";
23
+ export const LogicEditor = (props) => {
24
+ const [selectedTags, setSelectedTags] = useState([]);
25
+
26
+ useEffect(() => {
27
+ setSelectedTags(props?.logicData?.tags || []);
28
+ }, [props?.logicData?.tags]);
29
+
30
+ const handleDragEnd = (event) => {
31
+ const {
32
+ over,
33
+ active,
34
+ } = event;
35
+
36
+ if (over) {
37
+ const draggedTagIndex = selectedTags.findIndex(tag => tag.id === active.id);
38
+ if (draggedTagIndex !== -1) {
39
+ const updatedTags = [...selectedTags];
40
+ updatedTags[draggedTagIndex] = {
41
+ ...updatedTags[draggedTagIndex],
42
+ type: over.id, // "and" or "or"
43
+ };
44
+ setSelectedTags(updatedTags);
45
+ }
46
+ }
47
+ };
48
+
49
+ return <DndContext onDragEnd={handleDragEnd}>
50
+ <LogicEditorInner {...props} selectedTags={selectedTags} setSelectedTags={setSelectedTags} />
51
+ </DndContext>;
52
+ };
53
+
54
+ const LogicEditorInner = ({
55
+ onSave = () => alert("onSave missing"),
56
+ sdk,
57
+ logicData,
58
+ selectedTags,
59
+ setSelectedTags,
60
+ }) => {
61
+ const contentfulClient = getContentfulClient();
62
+ const [loading, setLoading] = useState(false);
63
+ const [tags, setTags] = useState([]);
64
+ const [categories, setCategories] = useState([]);
65
+ const [searchQuery, setSearchQuery] = useState("");
66
+ const [addTimepoint, setAddTimepoint] = useState(7);
67
+ const debouncedSearch = useDebounce(searchQuery, 500);
68
+ const [selectedTimepoints, setSelectedTimepoints] = useState([]);
69
+ const {
70
+ portal,
71
+ locale = "de",
72
+ } = sdk.parameters.invocation;
73
+
74
+ const {data: articles} = useQuery({
75
+ enabled: !!selectedTags.length && !!portal && !!contentfulClient,
76
+ queryKey: ["articles", selectedTags, portal],
77
+ queryFn: async () => {
78
+ const query = queryFromLogic({
79
+ tags: selectedTags,
80
+ project: portal,
81
+ });
82
+
83
+ try {
84
+ const result = await contentfulClient.getEntries({
85
+ content_type: "article",
86
+ locale: "de",
87
+ order: "-fields.date",
88
+ limit: 10,
89
+ ...query,
90
+ select: [
91
+ "fields.title",
92
+ "fields.slug",
93
+ "fields.date",
94
+ "sys.id",
95
+ ].join(","),
96
+ });
97
+
98
+ return result;
99
+ } catch (e) {
100
+ console.error(e);
101
+ return {items: []};
102
+ }
103
+ },
104
+ });
105
+
106
+ useEffect(() => {
107
+ if (logicData?.timepoints) {
108
+ setSelectedTimepoints(logicData?.timepoints);
109
+ } else {
110
+ setSelectedTimepoints([7, 12, 18]);
111
+ }
112
+ }, [logicData]);
113
+ const {
114
+ setNodeRef: setOrNodeRef,
115
+ isOver: isOrOver,
116
+ node: orNode,
117
+ } = useDroppable({id: 'or'});
118
+ const {
119
+ setNodeRef: setAndNodeRef,
120
+ isOver: isAndOver,
121
+ node: andNode,
122
+ } = useDroppable({id: "and"});
123
+ const {
124
+ setNodeRef: setExcludeNodeRef,
125
+ isOver: isExcludeOver,
126
+ node: excludeNode,
127
+ } = useDroppable({id: "exclude"});
128
+
129
+ useEffect(() => {
130
+ setLoading(true);
131
+ const params = {
132
+ limit: 100,
133
+ skip: 0,
134
+ content_type: "tags",
135
+ locale,
136
+ order: "-sys.updatedAt",
137
+ "fields.portal": portal,
138
+ select: [
139
+ "fields.title",
140
+ "fields.isCategory",
141
+ "fields.hidden",
142
+ "sys.publishedAt",
143
+ "sys.id",
144
+ ].join(","),
145
+ };
146
+ const paramsNavigation = {
147
+ limit: 1,
148
+ content_type: "navigation",
149
+ include: 2,
150
+ locale,
151
+ "fields.portal": portal,
152
+ "fields.type": "main",
153
+ };
154
+
155
+ if (debouncedSearch) {
156
+ params["fields.title[match]"] = debouncedSearch;
157
+ }
158
+
159
+ Promise.all([
160
+ contentfulClient.getEntries(params),
161
+ contentfulClient.getEntries(paramsNavigation).then(async (response) => {
162
+ if (!response?.items?.[0]) {
163
+ return [];
164
+ }
165
+ const allCategories = {};
166
+
167
+ try {
168
+ // Use different include levels based on portal to avoid multilingual content bloat
169
+ const includeLevel = portal === 'YACHT' ? 1 : 2;
170
+ const references = await sdk.cma.entry.references({entryId: response.items[0].sys.id, include: includeLevel});
171
+ const resolved = contentfulResolveResponse(references);
172
+
173
+ for (const category of resolved) {
174
+ if (!category?.fields?.children?.de) {
175
+ continue;
176
+ }
177
+
178
+ const children = category.fields.children.de || [];
179
+
180
+ for (const child of children) {
181
+ await getAllTagsFromCategory(child, allCategories, null, portal, sdk);
182
+ }
183
+ }
184
+ } catch (e) {
185
+ console.error("Error getting categories and tags from references: ", e);
186
+ return [];
187
+ }
188
+
189
+ return Object.values(allCategories).filter((c) => c.tags.length > 0);
190
+ }),
191
+ ]).then(([tags, categories]) => {
192
+ setLoading(false);
193
+ setTags(tags.items);
194
+ setCategories(categories);
195
+ });
196
+ }, [debouncedSearch, portal, locale]);
197
+
198
+ const andTags = selectedTags.filter((t) => t.type === "and");
199
+ const orTags = selectedTags.filter((t) => t.type === "or");
200
+ const excludeTags = selectedTags.filter((t) => t.type === "exclude");
201
+
202
+ return <div className="flex flex-col gap-2 p-4">
203
+ <div className="z-10 flex w-full flex-col gap-4 border-b border-gray-200 bg-white py-4">
204
+ <div className="flex w-full flex-row items-center gap-2" style={{minHeight: 36}}>
205
+ <SectionHeading style={{marginBottom: 0}}>Gewählte Zeitpunkte:</SectionHeading>
206
+ <div className="flex flex-row items-center gap-2">
207
+ {selectedTimepoints.map((timepoint) => {
208
+ return <Pill
209
+ key={timepoint}
210
+ draggable={false}
211
+ testId="pill-item"
212
+ variant="active"
213
+ label={`${timepoint} Uhr`}
214
+ onClose={() => {
215
+ setSelectedTimepoints(selectedTimepoints.filter((t) => t !== timepoint));
216
+ }}
217
+ />;
218
+ })}
219
+ <TextInput
220
+ type="number"
221
+ min={1}
222
+ max={24}
223
+ value={addTimepoint}
224
+ onChange={(e) => {
225
+ setAddTimepoint(e.target.value);
226
+ }}
227
+ />
228
+ <SectionHeading style={{marginBottom: 0}}>Uhr</SectionHeading>
229
+ <Button
230
+ variant="secondary"
231
+ onClick={() => {
232
+ if (selectedTimepoints.includes(addTimepoint)) {
233
+ return;
234
+ }
235
+
236
+ setSelectedTimepoints([...selectedTimepoints, addTimepoint]);
237
+ }}
238
+ >
239
+ <PlusIcon />
240
+ </Button>
241
+ </div>
242
+ </div>
243
+ <div className="flex w-full flex-row items-center gap-2" style={{minHeight: 36}}>
244
+ <SectionHeading style={{marginBottom: 0}}>Gewählte Kategorien & Tags:</SectionHeading>
245
+ <div className="grid w-full grid-cols-5 gap-4">
246
+ <div className="flex w-full flex-col gap-2">
247
+ <div>
248
+ <SectionHeading style={{marginBottom: 0}}>Oder</SectionHeading>
249
+ </div>
250
+ <div
251
+ ref={setOrNodeRef}
252
+ style={{
253
+ minHeight: 36,
254
+ backgroundColor: isOrOver ? "rgba(0, 255, 0, 0.3)" : "transparent",
255
+ }}
256
+ className="flex h-full w-full flex-row flex-wrap gap-2"
257
+ >
258
+ {orTags.map((tag) => {
259
+ return <DraggablePill
260
+ key={tag.id}
261
+ id={tag.id}
262
+ variant="active"
263
+ label={(`${tag.label}`)}
264
+ onClose={() => {
265
+ setSelectedTags(selectedTags.filter((t) => t.id !== tag.id));
266
+ }}
267
+ />;
268
+ })}
269
+ </div>
270
+ </div>
271
+ <div className="flex w-full flex-col gap-2">
272
+ <SectionHeading style={{marginBottom: 0}}>Und</SectionHeading>
273
+ <div
274
+ ref={setAndNodeRef}
275
+ style={{
276
+ minHeight: 36,
277
+ backgroundColor: isAndOver ? "rgba(0, 255, 0, 0.3)" : "transparent",
278
+ }}
279
+ className="flex h-full w-full flex-row flex-wrap gap-2"
280
+ >
281
+ {andTags.map((tag) => {
282
+ return <DraggablePill
283
+ key={tag.id}
284
+ id={tag.id}
285
+ testId={tag.id}
286
+ variant="active"
287
+ label={(`${tag.label}`)}
288
+ onClose={() => {
289
+ setSelectedTags(selectedTags.filter((t) => t.id !== tag.id));
290
+ }}
291
+ />;
292
+ })}
293
+ </div>
294
+ </div>
295
+ <div className="flex w-full flex-col gap-2">
296
+ <SectionHeading style={{marginBottom: 0}}>Ausschließen</SectionHeading>
297
+ <div
298
+ ref={setExcludeNodeRef}
299
+ style={{
300
+ minHeight: 36,
301
+ backgroundColor: isExcludeOver ? "rgba(0, 255, 0, 0.3)" : "transparent",
302
+ }}
303
+ className="flex h-full w-full flex-row flex-wrap gap-2"
304
+ >
305
+ {excludeTags.map((tag) => {
306
+ return <DraggablePill
307
+ key={tag.id}
308
+ id={tag.id}
309
+ testId={tag.id}
310
+ variant="active"
311
+ label={(`${tag.label}`)}
312
+ onClose={() => {
313
+ setSelectedTags(selectedTags.filter((t) => t.id !== tag.id));
314
+ }}
315
+ />;
316
+ })}
317
+ </div>
318
+ </div>
319
+ <div className="col-span-2 flex w-full flex-col gap-4">
320
+ <div>
321
+ <SectionHeading style={{marginBottom: 0}}>Artikel</SectionHeading>
322
+ </div>
323
+ <div className="flex flex-col gap-4">
324
+ <table className="w-full max-w-full table-auto gap-4 text-left">
325
+ <thead>
326
+ <tr>
327
+ <th>Datum</th>
328
+ <th className="pl-4">Titel</th>
329
+ </tr>
330
+ </thead>
331
+ <tbody>
332
+ {articles?.items?.map((article) => {
333
+ return <tr key={article.sys.id}>
334
+ <td className="whitespace-nowrap">
335
+ {new Date(article.fields.date.de).toLocaleString("de-DE", {
336
+ hour: "2-digit",
337
+ minute: "2-digit",
338
+ day: "2-digit",
339
+ month: "2-digit",
340
+ year: "numeric",
341
+ })}
342
+ </td>
343
+ <td className="truncate whitespace-nowrap pl-4">
344
+ {article.fields.title.de}
345
+ </td>
346
+ </tr>;
347
+ })}
348
+ </tbody>
349
+ </table>
350
+ </div>
351
+ </div>
352
+ </div>
353
+ </div>
354
+ <div className="flex w-full flex-row gap-2">
355
+ <TextInput
356
+ placeholder="Suche nach Kategorie oder Tag"
357
+ value={searchQuery}
358
+ onChange={(e) => setSearchQuery(e.target.value)}
359
+ />
360
+ </div>
361
+ </div>
362
+ <div className="grid grid-cols-2 gap-4 overflow-y-auto">
363
+ <div className="flex flex-col gap-4">
364
+ <div>
365
+ <SectionHeading style={{marginBottom: 0}}>Kategorien</SectionHeading>
366
+ </div>
367
+ <div className="grid grid-cols-3 gap-4">
368
+ {categories?.map((category) => {
369
+ return (
370
+ <Checkbox
371
+ key={category.id}
372
+ isChecked={selectedTags.some((t) => category.tags.some((c) => c.id === t.id))}
373
+ onChange={(e) => {
374
+ let newTags = [...selectedTags];
375
+
376
+ if (e.target.checked) {
377
+ for (const tag of category.tags) {
378
+ if (!selectedTags.find((t) => t.id === tag.id)) {
379
+ newTags.push({
380
+ label: tag.label,
381
+ id: tag.id,
382
+ type: "or",
383
+ });
384
+ }
385
+ }
386
+ } else {
387
+ for (const tag of category.tags) {
388
+ newTags = newTags.filter((t) => t.id !== tag.id);
389
+ }
390
+ }
391
+
392
+ setSelectedTags(newTags);
393
+ }}
394
+ >
395
+ {category.title} ({selectedTags.filter((t) =>
396
+ category.tags.some((c) => c.id === t.id)).length}/{category.tags.length})
397
+ </Checkbox>
398
+ );
399
+ })}
400
+ </div>
401
+ </div>
402
+ <div className="flex flex-col gap-4">
403
+ <div>
404
+ <SectionHeading style={{marginBottom: 0}}>Tags</SectionHeading>
405
+ </div>
406
+ <div className="grid grid-cols-3 gap-4">
407
+ {tags?.map((tag) => {
408
+ return (
409
+ <Checkbox
410
+ key={tag.sys.id}
411
+ isChecked={!!selectedTags.find((t) => t.id === tag.sys.id)}
412
+ onChange={(e) => {
413
+ if (e.target.checked) {
414
+ setSelectedTags([...selectedTags, {
415
+ label: tag.fields.title[locale],
416
+ id: tag.sys.id,
417
+ type: "or",
418
+ }]);
419
+ } else {
420
+ setSelectedTags(selectedTags.filter((t) => t.id !== tag.sys.id));
421
+ }
422
+ }}
423
+ >
424
+ {tag.fields.title[locale]}
425
+ </Checkbox>
426
+ );
427
+ })}
428
+ </div>
429
+ </div>
430
+ </div>
431
+ <div className="sticky bottom-0 flex w-full flex-row justify-between gap-2 border-t border-gray-200 bg-white py-4">
432
+ <Button
433
+ variant="positive"
434
+ type="button"
435
+ onClick={() => onSave({
436
+ tags: selectedTags,
437
+ timepoints: selectedTimepoints,
438
+ })}>Speichern</Button>
439
+ <Button
440
+ variant="negative"
441
+ type="button"
442
+ onClick={() => onSave({
443
+ tags: [],
444
+ timepoints: [],
445
+ })}>Logik löschen</Button>
446
+ </div>
447
+ </div>;
448
+ };
449
+
450
+ async function getAllTagsFromCategory(categoryNode, allCategories = {}, parentCategoryTitle = null, portal = null, sdk = null) {
451
+ if (!categoryNode || !categoryNode.fields?.children) {
452
+ return allCategories;
453
+ }
454
+
455
+ const children = categoryNode.fields.children.de || [];
456
+ const title = parentCategoryTitle ? `${parentCategoryTitle} > ${categoryNode.fields.title.de}` : categoryNode.fields.title.de;
457
+
458
+ allCategories[categoryNode.sys.id] = {
459
+ title,
460
+ tags: [],
461
+ id: categoryNode.sys.id,
462
+ };
463
+
464
+ if (categoryNode?.fields?.tags?.de) {
465
+ const tagRefs = categoryNode.fields.tags.de;
466
+
467
+ if (portal === 'YACHT') {
468
+ // For YACHT portal, fetch all tag titles at once for better performance
469
+ const tagIds = tagRefs.map(tag => tag.sys.id).filter(id => id);
470
+
471
+ if (tagIds.length > 0) {
472
+ try {
473
+ const tagPromises = tagIds.map(tagId =>
474
+ sdk.cma.entry.get({entryId: tagId}).catch(() => null),
475
+ );
476
+ const fetchedTags = await Promise.all(tagPromises);
477
+
478
+ fetchedTags.forEach((fetchedTag, index) => {
479
+ if (fetchedTag?.fields?.title?.de) {
480
+ const tagId = tagIds[index];
481
+ if (!allCategories[categoryNode.sys.id].tags.find((c) => c.id === tagId)) {
482
+ allCategories[categoryNode.sys.id].tags.push({
483
+ label: fetchedTag.fields.title.de,
484
+ id: tagId,
485
+ });
486
+ }
487
+ }
488
+ });
489
+ } catch (e) {
490
+ console.error("Error fetching tags for YACHT portal:", e);
491
+ }
492
+ }
493
+ } else {
494
+ // For non-YACHT portals, tags should have fields already included
495
+ for (const tag of tagRefs) {
496
+ if (!tag?.fields?.title) {
497
+ continue;
498
+ }
499
+
500
+ if (!allCategories[categoryNode.sys.id].tags.find((c) => c.id === tag.sys.id)) {
501
+ allCategories[categoryNode.sys.id].tags.push({
502
+ label: tag.fields.title.de,
503
+ id: tag.sys.id,
504
+ });
505
+ }
506
+ }
507
+ }
508
+ }
509
+
510
+ for (const child of children) {
511
+ if (!child || !child.fields) {
512
+ continue;
513
+ }
514
+
515
+ if (child?.fields?.tags?.de) {
516
+ const childTagRefs = child.fields.tags.de;
517
+
518
+ if (portal === 'YACHT') {
519
+ // For YACHT portal, fetch child tag titles
520
+ const childTagIds = childTagRefs.map(tag => tag.sys.id).filter(id => id);
521
+
522
+ if (childTagIds.length > 0) {
523
+ try {
524
+ const childTagPromises = childTagIds.map(tagId =>
525
+ sdk.cma.entry.get({entryId: tagId}).catch(() => null),
526
+ );
527
+ const fetchedChildTags = await Promise.all(childTagPromises);
528
+
529
+ fetchedChildTags.forEach((fetchedTag, index) => {
530
+ if (fetchedTag?.fields?.title?.de) {
531
+ const tagId = childTagIds[index];
532
+ if (!allCategories[categoryNode.sys.id].tags.find((c) => c.id === tagId)) {
533
+ allCategories[categoryNode.sys.id].tags.push({
534
+ label: fetchedTag.fields.title.de,
535
+ id: tagId,
536
+ });
537
+ }
538
+ }
539
+ });
540
+ } catch (e) {
541
+ console.error("Error fetching child tags for YACHT portal:", e);
542
+ }
543
+ }
544
+ } else {
545
+ // For non-YACHT portals, child tags should have fields already included
546
+ for (const tag of childTagRefs) {
547
+ if (!allCategories[categoryNode.sys.id].tags.find((c) => c.id === tag.sys.id)) {
548
+ if (!tag?.fields?.title) {
549
+ continue;
550
+ }
551
+
552
+ allCategories[categoryNode.sys.id].tags.push({
553
+ label: tag.fields.title.de,
554
+ id: tag.sys.id,
555
+ });
556
+ }
557
+ }
558
+ }
559
+ }
560
+
561
+ await getAllTagsFromCategory(child, allCategories, title, portal, sdk);
562
+ }
563
+
564
+ allCategories[categoryNode.sys.id].tags = [...new Set(allCategories[categoryNode.sys.id].tags)];
565
+
566
+ return allCategories;
567
+ }
568
+
569
+
570
+ function DraggablePill({
571
+ id, ...props
572
+ }) {
573
+ const {
574
+ attributes,
575
+ listeners,
576
+ setNodeRef,
577
+ transform,
578
+ transition,
579
+ } = useDraggable({id});
580
+ const style = {
581
+ transform: CSS.Translate.toString(transform),
582
+ transition,
583
+ };
584
+
585
+ return (
586
+ <div>
587
+ <Pill
588
+ dragHandleComponent={
589
+ <DragHandle
590
+ label="Reorder item"
591
+ variant="transparent"
592
+ {...attributes}
593
+ {...listeners}
594
+ />
595
+ }
596
+ isDraggable
597
+ ref={setNodeRef}
598
+ style={style}
599
+ {...props}
600
+ />
601
+ </div>
602
+ );
534
603
  }