From 710c2656014f0b6bd9b6e21d9006531e4f9cbace Mon Sep 17 00:00:00 2001 From: Simon Backx Date: Tue, 5 Jul 2022 10:34:52 +0200 Subject: [PATCH] Added basic member authentication refs https://github.com/TryGhost/Team/issues/1664 - Added basic API handling - Added member authentication - Basic avatar with form - Setup sentry --- apps/comments-ui/src/App.js | 90 +++++++++++++++- .../comments-ui/src/components/CommentsBox.js | 23 ++++ apps/comments-ui/src/components/Form.js | 38 ++++++- .../src/components/NotSignedInBox.js | 10 ++ apps/comments-ui/src/utils/api.js | 100 ++++++++++++++++++ apps/comments-ui/src/utils/helpers.js | 15 +++ 6 files changed, 269 insertions(+), 7 deletions(-) create mode 100644 apps/comments-ui/src/components/CommentsBox.js create mode 100644 apps/comments-ui/src/components/NotSignedInBox.js create mode 100644 apps/comments-ui/src/utils/api.js diff --git a/apps/comments-ui/src/App.js b/apps/comments-ui/src/App.js index 1d7bd23cd2..eb0b6b4f00 100644 --- a/apps/comments-ui/src/App.js +++ b/apps/comments-ui/src/App.js @@ -1,10 +1,12 @@ -import Form from './components/Form'; import CustomIFrame from './components/CustomIFrame'; import * as Sentry from '@sentry/react'; import React from 'react'; import ActionHandler from './actions'; -import {createPopupNotification} from './utils/helpers'; +import {createPopupNotification,isSentryEventAllowed} from './utils/helpers'; import AppContext from './AppContext'; +import {hasMode} from './utils/check-mode'; +import setupGhostApi from './utils/api'; +import CommentsBox from './components/CommentsBox'; function SentryErrorBoundary({dsn, children}) { if (dsn) { @@ -28,11 +30,41 @@ export default class App extends React.Component { // Todo: this state is work in progress this.state = { action: 'init:running', + initStatus: 'running', + member: null, popupNotification: null, customSiteUrl: props.customSiteUrl }; } + componentDidMount() { + this.initSetup(); + } + + /** Initialize comments setup on load, fetch data and setup state*/ + async initSetup() { + try { + // Fetch data from API, links, preview, dev sources + const {site, member} = await this.fetchApiData(); + const state = { + site, + member, + action: 'init:success', + initStatus: 'success' + }; + + this.setState(state); + } catch (e) { + /* eslint-disable no-console */ + console.error(`[Comments] Failed to initialize:`, e); + /* eslint-enable no-console */ + this.setState({ + action: 'init:failed', + initStatus: 'failed' + }); + } + } + /** Handle actions from across App and update App state */ async dispatchAction(action, data) { clearTimeout(this.timeoutId); @@ -66,13 +98,59 @@ export default class App extends React.Component { } } + /** Fetch site and member session data with Ghost Apis */ + async fetchApiData() { + const {siteUrl, customSiteUrl, apiUrl, apiKey} = this.props; + + try { + this.GhostApi = this.props.api || setupGhostApi({siteUrl, apiUrl, apiKey}); + const {site, member} = await this.GhostApi.init(); + + this.setupSentry({site}); + return {site, member}; + } catch (e) { + if (hasMode(['dev', 'test'], {customSiteUrl})) { + return {}; + } + + throw e; + } + } + + /** Setup Sentry */ + setupSentry({site}) { + if (hasMode(['test'])) { + return null; + } + const {portal_sentry: portalSentry, portal_version: portalVersion, version: ghostVersion} = site; + const appVersion = process.env.REACT_APP_VERSION || portalVersion; + const releaseTag = `comments@${appVersion}|ghost@${ghostVersion}`; + if (portalSentry && portalSentry.dsn) { + Sentry.init({ + dsn: portalSentry.dsn, + environment: portalSentry.env || 'development', + release: releaseTag, + beforeSend: (event) => { + if (isSentryEventAllowed({event})) { + return event; + } + return null; + }, + allowUrls: [ + /https?:\/\/((www)\.)?unpkg\.com\/@tryghost\/comments/ + ] + }); + } + } + /**Get final App level context from App state*/ getContextFromState() { - const {action, popupNotification, customSiteUrl} = this.state; + const {action, popupNotification, customSiteUrl, member} = this.state; return { action, popupNotification, customSiteUrl, + member, onAction: (_action, data) => this.dispatchAction(_action, data) }; } @@ -83,6 +161,10 @@ export default class App extends React.Component { } render() { + if (this.state.initStatus !== 'success') { + return null; + } + const iFrameStyles = { border: 'none', width: '100%' @@ -92,7 +174,7 @@ export default class App extends React.Component { -
+ diff --git a/apps/comments-ui/src/components/CommentsBox.js b/apps/comments-ui/src/components/CommentsBox.js new file mode 100644 index 0000000000..765a3ec30d --- /dev/null +++ b/apps/comments-ui/src/components/CommentsBox.js @@ -0,0 +1,23 @@ +import React from 'react'; +import AppContext from '../AppContext'; +import NotSignedInBox from './NotSignedInBox'; +import Form from './Form'; + +class CommentsBox extends React.Component { + static contextType = AppContext; + + constructor(props) { + super(props); + this.state = {}; + } + + render() { + return ( +
+ { this.context.member ? : } +
+ ); + } +} + +export default CommentsBox; diff --git a/apps/comments-ui/src/components/Form.js b/apps/comments-ui/src/components/Form.js index 88b40d3da9..c3cea4c493 100644 --- a/apps/comments-ui/src/components/Form.js +++ b/apps/comments-ui/src/components/Form.js @@ -38,12 +38,44 @@ class Form extends React.Component { this.setState({message: event.target.value}); } + getInitials() { + if (!this.context.member) { + return ''; + } + const parts = this.context.member.name.split(' '); + + if (parts.length === 0) { + return ''; + } + + if (parts.length === 1) { + return parts[0].substring(0, 1); + } + + return parts[0].substring(0, 1) + parts[parts.length - 1].substring(0, 1); + } + render() { return ( -
- -
+
+
+
+ { this.getInitials() } +
+ { this.context.member ? Avatar : '' } +
+
+
+ {this.context.member ? this.context.member.name : ''} +
+ + Add a bio + +
+ +
+