0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Added links page and look feel settings handling (#64)

no issue

- Handles new look feel settings for portal icon button
- Adds auto width calculation for portal button based on text
- Adds new link page for portal links and data attributes
- Added contrast color calculation for accent color complimentary text
- Added copy text utility for allowing copy to clipboard
This commit is contained in:
Rishabh Garg 2020-07-07 19:53:26 +05:30 committed by GitHub
parent c1f6545915
commit fb6a2c950f
12 changed files with 353 additions and 38 deletions

View file

@ -61,7 +61,7 @@ export default class App extends React.Component {
// Loads default page and popup state for local UI testing
if (process.env.NODE_ENV === 'development') {
return {
page: 'signup',
page: 'links',
showPopup: true
};
}
@ -85,6 +85,7 @@ export default class App extends React.Component {
...this.state.site,
...(previewSite || {})
},
member: this.getPreviewMember(),
...restPreview
});
}
@ -105,7 +106,7 @@ export default class App extends React.Component {
// Display the key/value pairs
for (let pair of qsParams.entries()) {
const key = pair[0];
const value = pair[1];
const value = decodeURIComponent(pair[1]);
if (key === 'button') {
previewSettings.site.portal_button = JSON.parse(value);
} else if (key === 'name') {
@ -118,6 +119,14 @@ export default class App extends React.Component {
allowedPlans.push('yearly');
} else if (key === 'page') {
previewSettings.page = value;
} else if (key === 'accentColor') {
previewSettings.site.accent_color = value;
} else if (key === 'buttonIcon') {
previewSettings.site.portal_button_icon = value;
} else if (key === 'signupButtonText') {
previewSettings.site.portal_button_signup_text = value;
} else if (key === 'buttonStyle') {
previewSettings.site.portal_button_style = value;
}
}
previewSettings.site.portal_plans = allowedPlans;
@ -127,6 +136,17 @@ export default class App extends React.Component {
return {};
}
getPreviewMember() {
if (this.isPreviewMode()) {
const {site: previewSite, ...restPreview} = this.getPreviewState();
if (restPreview.page.includes('account')) {
return Fixtures.member.free;
}
return null;
}
return null;
}
async initSetup() {
const {site, member} = await this.fetchData() || {};
if (!site) {
@ -143,7 +163,7 @@ export default class App extends React.Component {
...site,
...(previewSite || {})
},
member: member || (this.isPreviewMode() ? Fixtures.member.free : null),
member: member || this.getPreviewMember(),
page,
showPopup,
action: 'init:success',
@ -197,8 +217,9 @@ export default class App extends React.Component {
return {type, status, reason};
}
getBrandColor() {
return (this.state.site && this.state.site.brand && this.state.site.brand.primaryColor) || '#3db0ef';
getAccentColor() {
const {accent_color: accentColor = '#3db0ef'} = this.state.site || {};
return accentColor;
}
async onAction(action, data) {
@ -334,7 +355,7 @@ export default class App extends React.Component {
site,
member,
action,
brandColor: this.getBrandColor(),
brandColor: this.getAccentColor(),
page,
lastPage,
onAction: (_action, data) => this.onAction(_action, data)

View file

@ -8,7 +8,7 @@ const setup = (overrides) => {
site,
member: member.free,
action: 'init:success',
brandColor: site.brand.primaryColor,
brandColor: site.accent_color,
page: 'signup',
initStatus: 'success',
showPopup: true

View file

@ -17,12 +17,14 @@ export default class Frame extends Component {
setupFrameBaseStyle() {
this.iframeHtml = this.node.contentDocument.documentElement;
this.iframeHtml.style.fontSize = '62.5%';
this.iframeHtml.style.height = '100%';
this.iframeHead = this.node.contentDocument.head;
this.iframeRoot = this.node.contentDocument.body;
this.iframeRoot.style.margin = '0px';
this.iframeRoot.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif';
this.iframeRoot.style.fontSize = '1.6rem';
this.iframeRoot.style.height = '100%';
this.iframeRoot.style.lineHeight = '1.6em';
this.iframeRoot.style.fontWeight = '400';
this.iframeRoot.style.fontStyle = 'normal';

View file

@ -9,6 +9,7 @@ import AppContext from '../AppContext';
import FrameStyle from './Frame.styles';
import AccountPlanPage from './pages/AccountPlanPage';
import AccountProfilePage from './pages/AccountProfilePage';
import LinkPage from './pages/LinkPage';
const React = require('react');
@ -80,7 +81,10 @@ const StylesWrapper = ({member}) => {
minHeight: '290px',
maxHeight: '290px'
},
accountProfile
accountProfile,
links: {
width: '600px'
}
},
popup: {
container: {
@ -118,7 +122,8 @@ const Pages = {
accountPlan: AccountPlanPage,
accountProfile: AccountProfilePage,
magiclink: MagicLinkPage,
loading: LoadingPage
loading: LoadingPage,
links: LinkPage
};
class PopupContent extends React.Component {

View file

@ -3,23 +3,29 @@ import Frame from './Frame';
import MemberGravatar from './common/MemberGravatar';
import AppContext from '../AppContext';
import {ReactComponent as UserIcon} from '../images/icons/user.svg';
import {ReactComponent as CloseIcon} from '../images/icons/close.svg';
import getContrastColor from '../utils/contrast-color';
const React = require('react');
const Styles = ({brandColor}) => {
const Styles = ({brandColor, hasText}) => {
const frame = {
...(hasText ? {borderRadius: '12px'} : {}),
...(!hasText ? {width: '60px'} : {})
};
return {
frame: {
zIndex: '2147483000',
position: 'fixed',
bottom: '20px',
right: '20px',
width: '60px',
width: '500px',
maxWidth: '500px',
height: '60px',
boxShadow: 'rgba(0, 0, 0, 0.06) 0px 1px 6px 0px, rgba(0, 0, 0, 0.16) 0px 2px 32px 0px',
borderRadius: '50%',
backgroundColor: brandColor,
animation: '250ms ease 0s 1 normal none running animation-bhegco',
transition: 'opacity 0.3s ease 0s'
transition: 'opacity 0.3s ease 0s',
...frame
},
launcher: {
position: 'absolute',
@ -35,17 +41,17 @@ const Styles = ({brandColor}) => {
overflow: 'hidden'
},
button: {
userSelect: 'none',
cursor: 'pointer',
display: 'flex',
WebkitBoxAlign: 'center',
alignItems: 'center',
WebkitBoxPack: 'center',
justifyContent: 'center',
position: 'absolute',
top: '0px',
bottom: '0px',
width: '100%',
opacity: '1',
transform: 'rotate(0deg) scale(1)',
height: '100%',
transition: 'transform 0.16s linear 0s, opacity 0.08s linear 0s'
},
userIcon: {
@ -61,44 +67,162 @@ const Styles = ({brandColor}) => {
};
};
export default class TriggerButton extends React.Component {
class TriggerButtonContent extends React.Component {
static contextType = AppContext;
onToggle() {
this.context.onAction('togglePopup');
constructor(props) {
super(props);
this.state = { };
this.container = React.createRef();
this.height = null;
this.width = null;
}
updateHeight(height) {
this.props.updateHeight && this.props.updateHeight(height);
}
updateWidth(width) {
this.props.updateWidth && this.props.updateWidth(width);
}
componentDidMount() {
if (this.container) {
this.height = this.container.current && this.container.current.offsetHeight;
this.width = this.container.current && this.container.current.offsetWidth;
this.updateHeight(this.height);
this.updateWidth(this.width);
}
}
componentDidUpdate() {
if (this.container) {
const height = this.container.current && this.container.current.offsetHeight;
let width = this.container.current && this.container.current.offsetWidth;
if (height !== this.height) {
this.height = height;
this.updateHeight(this.height);
}
if (width !== this.width) {
this.width = width;
this.updateWidth(this.width);
}
}
}
renderTriggerIcon() {
const {portal_button_icon: buttonIcon = '', portal_button_style: buttonStyle = ''} = this.context.site || {};
const Style = Styles({brandColor: this.context.brandColor});
const memberGravatar = this.context.member && this.context.member.avatar_image;
if (!buttonStyle.includes('icon')) {
return null;
}
if (memberGravatar) {
return (
<MemberGravatar gravatar={memberGravatar} />
);
}
if (this.props.isPopupOpen) {
if (buttonIcon) {
return (
<CloseIcon style={Style.closeIcon} />
<img style={{width: '26px', height: '26px'}} src={buttonIcon} alt="Icon" />
);
} else {
return (
<UserIcon style={Style.userIcon} />
);
}
}
return (
<UserIcon style={Style.userIcon} />
);
hasText() {
const {
portal_button_signup_text: buttonText,
portal_button_style: buttonStyle
} = this.context.site;
return ['icon-and-text', 'text-only'].includes(buttonStyle) && !this.context.member && buttonText;
}
renderText() {
const {
portal_button_signup_text: buttonText
} = this.context.site;
const {brandColor} = this.context;
const textColor = getContrastColor(brandColor);
if (this.hasText()) {
return (
<span style={{padding: '0 12px', color: textColor}}> {buttonText} </span>
);
}
return null;
}
onToggle() {
this.context.onAction('togglePopup');
}
render() {
const hasText = this.hasText();
const Style = Styles({brandColor: this.context.brandColor});
return (
<Frame style={Style.frame} title="membersjs-trigger">
<div style={Style.launcher} onClick={e => this.onToggle(e)}>
<div style={Style.button}>
if (hasText) {
return (
<div style={Style.button} onClick={e => this.onToggle(e)}>
<div style={{padding: '0 24px', display: 'flex'}} ref={this.container}>
{this.renderTriggerIcon()}
{this.renderText()}
</div>
</div>
);
}
return (
<div style={Style.button} onClick={e => this.onToggle(e)}>
<div style={{padding: '0 24px', display: 'flex'}}>
{this.renderTriggerIcon()}
</div>
</div>
);
}
}
export default class TriggerButton extends React.Component {
static contextType = AppContext;
constructor(props) {
super(props);
this.state = {
width: null
};
}
onWidthChange(width) {
this.setState({width});
}
hasText() {
const {
portal_button_signup_text: buttonText,
portal_button_style: buttonStyle
} = this.context.site;
return ['icon-and-text', 'text-only'].includes(buttonStyle) && !this.context.member && buttonText;
}
render() {
const hasText = this.hasText();
const Style = Styles({brandColor: this.context.brandColor, hasText});
const frameStyle = {
...Style.frame
};
if (this.state.width) {
const updatedWidth = this.state.width + 2;
frameStyle.width = `${updatedWidth}px`;
}
return (
<Frame style={frameStyle} title="membersjs-trigger">
<TriggerButtonContent isPopupOpen={this.props.isPopupOpen} updateWidth={width => this.onWidthChange(width)} />
</Frame>
);
}

View file

@ -1,14 +1,16 @@
import React from 'react';
import getContrastColor from '../../utils/contrast-color';
const Styles = ({brandColor, retry, disabled, style = {}}) => {
let backgroundColor = (brandColor || '#3eb0ef');
if (retry) {
backgroundColor = 'red';
backgroundColor = '#FF0000';
}
if (disabled) {
backgroundColor = 'grey';
backgroundColor = '#D3D3D3';
}
const textColor = getContrastColor(backgroundColor);
return {
button: {
display: 'inline-block',
@ -24,7 +26,7 @@ const Styles = ({brandColor, retry, disabled, style = {}}) => {
borderRadius: '5px',
cursor: 'pointer',
transition: '.4s ease',
color: '#fff',
color: textColor,
backgroundColor,
boxShadow: 'none',
userSelect: 'none',

View file

@ -0,0 +1,108 @@
import AppContext from '../../AppContext';
import CopyToClipboard from '../../utils/copy-to-clipboard';
const React = require('react');
function getLinkOrAttribute({page, isLink, siteUrl}) {
if (page === 'default') {
return (isLink ? `${siteUrl}/#/portal` : 'data-portal');
} else if (page === 'signup') {
return (isLink ? `${siteUrl}/#/portal/signup` : `data-portal="signup"`);
} else if (page === 'signin') {
return (isLink ? `${siteUrl}/#/portal/signin` : `data-portal="signin"`);
} else if (page === 'accountHome') {
return (isLink ? `${siteUrl}/#/portal/account` : `data-portal="account"`);
} else if (page === 'accountPlan') {
return (isLink ? `${siteUrl}/#/portal/account/plans` : `data-portal="account/plans"`);
} else if (page === 'accountProfile') {
return (isLink ? `${siteUrl}/#/portal/account/profile` : `data-portal="account/profile"`);
}
}
const LinkAttributeValue = ({page, isLink, siteUrl}) => {
const rightItemStyle = {
paddingBottom: '24px',
display: 'flex'
};
const value = getLinkOrAttribute({page, isLink, siteUrl});
return (
<div style={rightItemStyle}>
<span style={{
flexGrow: 1,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
paddingRight: '12px'
}}> {value} </span>
<button type="button" onClick={(e) => {
CopyToClipboard(value);
}}> Copy </button>
</div>
);
};
const LinkAttributeSection = ({siteUrl, showLinks: isLink, toggleShowLinks}) => {
return (
<div style={{flexGrow: 1, minWidth: '350px'}}>
<div style={{display: 'flex', borderBottom: '1px solid black', marginBottom: '12px'}}>
<span style={{flexGrow: 1, fontWeight: 'bold'}}> {isLink ? 'Link' : 'Data Attribute'} </span>
<LinkAttributeToggle showLinks={isLink} toggleShowLinks={toggleShowLinks}/>
</div>
<LinkAttributeValue page="default" isLink={isLink} siteUrl={siteUrl} />
<LinkAttributeValue page="signup" isLink={isLink} siteUrl={siteUrl} />
<LinkAttributeValue page="signin" isLink={isLink} siteUrl={siteUrl} />
<LinkAttributeValue page="accountHome" isLink={isLink} siteUrl={siteUrl} />
<LinkAttributeValue page="accountPlan" isLink={isLink} siteUrl={siteUrl} />
<LinkAttributeValue page="accountProfile" isLink={isLink} siteUrl={siteUrl} />
</div>
);
};
const LinkAttributeToggle = ({showLinks, toggleShowLinks}) => {
const text = showLinks ? 'Show Data Attributes' : 'Show Links';
return (
<span
style={{cursor: 'pointer', color: '#3eb0ef'}}
onClick={() => toggleShowLinks({showLinks: !showLinks})}> {text}
</span>
);
};
export default class LinkPage extends React.Component {
static contextType = AppContext;
constructor(props) {
super(props);
this.state = {
showLinks: true
};
}
render() {
const itemStyle = {
paddingRight: '32px',
paddingBottom: '24px'
};
const {url: siteUrl = ''} = this.context.site;
return (
<div style={{display: 'flex', flexDirection: 'column', color: '#313131', padding: '12px 24px'}}>
<div> Use these links or data attributes to show the various sections of members modal.</div>
<div style={{display: 'flex', marginTop: '12px'}}>
<div style={{flexShrink: 0}}>
<div style={{borderBottom: '1px solid black', marginBottom: '12px', fontWeight: 'bold'}}> Section </div>
<div style={itemStyle}> Default </div>
<div style={itemStyle}> Signup </div>
<div style={itemStyle}> Signin </div>
<div style={itemStyle}> Account home </div>
<div style={itemStyle}> Account -&gt; Plans </div>
<div style={itemStyle}> Account -&gt; Profile </div>
</div>
<LinkAttributeSection
showLinks={this.state.showLinks}
toggleShowLinks={({showLinks}) => this.setState({showLinks})}
siteUrl={siteUrl}
/>
</div>
</div>
);
}
}

View file

@ -217,7 +217,7 @@ function setupGhostApi({siteUrl = window.location.origin}) {
api.init = async () => {
// Load member from fixtures for local development
if (process.env.NODE_ENV === 'development') {
return {site: Fixtures.site, member: Fixtures.member.free};
return {site: Fixtures.site, member: null};
}
const {site} = await api.site.read();

View file

@ -0,0 +1,39 @@
function padZero(str, len) {
len = len || 2;
var zeros = new Array(len).join('0');
return (zeros + str).slice(-len);
}
function invertColor(hex, bw = true) {
if (!hex.match(/#[0-9A-Fa-f]{6}$/)) {
// Return white in case not a valid hex
return '#000000';
}
if (hex.indexOf('#') === 0) {
hex = hex.slice(1);
}
// convert 3-digit hex to 6-digits.
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
if (hex.length !== 6) {
throw new Error('Invalid HEX color.');
}
let r = parseInt(hex.slice(0, 2), 16),
g = parseInt(hex.slice(2, 4), 16),
b = parseInt(hex.slice(4, 6), 16);
if (bw) {
// http://stackoverflow.com/a/3943023/112731
return (r * 0.299 + g * 0.587 + b * 0.114) > 186
? '#000000'
: '#FFFFFF';
}
// invert color components
r = (255 - r).toString(16);
g = (255 - g).toString(16);
b = (255 - b).toString(16);
// pad each with zeros and return
return '#' + padZero(r) + padZero(g) + padZero(b);
}
module.exports = invertColor;

View file

@ -0,0 +1,13 @@
function copyTextToClipboard(text) {
let textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
module.exports = copyTextToClipboard;

View file

@ -2,9 +2,7 @@ export const site = {
title: 'Ghost Site',
description: 'Thoughts, stories and ideas.',
logo: 'https://pbs.twimg.com/profile_images/1111773508231667713/mf2N0uqc_400x400.png',
brand: {
primaryColor: '#AB19E4'
},
accent_color: '#AB19E4',
url: 'http://localhost:2368/',
plans: {
monthly: 12,
@ -16,7 +14,10 @@ export const site = {
is_stripe_configured: true,
portal_button: true,
portal_name: true,
portal_plans: ['free', 'monthly', 'yearly']
portal_plans: ['free', 'monthly', 'yearly'],
portal_button_icon: 'https://raw.githubusercontent.com/TryGhost/members.js/master/src/images/icons/user.svg',
portal_button_signup_text: 'Subscribe Now for free access to everything',
portal_button_style: 'icon-and-text'
};
export const member = {

View file

@ -21,7 +21,7 @@ const customRender = (ui, {options = {}, overrideContext = {}} = {}) => {
site,
member: member.free,
action: 'init:success',
brandColor: site.brand.primaryColor,
brandColor: site.accent_color,
page: 'signup',
onAction: mockOnActionFn,
...overrideContext