@vonaffenfels/contentful-teasermanager 1.0.4
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 +38 -0
- package/.nvmrc +1 -0
- package/package.json +105 -0
- package/postcss.config.js +7 -0
- package/src/components/Contentful/ConfigScreen.js +71 -0
- package/src/components/Contentful/Dialog.js +7 -0
- package/src/components/Contentful/EntryEditor.js +45 -0
- package/src/components/Contentful/Page.js +13 -0
- package/src/components/Teasermanager/Timeline.js +83 -0
- package/src/components/Teasermanager/Timeline.module.css +58 -0
- package/src/components/Teasermanager.js +81 -0
- package/src/components/Teasermanager.module.css +38 -0
- package/src/dev.js +4 -0
- package/src/hooks/useOnScreen.js +24 -0
- package/src/index.html +12 -0
- package/src/index.js +52 -0
- package/src/lib/contentfulClient.js +14 -0
- package/src/scss/dev.scss +0 -0
- package/src/scss/index.scss +12 -0
- package/tailwind.config.js +6 -0
- package/webpack.config.dev.js +26 -0
- package/webpack.config.js +146 -0
package/.babelrc
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"presets": [
|
|
3
|
+
"next/babel",
|
|
4
|
+
[
|
|
5
|
+
"@babel/preset-env",
|
|
6
|
+
{
|
|
7
|
+
"modules": false,
|
|
8
|
+
"targets": {
|
|
9
|
+
"browsers": [
|
|
10
|
+
"last 2 Chrome versions",
|
|
11
|
+
"last 2 Firefox versions",
|
|
12
|
+
"last 2 Safari versions",
|
|
13
|
+
"last 2 iOS versions",
|
|
14
|
+
"last 1 Android version",
|
|
15
|
+
"last 1 ChromeAndroid version",
|
|
16
|
+
"ie 11"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"@babel/preset-react"
|
|
22
|
+
],
|
|
23
|
+
"plugins": [
|
|
24
|
+
[
|
|
25
|
+
"@babel/plugin-proposal-private-property-in-object",
|
|
26
|
+
{
|
|
27
|
+
"loose": true
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
"@babel/plugin-proposal-class-properties",
|
|
32
|
+
{
|
|
33
|
+
"loose": true
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"@babel/plugin-syntax-dynamic-import"
|
|
37
|
+
]
|
|
38
|
+
}
|
package/.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
v14.16.1
|
package/package.json
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vonaffenfels/contentful-teasermanager",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"prepublish": "yarn run build",
|
|
6
|
+
"dev": "yarn run start",
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
8
|
+
"build": "echo \"Don't build this module!\"",
|
|
9
|
+
"link": "yarn link",
|
|
10
|
+
"start": "webpack serve --config webpack.config.dev.js"
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"@babel/core": "^7.20.12",
|
|
14
|
+
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
|
15
|
+
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
|
16
|
+
"@babel/preset-env": "^7.13.15",
|
|
17
|
+
"@babel/preset-react": "^7.13.13",
|
|
18
|
+
"@contentful/app-sdk": "^3.33.0",
|
|
19
|
+
"@contentful/field-editor-single-line": "^0.14.1",
|
|
20
|
+
"@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
|
+
"@svgr/webpack": "^5.5.0",
|
|
25
|
+
"babel-loader": "^8.2.2",
|
|
26
|
+
"classnames": "^2.3.2",
|
|
27
|
+
"clean-webpack-plugin": "^3.0.0",
|
|
28
|
+
"cloudinary": "^1.25.1",
|
|
29
|
+
"contentful-management": "^7.17.0",
|
|
30
|
+
"copy-webpack-plugin": "^8.1.1",
|
|
31
|
+
"cross-env": "^7.0.3",
|
|
32
|
+
"css-loader": "^5.2.4",
|
|
33
|
+
"cssnano": "^5.0.1",
|
|
34
|
+
"date-fns": "^2.30.0",
|
|
35
|
+
"dotenv-webpack": "^7.0.2",
|
|
36
|
+
"file-loader": "^6.2.0",
|
|
37
|
+
"html-webpack-plugin": "^5.3.1",
|
|
38
|
+
"mini-css-extract-plugin": "^1.5.0",
|
|
39
|
+
"postcss": "8.4.14",
|
|
40
|
+
"postcss-loader": "^5.2.0",
|
|
41
|
+
"raw-loader": "^4.0.2",
|
|
42
|
+
"react": "18.2.0",
|
|
43
|
+
"react-dom": "18.2.0",
|
|
44
|
+
"react-uid": "^2.3.1",
|
|
45
|
+
"regenerator-runtime": "^0.14.0",
|
|
46
|
+
"sass": "^1.32.11",
|
|
47
|
+
"sass-loader": "^11.0.1",
|
|
48
|
+
"speakingurl": "^14.0.1",
|
|
49
|
+
"style-loader": "^2.0.0",
|
|
50
|
+
"tailwindcss": "3.3.2",
|
|
51
|
+
"url-loader": "^4.1.1",
|
|
52
|
+
"webpack-cli": "^4.6.0",
|
|
53
|
+
"webpack-dev-server": "^4.0.0-beta.2"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@babel/core": "^7.20.12",
|
|
57
|
+
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
|
58
|
+
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
|
59
|
+
"@babel/preset-env": "^7.13.15",
|
|
60
|
+
"@babel/preset-react": "^7.13.13",
|
|
61
|
+
"@contentful/app-sdk": "^3.33.0",
|
|
62
|
+
"@contentful/field-editor-single-line": "^0.14.1",
|
|
63
|
+
"@contentful/field-editor-test-utils": "^0.11.1",
|
|
64
|
+
"@contentful/forma-36-fcss": "^0.3.1",
|
|
65
|
+
"@contentful/forma-36-react-components": "^3.88.3",
|
|
66
|
+
"@contentful/forma-36-tokens": "^0.10.1",
|
|
67
|
+
"@svgr/webpack": "^5.5.0",
|
|
68
|
+
"babel-loader": "^8.2.2",
|
|
69
|
+
"classnames": "^2.3.2",
|
|
70
|
+
"clean-webpack-plugin": "^3.0.0",
|
|
71
|
+
"cloudinary": "^1.25.1",
|
|
72
|
+
"contentful-management": "^7.17.0",
|
|
73
|
+
"copy-webpack-plugin": "^8.1.1",
|
|
74
|
+
"cross-env": "^7.0.3",
|
|
75
|
+
"css-loader": "^5.2.4",
|
|
76
|
+
"cssnano": "^5.0.1",
|
|
77
|
+
"date-fns": "^2.30.0",
|
|
78
|
+
"dotenv-webpack": "^7.0.2",
|
|
79
|
+
"file-loader": "^6.2.0",
|
|
80
|
+
"html-webpack-plugin": "^5.3.1",
|
|
81
|
+
"mini-css-extract-plugin": "^1.5.0",
|
|
82
|
+
"postcss": "8.4.14",
|
|
83
|
+
"postcss-loader": "^5.2.0",
|
|
84
|
+
"raw-loader": "^4.0.2",
|
|
85
|
+
"react": "18.2.0",
|
|
86
|
+
"react-dom": "18.2.0",
|
|
87
|
+
"react-uid": "^2.3.1",
|
|
88
|
+
"regenerator-runtime": "^0.14.0",
|
|
89
|
+
"sass": "^1.32.11",
|
|
90
|
+
"sass-loader": "^11.0.1",
|
|
91
|
+
"speakingurl": "^14.0.1",
|
|
92
|
+
"style-loader": "^2.0.0",
|
|
93
|
+
"tailwindcss": "3.3.2",
|
|
94
|
+
"url-loader": "^4.1.1",
|
|
95
|
+
"webpack-cli": "^4.6.0",
|
|
96
|
+
"webpack-dev-server": "^4.0.0-beta.2"
|
|
97
|
+
},
|
|
98
|
+
"dependencies": {
|
|
99
|
+
"webpack": "5.73.0"
|
|
100
|
+
},
|
|
101
|
+
"gitHead": "ba6d3fea2cca04a822de7e3d7dff5949e62a9d46",
|
|
102
|
+
"publishConfig": {
|
|
103
|
+
"access": "public"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, {Component} from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Heading, Form, TextField, Workbench, Paragraph,
|
|
4
|
+
} from '@contentful/forma-36-react-components';
|
|
5
|
+
import {css} from 'emotion';
|
|
6
|
+
|
|
7
|
+
export default class Config extends Component {
|
|
8
|
+
|
|
9
|
+
constructor(props) {
|
|
10
|
+
super(props);
|
|
11
|
+
this.state = {parameters: {}};
|
|
12
|
+
|
|
13
|
+
// `onConfigure` allows to configure a callback to be
|
|
14
|
+
// invoked when a user attempts to install the app or update
|
|
15
|
+
// its configuration.
|
|
16
|
+
props.sdk.app.onConfigure(() => this.onConfigure());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async componentDidMount() {
|
|
20
|
+
// Get current parameters of the app.
|
|
21
|
+
// If the app is not installed yet, `parameters` will be `null`.
|
|
22
|
+
const parameters = await this.props.sdk.app.getParameters();
|
|
23
|
+
|
|
24
|
+
this.setState(parameters ? {parameters} : this.state, () => {
|
|
25
|
+
// Once preparation has finished, call `setReady` to hide
|
|
26
|
+
// the loading screen and present the app to a user.
|
|
27
|
+
this.props.sdk.app.setReady();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async onConfigure() {
|
|
32
|
+
// This method will be called when a user clicks on "Install"
|
|
33
|
+
// or "Save" in the configuration screen.
|
|
34
|
+
// for more details see https://www.contentful.com/developers/docs/extensibility/ui-extensions/sdk-reference/#register-an-app-configuration-hook
|
|
35
|
+
|
|
36
|
+
// Get current the state of EditorInterface and other entities
|
|
37
|
+
// related to this app installation
|
|
38
|
+
const currentState = await this.props.sdk.app.getCurrentState();
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
// Parameters to be persisted as the app configuration.
|
|
42
|
+
parameters: this.state.parameters,
|
|
43
|
+
// In case you don't want to submit any update to app
|
|
44
|
+
// locations, you can just pass the currentState as is
|
|
45
|
+
targetState: currentState,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
onFieldChanged(value, field) {
|
|
50
|
+
this.setState({
|
|
51
|
+
parameters: {
|
|
52
|
+
...this.state.parameters,
|
|
53
|
+
[field]: value,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
render() {
|
|
59
|
+
return (
|
|
60
|
+
<Workbench className={css({
|
|
61
|
+
padding: '80px',
|
|
62
|
+
backgroundColor: "#FFF",
|
|
63
|
+
})}>
|
|
64
|
+
<Form>
|
|
65
|
+
<Heading>No Configuration necessary</Heading>
|
|
66
|
+
</Form>
|
|
67
|
+
</Workbench>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
useEffect, useState,
|
|
3
|
+
} from 'react';
|
|
4
|
+
import {Teasermanager} from "../Teasermanager";
|
|
5
|
+
|
|
6
|
+
const Entry = ({sdk}) => {
|
|
7
|
+
const contentField = sdk.parameters.instance.contentField;
|
|
8
|
+
const [locale, setLocale] = useState(sdk.locales.default);
|
|
9
|
+
const [portal, setPortal] = useState();
|
|
10
|
+
const portalField = sdk?.entry?.fields?.portal;
|
|
11
|
+
|
|
12
|
+
portalField?.onValueChanged(() => {
|
|
13
|
+
if (portalField.getValue() !== portal) {
|
|
14
|
+
setPortal(portalField.getValue());
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const detachHandler = sdk.editor.onLocaleSettingsChanged((loc) => {
|
|
20
|
+
setLocale(loc?.focused || sdk.locales.default);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return () => {
|
|
24
|
+
detachHandler();
|
|
25
|
+
};
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
const onSlotClick = (slotId, currentDate) => {
|
|
29
|
+
console.log("KIENZ_DEBUG: :31 / onSlotClick", {
|
|
30
|
+
slotId,
|
|
31
|
+
currentDate,
|
|
32
|
+
});
|
|
33
|
+
sdk.dialogs.selectSingleEntry({contentTypes: ["article"]}).then((entry) => {
|
|
34
|
+
if (!entry) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(`Selected entry ${entry.sys.id} for slot ${slotId} and date ${currentDate}`);
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return <Teasermanager entryId={sdk.entry.getSys().id} locale={locale} contentFieldName={contentField} onSlotClick={onSlotClick}/>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default Entry;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Teasermanager} from "../Teasermanager";
|
|
3
|
+
|
|
4
|
+
const Page = ({sdk}) => {
|
|
5
|
+
|
|
6
|
+
const onSlotClick = (slotId, currentDate) => {
|
|
7
|
+
console.log("KIENZ_DEBUG: :7 / onSlotClick", {sdk});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return <Teasermanager entryId={"4RvZ6fcUtBIgw2Hw5YILPs"} onSlotClick={onSlotClick} />;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default Page;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useEffect, useState,
|
|
3
|
+
} from "react";
|
|
4
|
+
import styles from "./Timeline.module.css";
|
|
5
|
+
import format from "date-fns/format";
|
|
6
|
+
import subtract from "date-fns/sub";
|
|
7
|
+
import classNames from "classnames";
|
|
8
|
+
|
|
9
|
+
export const Timeline = ({
|
|
10
|
+
currentDate,
|
|
11
|
+
setCurrentDate,
|
|
12
|
+
}) => {
|
|
13
|
+
const stepsInRange = (24 * 60) / 15 + 1; // must be uneven so we have a center ;)
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const currentDate = new Date();
|
|
17
|
+
currentDate.setMinutes((Math.round(currentDate.getMinutes() / 15) * 15) % 60);
|
|
18
|
+
|
|
19
|
+
setCurrentDate(currentDate);
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
const leftDate = currentDate && subtract(currentDate, {minutes: stepsInRange / 2 * 15});
|
|
23
|
+
if (leftDate) {
|
|
24
|
+
leftDate.setMinutes((Math.round(currentDate.getMinutes() / 15) * 15) % 60);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const handleDateChange = (e) => {
|
|
28
|
+
if (!e.target.value) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let newDate = new Date(e.target.value);
|
|
33
|
+
|
|
34
|
+
newDate.setHours(currentDate.getHours(), currentDate.getMinutes());
|
|
35
|
+
|
|
36
|
+
setCurrentDate(newDate);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleTimeChange = e => {
|
|
40
|
+
if (!e.target.value) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let newDate = new Date(currentDate);
|
|
45
|
+
|
|
46
|
+
let [hours, minutes] = e.target.value.split(":");
|
|
47
|
+
|
|
48
|
+
newDate.setHours(hours, minutes);
|
|
49
|
+
|
|
50
|
+
setCurrentDate(newDate);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return <div className="flex w-full">
|
|
54
|
+
<div className={styles.wrapper}>
|
|
55
|
+
<div className={styles.date}>
|
|
56
|
+
{!!currentDate && (
|
|
57
|
+
<>
|
|
58
|
+
<input type="date" value={format(currentDate, "yyyy-MM-dd")} onChange={handleDateChange} className="mr-4" />
|
|
59
|
+
<input type="time" value={format(currentDate, "HH:mm")} onChange={handleTimeChange} />
|
|
60
|
+
</>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
<div className={styles.timeline}>
|
|
64
|
+
{!!leftDate && Array(stepsInRange).fill(0).map((_, index) => {
|
|
65
|
+
const dotDate = new Date(leftDate.getTime() + (15 * 60 * 1000 * (index + 1)));
|
|
66
|
+
|
|
67
|
+
return <div
|
|
68
|
+
key={index}
|
|
69
|
+
className={classNames(styles.timelineDot, {[styles.timelineDotActive]: dotDate.getTime() === currentDate.getTime()})}
|
|
70
|
+
onClick={e => {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
setCurrentDate(dotDate);
|
|
73
|
+
return false;
|
|
74
|
+
}}>
|
|
75
|
+
<div className={classNames(styles.timelineDotLabel, {[styles.timelineDotLabelPermanent]: dotDate.getHours() % 6 === 0 && dotDate.getMinutes() === 0})}>
|
|
76
|
+
{leftDate && format(dotDate, "dd.MM.yyyy HH:mm")}
|
|
77
|
+
</div>
|
|
78
|
+
</div>;
|
|
79
|
+
})}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>;
|
|
83
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
.wrapper {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
position: relative;
|
|
5
|
+
width: 100%;
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.timeline {
|
|
10
|
+
background-color: #e1fff9;
|
|
11
|
+
height: auto;
|
|
12
|
+
padding: 6px 0 6px 0;
|
|
13
|
+
width: 100%;
|
|
14
|
+
display: flex;
|
|
15
|
+
font-weight: bold;
|
|
16
|
+
font-size: 1em;
|
|
17
|
+
justify-content: space-between;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.timeline-dot {
|
|
21
|
+
display: block;
|
|
22
|
+
width: 10px;
|
|
23
|
+
height: 10px;
|
|
24
|
+
background-color: #004f73;
|
|
25
|
+
border-radius: 9999px;
|
|
26
|
+
cursor: pointer;
|
|
27
|
+
position: relative;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.timeline-dot:hover .timeline-dot-label {
|
|
31
|
+
display: block;
|
|
32
|
+
z-index: 100;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.timeline-dot-active {
|
|
36
|
+
outline: 2px solid red;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.timeline-dot-label.timeline-dot-label-permanent {
|
|
40
|
+
display: block;
|
|
41
|
+
z-index: 10;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.timeline-dot-label {
|
|
45
|
+
display: none;
|
|
46
|
+
background: white;
|
|
47
|
+
position: absolute;
|
|
48
|
+
top: -20px;
|
|
49
|
+
left: -100px;
|
|
50
|
+
width: 200px;
|
|
51
|
+
text-align: center;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.date {
|
|
55
|
+
width: 100%;
|
|
56
|
+
text-align: center;
|
|
57
|
+
padding: 1em 0 1em 0;
|
|
58
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React, {useEffect, useRef, useState} from "react";
|
|
2
|
+
|
|
3
|
+
import {getContentfulClient} from "../lib/contentfulClient";
|
|
4
|
+
import Renderer from "@vonaffenfels/slate-editor/dist/Renderer";
|
|
5
|
+
import componentLoader from "@vonaffenfels/slate-editor/componentLoader";
|
|
6
|
+
import {Timeline} from "./Teasermanager/Timeline";
|
|
7
|
+
import styles from "./Teasermanager.module.css";
|
|
8
|
+
|
|
9
|
+
export const Teasermanager = ({entryId, onSlotClick = () => console.error("missing onSlotClick"), contentFieldName = "wysiwyg", locale = "en-US"}) => {
|
|
10
|
+
const contentfulClient = getContentfulClient();
|
|
11
|
+
const [entry, setEntry] = useState(null);
|
|
12
|
+
const wrapperRef = useRef(null);
|
|
13
|
+
const [currentDate, setCurrentDate] = useState(null);
|
|
14
|
+
|
|
15
|
+
const initSlot = (node) => {
|
|
16
|
+
if (node.dataset.__teasermanager_slot_initialized) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
node.dataset.__teasermanager_slot_initialized = true;
|
|
21
|
+
|
|
22
|
+
const managementNode = document.createElement("div");
|
|
23
|
+
managementNode.classList.add(styles.management);
|
|
24
|
+
|
|
25
|
+
const managementLabelNode = document.createElement("div");
|
|
26
|
+
managementLabelNode.classList.add(styles.label);
|
|
27
|
+
managementLabelNode.innerHTML = `Klicken um den Artikel zu wechseln`;
|
|
28
|
+
|
|
29
|
+
managementNode.appendChild(managementLabelNode);
|
|
30
|
+
|
|
31
|
+
managementNode.addEventListener("click", (e) => {
|
|
32
|
+
const slotId = node.dataset.teasermanagerSlot;
|
|
33
|
+
onSlotClick(slotId, currentDate);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
node.appendChild(managementNode);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const updateSlots = (mutationList) => {
|
|
40
|
+
const slots = wrapperRef.current.querySelectorAll(`*[data-teasermanager-slot]`);
|
|
41
|
+
slots.forEach(initSlot);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!entryId && entry) {
|
|
46
|
+
setEntry(null);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
contentfulClient.getEntry(entryId).then((loadedEntry, b) => {
|
|
51
|
+
setEntry(loadedEntry);
|
|
52
|
+
});
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!wrapperRef.current) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const observer = new MutationObserver(updateSlots);
|
|
61
|
+
observer.observe(wrapperRef.current, {attributes: false, childList: true, subtree: true});
|
|
62
|
+
|
|
63
|
+
return () => {
|
|
64
|
+
observer.disconnect();
|
|
65
|
+
}
|
|
66
|
+
}, [wrapperRef, currentDate]);
|
|
67
|
+
|
|
68
|
+
return <div className="w-full flex flex-col">
|
|
69
|
+
<div className="w-full">
|
|
70
|
+
<Timeline currentDate={currentDate} setCurrentDate={setCurrentDate} />
|
|
71
|
+
</div>
|
|
72
|
+
<div className={styles.wrapper} ref={wrapperRef}>
|
|
73
|
+
{!!entry?.fields?.[contentFieldName]?.[locale] &&
|
|
74
|
+
<Renderer
|
|
75
|
+
value={entry?.fields?.[contentFieldName]?.[locale]}
|
|
76
|
+
storybookComponentLoader={componentLoader}/>}
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
.wrapper *[data-teasermanager-slot] {
|
|
2
|
+
position: relative;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.management {
|
|
6
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
7
|
+
|
|
8
|
+
background-color: rgba(255, 255, 255, 0.4);
|
|
9
|
+
border: 1px dashed black;
|
|
10
|
+
color: black;
|
|
11
|
+
text-align: center;
|
|
12
|
+
vertical-align: middle;
|
|
13
|
+
position: absolute;
|
|
14
|
+
top: 0;
|
|
15
|
+
left: 0;
|
|
16
|
+
width: 100%;
|
|
17
|
+
height: 100%;
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
|
|
20
|
+
display: flex;
|
|
21
|
+
justify-items: center;
|
|
22
|
+
align-items: center;
|
|
23
|
+
padding: 2em;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.management:hover {
|
|
27
|
+
background-color: rgba(255, 255, 255, 0.9);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.label {
|
|
31
|
+
visibility: hidden;
|
|
32
|
+
font-weight: bold;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.management:hover .label {
|
|
36
|
+
visibility: visible;
|
|
37
|
+
}
|
|
38
|
+
|
package/src/dev.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
useState, useEffect, useRef,
|
|
3
|
+
} from "react";
|
|
4
|
+
|
|
5
|
+
export default function useOnScreen(ref) {
|
|
6
|
+
const [isIntersecting, setIntersecting] = useState(false);
|
|
7
|
+
const observerRef = useRef();
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!observerRef.current && ref.current) {
|
|
11
|
+
observerRef.current = new IntersectionObserver(([entry]) => setIntersecting(entry.isIntersecting));
|
|
12
|
+
|
|
13
|
+
observerRef.current.observe(ref.current);
|
|
14
|
+
|
|
15
|
+
return () => {
|
|
16
|
+
if (observerRef.current) {
|
|
17
|
+
observerRef.current.disconnect();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
return isIntersecting;
|
|
24
|
+
}
|
package/src/index.html
ADDED
package/src/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {createRoot} from 'react-dom/client';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
init,
|
|
6
|
+
locations,
|
|
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
|
+
import '@vonaffenfels/slate-editor/dist/index.css';
|
|
12
|
+
import './scss/index.scss';
|
|
13
|
+
|
|
14
|
+
import EntryEditor from './components/Contentful/EntryEditor';
|
|
15
|
+
import Config from './components/Contentful/ConfigScreen';
|
|
16
|
+
import Dialog from "./components/Contentful/Dialog";
|
|
17
|
+
import Page from "./components/Contentful/Page";
|
|
18
|
+
import {initContentfulClient} from "./lib/contentfulClient";
|
|
19
|
+
|
|
20
|
+
export const BaseContentfulApp = () => {
|
|
21
|
+
init((sdk) => {
|
|
22
|
+
const rootContainer = document.getElementById('root');
|
|
23
|
+
|
|
24
|
+
initContentfulClient(sdk);
|
|
25
|
+
|
|
26
|
+
const ComponentLocationSettings = [
|
|
27
|
+
{
|
|
28
|
+
location: locations.LOCATION_APP_CONFIG,
|
|
29
|
+
component: <Config sdk={sdk}/>,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
location: locations.LOCATION_ENTRY_EDITOR,
|
|
33
|
+
component: <EntryEditor sdk={sdk}/>,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
location: locations.LOCATION_DIALOG,
|
|
37
|
+
component: <Dialog sdk={sdk}/>,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
location: locations.LOCATION_PAGE,
|
|
41
|
+
component: <Page sdk={sdk}/>,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
ComponentLocationSettings.forEach((componentLocationSetting) => {
|
|
46
|
+
if (sdk.location.is(componentLocationSetting.location)) {
|
|
47
|
+
const root = createRoot(rootContainer); // createRoot(container!) if you use TypeScript
|
|
48
|
+
root.render(componentLocationSetting.component);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
let __contentfulClient;
|
|
2
|
+
|
|
3
|
+
export const getContentfulClient = () => {
|
|
4
|
+
if (!__contentfulClient) {
|
|
5
|
+
throw new Error(`Contentful client not initialized run initContentfulClient(sdk) first`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return __contentfulClient;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const initContentfulClient = (sdk) => {
|
|
12
|
+
__contentfulClient = sdk.space;
|
|
13
|
+
}
|
|
14
|
+
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
let config = require("./webpack.config")({
|
|
3
|
+
componentImportRoot: "@vonaffenfels/slate-editor/src/dev/testComponents",
|
|
4
|
+
relativePathToComponents: path.resolve(__dirname + "../../../@base/slate-editor/src/dev/testComponents"),
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
config.entry = {index: ["regenerator-runtime/runtime.js", './src/dev.js']};
|
|
9
|
+
|
|
10
|
+
config.mode = "development";
|
|
11
|
+
config.watch = true;
|
|
12
|
+
config.watchOptions = {ignored: ['dist/**', 'node_modules/**']};
|
|
13
|
+
config.cache = {type: "memory"};
|
|
14
|
+
|
|
15
|
+
config.output.publicPath = "/dist/";
|
|
16
|
+
config.devtool = 'inline-source-map';
|
|
17
|
+
|
|
18
|
+
config.devServer = {
|
|
19
|
+
static: path.join(__dirname, 'dist'),
|
|
20
|
+
compress: true,
|
|
21
|
+
historyApiFallback: true,
|
|
22
|
+
port: process.env.PORT || 5842,
|
|
23
|
+
devMiddleware: {writeToDisk: true},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
module.exports = config;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|
3
|
+
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
4
|
+
const webpack = require("webpack");
|
|
5
|
+
const {readFileSync} = require("fs");
|
|
6
|
+
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
|
|
7
|
+
|
|
8
|
+
module.exports = ({
|
|
9
|
+
babelConfig = JSON.parse(readFileSync("../../.babelrc").toString()),
|
|
10
|
+
componentImportRoot = "@vonaffenfels/slate-editor/src/dev/testComponents",
|
|
11
|
+
relativePathToComponents = path.resolve(__dirname + "../../../@base/slate-editor/src/dev/testComponents"),
|
|
12
|
+
postCssConfig = {
|
|
13
|
+
plugins: {
|
|
14
|
+
'postcss-import': {},
|
|
15
|
+
tailwindcss: {},
|
|
16
|
+
autoprefixer: {},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
transpilePaths = [],
|
|
20
|
+
}) => {
|
|
21
|
+
return {
|
|
22
|
+
mode: 'production',
|
|
23
|
+
entry: {index: ["regenerator-runtime/runtime.js", './src/index.js']},
|
|
24
|
+
module: {
|
|
25
|
+
rules: [
|
|
26
|
+
{
|
|
27
|
+
test: /componentLoader.js/i,
|
|
28
|
+
use: [
|
|
29
|
+
{
|
|
30
|
+
loader: "@vonaffenfels/slate-editor/componentLoader.js",
|
|
31
|
+
options: {
|
|
32
|
+
componentRoot: relativePathToComponents,
|
|
33
|
+
componentImportRoot: componentImportRoot,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
loader: 'babel-loader',
|
|
38
|
+
options: babelConfig,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
test: /\.js$/,
|
|
44
|
+
include: [
|
|
45
|
+
path.resolve(__dirname, 'src'),
|
|
46
|
+
relativePathToComponents,
|
|
47
|
+
...transpilePaths,
|
|
48
|
+
],
|
|
49
|
+
use: [
|
|
50
|
+
{
|
|
51
|
+
loader: 'babel-loader',
|
|
52
|
+
options: babelConfig,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
test: /\.s[ac]ss$/i,
|
|
58
|
+
include: path.resolve(__dirname, 'src', 'scss'),
|
|
59
|
+
use: [
|
|
60
|
+
"style-loader",
|
|
61
|
+
{loader: 'css-loader', options: {importLoaders: 1}},
|
|
62
|
+
{loader: 'postcss-loader', options: {postcssOptions: postCssConfig}},
|
|
63
|
+
"sass-loader",
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
test: /module\.css$/i,
|
|
68
|
+
use: [
|
|
69
|
+
'style-loader',
|
|
70
|
+
{
|
|
71
|
+
loader: 'css-loader',
|
|
72
|
+
options: {
|
|
73
|
+
importLoaders: 1,
|
|
74
|
+
modules: {exportLocalsConvention: "camelCase"},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{loader: 'postcss-loader', options: {postcssOptions: postCssConfig}},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
test: /\.css$/i,
|
|
82
|
+
exclude: [/module\.css$/i],
|
|
83
|
+
use: [
|
|
84
|
+
'style-loader',
|
|
85
|
+
{
|
|
86
|
+
loader: 'css-loader',
|
|
87
|
+
options: {importLoaders: 1},
|
|
88
|
+
},
|
|
89
|
+
{loader: 'postcss-loader', options: {postcssOptions: postCssConfig}},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
test: /\.svg$/,
|
|
94
|
+
resourceQuery: {not: [/url/]}, // exclude react component if *.svg?url
|
|
95
|
+
use: [
|
|
96
|
+
{
|
|
97
|
+
loader: '@svgr/webpack',
|
|
98
|
+
options: {
|
|
99
|
+
dimensions: false,
|
|
100
|
+
svgo: false,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
'url-loader',
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
test: /\.(woff(2)?|ttf|eot|png)(\?v=\d+\.\d+\.\d+)?$/,
|
|
108
|
+
use: [
|
|
109
|
+
{
|
|
110
|
+
loader: 'file-loader',
|
|
111
|
+
options: {
|
|
112
|
+
name: '[name].[ext]',
|
|
113
|
+
outputPath: 'fonts/',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
test: /\.(graphql|gql)$/,
|
|
120
|
+
loader: 'raw-loader',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
plugins: [
|
|
125
|
+
new CleanWebpackPlugin(),
|
|
126
|
+
new HtmlWebpackPlugin({template: path.join(__dirname, 'src', 'index.html')}),
|
|
127
|
+
new MiniCssExtractPlugin({}),
|
|
128
|
+
new webpack.DefinePlugin({'process.env': JSON.stringify(process.env)}),
|
|
129
|
+
],
|
|
130
|
+
resolve: {
|
|
131
|
+
extensions: ['.json', '.js', '.jsx'],
|
|
132
|
+
fallback: {
|
|
133
|
+
stream: false,
|
|
134
|
+
path: false,
|
|
135
|
+
timers: false,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
output: {
|
|
139
|
+
filename: '[name].js',
|
|
140
|
+
libraryTarget: "umd",
|
|
141
|
+
publicPath: "/",
|
|
142
|
+
globalObject: "this",
|
|
143
|
+
path: path.resolve(__dirname, 'dist'),
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
};
|