Added share/send preview footers to post preview modal

refs https://github.com/TryGhost/Team/issues/451

- disabled email tab when previewing a page (pages can't be emailed)
- added share preview url footer to browser tab
- added send preview footer to email tab
- added first pass at social tab contents
This commit is contained in:
Kevin Ansfield 2021-01-26 22:49:05 +00:00 committed by Daniel Lockyer
parent 76419917bf
commit 7f7d2cafc6
13 changed files with 250 additions and 13 deletions

View File

@ -2,8 +2,10 @@
<header class="modal-header gh-pe-header gh-pe-header-border" data-test-modal="preview-email" style="display:flex">
<h2 class="f6 fw6">Preview post</h2>
<div class="gh-contentfilter gh-btn-group gh-pe-btn-group" style="display:flex;flex-grow:1;justify-content:center">
<button type="button" class="gh-btn {{if (eq this.tab "desktop") "gh-btn-group-selected"}}" {{on "click" (fn this.changeTab "desktop")}}><span>Desktop</span></button>
<button type="button" class="gh-btn {{if (eq this.tab "email") "gh-btn-group-selected"}}" {{on "click" (fn this.changeTab "email")}}><span>Email</span></button>
<button type="button" class="gh-btn {{if (eq this.tab "browser") "gh-btn-group-selected"}}" {{on "click" (fn this.changeTab "browser")}}><span>Browser</span></button>
{{#if this.model.isPost}}
<button type="button" class="gh-btn {{if (eq this.tab "email") "gh-btn-group-selected"}}" {{on "click" (fn this.changeTab "email")}}><span>Email</span></button>
{{/if}}
<button type="button" class="gh-btn {{if (eq this.tab "social") "gh-btn-group-selected"}}" {{on "click" (fn this.changeTab "social")}}><span>Social</span></button>
</div>
<div class="gh-pe-close">
@ -13,11 +15,11 @@
</div>
</header>
{{#if (eq this.tab "desktop")}}
<ModalPostPreview::Desktop @post={{@model}} />
{{#if (eq this.tab "browser")}}
<ModalPostPreview::Browser @post={{@model}} />
{{/if}}
{{#if (eq this.tab "email")}}
{{#if (and (eq this.tab "email") this.model.isPost)}}
<ModalPostPreview::Email @post={{@model}} />
{{/if}}

View File

@ -6,7 +6,7 @@ import {tracked} from '@glimmer/tracking';
// TODO: update modals to work fully with Glimmer components
@classic
export default class ModalPostPreviewComponent extends ModalBase {
@tracked tab = 'desktop';
@tracked tab = 'browser';
@action
changeTab(tab) {
@ -17,4 +17,15 @@ export default class ModalPostPreviewComponent extends ModalBase {
close() {
this.closeModal();
}
actions = {
confirm() {
// noop - needed to override ModalBase.actions.confirm
},
// needed because ModalBase uses .send() for keyboard events
closeModal() {
this.closeModal();
}
}
}

View File

@ -0,0 +1,20 @@
<div class="modal-body modal-preview-email-content gh-pe-desktop-container flex-grow-1 h-auto overflow-auto">
<div class="gh-pe-emailclient-mockup">
<iframe class="gh-pe-iframe" src={{@post.previewUrl}} style="height: 100%"></iframe>
</div>
</div>
<div class="flex items-center pa4 pl8 bt b--whitegrey">
<span class="mr3 flex-grow-1 nowrap">Share this preview privately</span>
<div class="mr3 truncate midlightgrey">
{{@post.previewUrl}}
</div>
<button type="button" {{on "click" (perform this.copyPreviewUrl)}} class="gh-btn gh-btn-green gh-btn-icon">
<span>
{{#if this.copyPreviewUrl.isRunning}}
{{svg-jar "check-circle" class="w3 v-mid mr2 stroke-white"}} Copied
{{else}}
Copy
{{/if}}
</span>
</button>
</div>

View File

@ -0,0 +1,12 @@
import Component from '@glimmer/component';
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
import {task} from 'ember-concurrency-decorators';
import {timeout} from 'ember-concurrency';
export default class ModalPostPreviewBrowserComponent extends Component {
@task
*copyPreviewUrl() {
copyTextToClipboard(this.args.post.previewUrl);
yield timeout(this.isTesting ? 50 : 3000);
}
}

View File

@ -1,5 +0,0 @@
<div class="modal-body modal-preview-email-content gh-pe-desktop-container flex-grow-1" style="height: auto">
<div class="gh-pe-emailclient-mockup">
<iframe class="gh-pe-iframe" src={{@post.previewUrl}} style="height: 100%"></iframe>
</div>
</div>

View File

@ -1,4 +1,4 @@
<div class="modal-body modal-preview-email-content gh-pe-mobile-container">
<div class="modal-body modal-preview-email-content gh-pe-mobile-container h-auto overflow-auto">
<div class="gh-pe-mobile-bezel">
<div class="gh-pe-mobile-screen">
<div class="gh-pe-emailclient-sender">
@ -10,4 +10,26 @@
<iframe class="bn gh-pe-iframe" {{did-insert this.renderEmailPreview}} sandbox="allow-same-origin"></iframe>
</div>
</div>
</div>
<div class="flex items-center pa4 pl8 bt b--whitegrey">
<span class="mr3 flex-grow-1 nowrap">Send a preview newsletter</span>
<div class="mr3 truncate midlightgrey">
<Input
@value={{this.previewEmailAddress}}
class="gh-input"
placeholder="you@yoursite.com"
{{on-key "Enter" (perform this.sendPreviewEmailTask)}}
/>
{{#if this.sendPreviewEmailError}}
<div class="error"><p class="response">{{this.sendPreviewEmailError}}</p></div>
{{/if}}
</div>
<GhTaskButton
@task={{this.sendPreviewEmailTask}}
@buttonText="Send"
@successText="Sent"
@runningText="Sending..."
@class="gh-btn gh-btn-green gh-btn-icon"
/>
</div>

View File

@ -1,6 +1,9 @@
import Component from '@glimmer/component';
import validator from 'validator';
import {action} from '@ember/object';
import {htmlSafe} from '@ember/string';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency-decorators';
import {timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
@ -24,6 +27,15 @@ export default class ModalPostPreviewEmailComponent extends Component {
@tracked html = '';
@tracked subject = '';
@tracked emailPreviewAddress = '';
@tracked sendPreviewEmailError = '';
get mailgunIsEnabled() {
return this.config.get('mailgunIsConfigured') ||
this.settings.get('mailgunApiKey') &&
this.settings.get('mailgunDomain') &&
this.settings.get('mailgunBaseUrl');
}
@action
async renderEmailPreview(iframe) {
@ -38,6 +50,46 @@ export default class ModalPostPreviewEmailComponent extends Component {
}
}
@task({drop: true})
*sendPreviewEmailTask() {
try {
const resourceId = this.post.id;
const testEmail = this.emailPreviewAddress.trim();
if (!validator.isEmail(testEmail)) {
this.sendPreviewEmailError = 'Please enter a valid email';
return false;
}
if (!this.mailgunIsEnabled) {
this.sendPreviewEmailError = 'Please verify your email settings';
return false;
}
this.sendPreviewEmailError = '';
const url = this.ghostPaths.url.api('/email_preview/posts', resourceId);
const data = {emails: [testEmail]};
const options = {
data,
dataType: 'json'
};
return yield this.ajax.post(url, options);
} catch (error) {
if (error) {
let message = 'Email could not be sent, verify mail settings';
// grab custom error message if present
if (
error.payload && error.payload.errors
&& error.payload.errors[0] && error.payload.errors[0].message) {
message = htmlSafe(error.payload.errors[0].message);
}
this.sendPreviewEmailError = message;
}
}
}
async _fetchEmailData() {
let {html, subject} = this;
let {post} = this.args;

View File

@ -0,0 +1,42 @@
<div class="modal-body pa8 flex-grow-1">
<div class="gh-og-preview w-40 mb6">
{{#if this.facebookImage}}
<div class="gh-og-preview-image" style={{background-image-style this.facebookImage}}></div>
{{/if}}
<div class="gh-og-preview-content">
<div class="gh-og-preview-title">{{truncate this.facebookTitle 88}}</div>
<div class="gh-og-preview-description">{{truncate this.facebookDescription 300}}</div>
<div class="gh-og-preview-footer">
<div class="gh-og-preview-footer-left">
{{this.config.blogDomain}} <span class="gh-og-preview-footer-left-divider">|</span> by <span class="gh-og-preview-footer-author">{{author-names @post.authors}}</span>
</div>
<div class="gh-og-preview-footer-right">
</div>
</div>
</div>
</div>
<div class="gh-twitter-preview w-40 mb6">
{{#if this.twitterImage}}
<div class="gh-twitter-preview-image" style={{background-image-style this.twitterImage}}></div>
{{/if}}
<div class="gh-twitter-preview-content">
<div class="gh-twitter-preview-title">{{this.twitterTitle}}</div>
<div class="gh-twitter-preview-description">{{truncate this.twitterDescription 155}}</div>
<div class="gh-twitter-preview-footer">
<div class="gh-twitter-preview-footer-left">
{{this.config.blogDomain}}
</div>
<div class="gh-twitter-preview-footer-right">
</div>
</div>
</div>
</div>
<div class="seo-preview w-40 mb6">
<div class="seo-preview-title">{{truncate this.serpTitle 70}}</div>
<div class="seo-preview-link">{{truncate this.serpURL 70}}</div>
<div class="seo-preview-description">{{truncate this.serpDescription 300}}</div>
</div>
</div>

View File

@ -0,0 +1,65 @@
import Component from '@glimmer/component';
import formatMarkdown from 'ghost-admin/utils/format-markdown';
import {inject as service} from '@ember/service';
export default class ModalPostPreviewSocialComponent extends Component {
@service config;
@service settings;
get _fallbackDescription() {
return this.args.post.customExcerpt ||
this.serpDescription ||
this.args.post.excerpt ||
this.settings.get('description');
}
// SERP
get serpTitle() {
return this.args.post.metaTitle || this.args.post.title || '(Untitled)';
}
get serpDescription() {
const metaDescription = this.args.post.metaDescription;
if (metaDescription) {
return metaDescription;
}
const mobiledoc = this.args.post.scratch;
const [markdownCard] = (mobiledoc && mobiledoc.cards) || [];
const markdown = markdownCard && markdownCard[1] && markdownCard[1].markdown;
let serpDescription;
const div = document.createElement('div');
div.innerHTML = formatMarkdown(markdown, false);
// Strip HTML
serpDescription = div.textContent;
// Replace new lines and trim
serpDescription = serpDescription.replace(/\n+/g, ' ').trim();
return serpDescription;
}
// Facebook
get facebookTitle() {
return this.args.post.ogTitle || this.serpTitle;
}
get facebookDescription() {
return this.args.post.ogDescription || this._fallbackDescription;
}
// Twitter
get twitterTitle() {
return this.args.post.twitterTitle || this.serpTitle;
}
get twitterDescription() {
return this.args.post.twitterDescription || this._fallbackDescription;
}
}

View File

@ -19,6 +19,7 @@
top: 0;
left: 0;
right: 0;
min-height: 70px;
display: flex;
align-items: center;
justify-content: center;
@ -146,9 +147,9 @@
}
.fullscreen-modal-email-preview .gh-pe-mobile-container {
height: calc(100vh - 140px);
display: flex;
flex-direction: column;
flex-grow: 1;
align-items: center;
background: var(--whitegrey-l1);
padding: 30px 30px 45px;

View File

@ -35,6 +35,10 @@ module.exports = function (environment) {
moment: {
includeTimezone: 'all'
},
emberKeyboard: {
disableInputsInitializer: true
}
};

View File

@ -84,6 +84,7 @@
"ember-fetch": "8.0.4",
"ember-in-viewport": "3.8.1",
"ember-infinity": "2.1.2",
"ember-keyboard": "^6.0.2",
"ember-load": "0.0.17",
"ember-load-initializers": "2.1.2",
"ember-mocha": "0.16.2",

View File

@ -6276,6 +6276,16 @@ ember-invoke-action@^1.5.0:
dependencies:
ember-cli-babel "^6.6.0"
ember-keyboard@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/ember-keyboard/-/ember-keyboard-6.0.2.tgz#efe260be34621a403a4d4688b038d65b371f6892"
integrity sha512-ZGAXYGfN4gxGFRcv3ix2X8HA8j/VluwhlENT9pfbFjAAGtvFz9wzNUJuaq3LS5Ksj+f2oXL5f++tSOrO7Ha1wA==
dependencies:
ember-cli-babel "^7.22.1"
ember-cli-htmlbars "^5.3.1"
ember-compatibility-helpers "^1.2.1"
ember-modifier "^2.1.0"
ember-load-initializers@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ember-load-initializers/-/ember-load-initializers-2.1.2.tgz#8a47a656c1f64f9b10cecdb4e22a9d52ad9c7efa"