Added AppContext and basic Form component

This commit is contained in:
Simon Backx 2022-07-04 17:23:01 +02:00
parent fccc18f51c
commit 6f0defc6a1
9 changed files with 275 additions and 12 deletions

View File

@ -5,6 +5,7 @@
"repository": "git@github.com:TryGhost/comments-ui.git",
"author": "Ghost Foundation",
"dependencies": {
"@sentry/react": "^7.5.0",
"@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.2",
"@testing-library/user-event": "14.0.0",

View File

@ -1,11 +1,94 @@
import './App.css';
import Form from './components/Form';
import * as Sentry from '@sentry/react';
import React from 'react';
import ActionHandler from './actions';
import {createPopupNotification} from './utils/helpers';
import AppContext from './AppContext';
function App() {
function SentryErrorBoundary({dsn, children}) {
if (dsn) {
return (
<Sentry.ErrorBoundary>
{children}
</Sentry.ErrorBoundary>
);
}
return (
<div className="App">
Hello world
</div>
<>
{children}
</>
);
}
export default App;
export default class App extends React.Component {
constructor(props) {
super(props);
// Todo: this state is work in progress
this.state = {
action: 'init:running',
popupNotification: null,
customSiteUrl: props.customSiteUrl
};
}
/** Handle actions from across App and update App state */
async dispatchAction(action, data) {
clearTimeout(this.timeoutId);
this.setState({
action: `${action}:running`
});
try {
const updatedState = await ActionHandler({action, data, state: this.state, api: this.GhostApi});
this.setState(updatedState);
/** Reset action state after short timeout if not failed*/
if (updatedState && updatedState.action && !updatedState.action.includes(':failed')) {
this.timeoutId = setTimeout(() => {
this.setState({
action: ''
});
}, 2000);
}
} catch (error) {
const popupNotification = createPopupNotification({
type: `${action}:failed`,
autoHide: true, closeable: true, status: 'error', state: this.state,
meta: {
error
}
});
this.setState({
action: `${action}:failed`,
popupNotification
});
}
}
/**Get final App level context from App state*/
getContextFromState() {
const {action, popupNotification, customSiteUrl} = this.state;
return {
action,
popupNotification,
customSiteUrl,
onAction: (_action, data) => this.dispatchAction(_action, data)
};
}
componentWillUnmount() {
/**Clear timeouts and event listeners on unmount */
clearTimeout(this.timeoutId);
}
render() {
return (
<SentryErrorBoundary dsn={this.props.sentryDsn}>
<AppContext.Provider value={this.getContextFromState()}>
<Form />
</AppContext.Provider>
</SentryErrorBoundary>
);
}
}

View File

@ -0,0 +1,15 @@
// Ref: https://reactjs.org/docs/context.html
const React = require('react');
const AppContext = React.createContext({
site: {},
member: {},
action: '',
brandColor: '',
pageData: {},
onAction: (action, data) => {
return {action, data};
}
});
export default AppContext;

View File

@ -0,0 +1,12 @@
const Actions = {
// Put your actions here
};
/** Handle actions in the App, returns updated state */
export default async function ActionHandler({action, data, state, api}) {
const handler = Actions[action];
if (handler) {
return await handler({data, state, api}) || {};
}
return {};
}

View File

@ -0,0 +1,54 @@
import React from 'react';
import AppContext from '../AppContext';
class Form extends React.Component {
static contextType = AppContext;
constructor(props) {
super(props);
this.state = {
message: ''
};
this.submitForm = this.submitForm.bind(this);
this.handleChange = this.handleChange.bind(this);
}
async submitForm(event) {
event.preventDefault();
const message = this.state.message;
if (message.length === 0) {
alert('Please enter a message');
return;
}
try {
// Todo: send comment to server
// Clear message on success
this.setState({message: ''});
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
}
}
handleChange(event) {
this.setState({message: event.target.value});
}
render() {
return (
<form onSubmit={this.submitForm} className="comment-form">
<figure className="avatar">
<span />
</figure>
<textarea value={this.state.message} onChange={this.handleChange} placeholder="What are your thoughts?" />
<button type="submit" className="button primary">Comment</button>
</form>
);
}
}
export default Form;

View File

@ -1,7 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
const ROOT_DIV_ID = 'ghost-comments-root';
function addRootDiv() {
@ -27,7 +27,8 @@ function getSiteData() {
const siteUrl = scriptTag.dataset.ghost;
const apiKey = scriptTag.dataset.key;
const apiUrl = scriptTag.dataset.api;
return {siteUrl, apiKey, apiUrl};
const sentryDsn = scriptTag.dataset.sentryDsn;
return {siteUrl, apiKey, apiUrl, sentryDsn};
}
return {};
}
@ -47,13 +48,12 @@ function setup({siteUrl}) {
function init() {
// const customSiteUrl = getSiteUrl();
const {siteUrl: customSiteUrl} = getSiteData();
const {siteUrl: customSiteUrl, sentryDsn} = getSiteData();
const siteUrl = customSiteUrl || window.location.origin;
setup({siteUrl});
ReactDOM.render(
<React.StrictMode>
Hello World
{/* <App siteUrl={siteUrl} customSiteUrl={customSiteUrl} apiKey={apiKey} apiUrl={apiUrl} /> */}
{<App siteUrl={siteUrl} customSiteUrl={customSiteUrl} sentryDsn={sentryDsn}/>}
</React.StrictMode>,
document.getElementById(ROOT_DIV_ID)
);

View File

@ -0,0 +1,22 @@
export const isDevMode = function ({customSiteUrl = ''} = {}) {
if (customSiteUrl && process.env.NODE_ENV === 'development') {
return false;
}
return (process.env.NODE_ENV === 'development');
};
export const isTestMode = function () {
return (process.env.NODE_ENV === 'test');
};
const modeFns = {
dev: isDevMode,
test: isTestMode
};
export const hasMode = (modes = [], options = {}) => {
return modes.some((mode) => {
const modeFn = modeFns[mode];
return !!(modeFn && modeFn(options));
});
};

View File

@ -0,0 +1,16 @@
export const createPopupNotification = ({type, status, autoHide, duration = 2600, closeable, state, message, meta = {}}) => {
let count = 0;
if (state && state.popupNotification) {
count = (state.popupNotification.count || 0) + 1;
}
return {
type,
status,
autoHide,
closeable,
duration,
meta,
message,
count
};
};

View File

@ -1503,6 +1503,59 @@
estree-walker "^1.0.1"
picomatch "^2.2.2"
"@sentry/browser@7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.5.0.tgz#1ac651117625c732de58cfbd46dd9cf302212e42"
integrity sha512-tTtccbqYti8liTuLudTI0Qrgpe3sKajm0lwsMN4tb1YE879a9JiQ6U6kZ1G/fOIMjOm09pE7J8ozQ+FihPHw5g==
dependencies:
"@sentry/core" "7.5.0"
"@sentry/types" "7.5.0"
"@sentry/utils" "7.5.0"
tslib "^1.9.3"
"@sentry/core@7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.5.0.tgz#4ccc2312017fc6158cc379f5828dc6bbe2cdf1f7"
integrity sha512-2KO2hVUki3WgvPlB0qj9+yea56CmsK2b1XtBSyAnqbs+JiXWgerF4qshVsH52kS/1h2B0CisyeIv64/WfuGvQQ==
dependencies:
"@sentry/hub" "7.5.0"
"@sentry/types" "7.5.0"
"@sentry/utils" "7.5.0"
tslib "^1.9.3"
"@sentry/hub@7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.5.0.tgz#30801accb9475cc3f155802a3fefd218d66fbfda"
integrity sha512-R3jGEOtRtZaYCswSNs/7SmjOj/Pp8BhRyXk4q0a5GXghbuVAdzZvlJH0XnD/6jOJAF0iSXFuyGSLqVUmjkY9Ow==
dependencies:
"@sentry/types" "7.5.0"
"@sentry/utils" "7.5.0"
tslib "^1.9.3"
"@sentry/react@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.5.0.tgz#52b9d611995a9538f36d3b976cfc19abb40c5375"
integrity sha512-kkSRRK5WIrsDzpbTJBmWqKxpOkmtRTjtEAQjJ8+5iIbfFYRjv3kp58aN2v7+7W4XrITkrht3wMhB2W6p+hzxfQ==
dependencies:
"@sentry/browser" "7.5.0"
"@sentry/types" "7.5.0"
"@sentry/utils" "7.5.0"
hoist-non-react-statics "^3.3.2"
tslib "^1.9.3"
"@sentry/types@7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.5.0.tgz#610f14c1219ba461ca84a3c89e06de8c0cf357bc"
integrity sha512-VPQ/53mLo5N8NQUB4k6R2GQBWoW8otFyhhPnC75gYXeBTItVCzJAylVyWy8b+gGqGst+pQN3wb2dl9xhrd69YQ==
"@sentry/utils@7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.5.0.tgz#64435ea094aa7d79d1dfe7586d2d5a2bff9e3839"
integrity sha512-DgHrkGgHplVMgMbU9hGBfGBV6LcOwNBrhHiVaFwo2NHiXnGwMkaILi5XTRjKm9Iu/m2choAFABA80HEtPKmjtA==
dependencies:
"@sentry/types" "7.5.0"
tslib "^1.9.3"
"@sinclair/typebox@^0.23.3":
version "0.23.5"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.23.5.tgz#93f7b9f4e3285a7a9ade7557d9a8d36809cbc47d"
@ -5776,6 +5829,13 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
homedir-polyfill@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"
@ -9514,7 +9574,7 @@ react-error-overlay@6.0.9, react-error-overlay@^6.0.9:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
react-is@^16.13.1:
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@ -11141,7 +11201,7 @@ tsconfig-paths@^3.14.1:
minimist "^1.2.6"
strip-bom "^3.0.0"
tslib@^1.8.1:
tslib@^1.8.1, tslib@^1.9.3:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==