Skip to content

Commit 2d2d7f5

Browse files
JohnONolanErisDS
authored andcommitted
🎨 Updated private page access and signup flow (TryGhost#26762)
The private page now needs a richer branded experience and signup flow, but Portal is not available on this template. Keeping the behavior local to Ghost core gives us the required UX without broadening the page runtime. The private page has been updated to use publication branding and description, instead of just an inline form to gain access. The access form has been moved to a dialog with a footer link, alongside an admin login link with Portal-style footer treatment, making these secondary concerns for the page. The main content is now a self-signup form, with brand-aware styling, ading/success states, success redirect handling, and normalized member signup errors Expanding the brand experience on the private page necessitated adding 4 new handlebars utilities: @site.admin_url, json, contrast_text_color and color_to_rgba - these have all been added to GScan, and docs will be updated soon. --------- Co-authored-by: Hannah Wolfe <github.erisds@gmail.com>
1 parent 77110b0 commit 2d2d7f5

21 files changed

Lines changed: 1516 additions & 36 deletions

File tree

ghost/core/bin/minify-assets.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ const filesToMinify = [
7575
options: {
7676
bundle: false
7777
}
78+
},
79+
{
80+
src: 'core/frontend/public/private.js',
81+
dest: 'core/frontend/public/private.min.js',
82+
options: {
83+
bundle: false
84+
}
7885
}
7986
];
8087

ghost/core/core/frontend/apps/private-blogging/lib/helpers/input_password.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ module.exports = function input_password(options) { // eslint-disable-line camel
1818
extras += ` placeholder="${options.hash.placeholder}"`;
1919
}
2020

21+
if (options.hash['data-1p-ignore']) {
22+
extras += ' data-1p-ignore';
23+
}
24+
2125
const output = templates.input({
2226
type: 'password',
2327
name: 'password',

ghost/core/core/frontend/apps/private-blogging/lib/views/private.hbs

Lines changed: 81 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,34 +34,101 @@
3434
<meta name="apple-mobile-web-app-capable" content="yes" />
3535

3636
<link rel="stylesheet" href="{{asset "public/ghost.css" hasMinFile="true"}}"/>
37+
<script>
38+
document.documentElement.classList.remove('no-js');
39+
document.documentElement.classList.add('js');
40+
</script>
3741
</head>
38-
<body>
42+
<body{{#if @site.accent_color}} style="--gh-private-accent-color: {{@site.accent_color}}; --gh-private-accent-color-rgba: {{color_to_rgba @site.accent_color 0.25}}; --gh-private-accent-contrast-color: {{contrast_text_color @site.accent_color}};"{{/if}}>
3943
<div class="gh-app">
4044
<div class="gh-viewport">
4145
<main class="gh-main" role="main">
4246
<div class="gh-flow">
4347
<div class="gh-flow-content-wrap">
44-
{{#if @site.icon}}
45-
<img class="site-icon" src="{{img_url @site.icon absolute="true"}}" alt="icon">
46-
{{/if}}
4748
<section class="gh-flow-content private">
4849
<header>
49-
<h1>{{t "This site is private."}}</h1>
50+
{{#if @site.icon}}
51+
<img class="site-icon" src="{{img_url @site.icon absolute="true"}}" alt="icon">
52+
{{/if}}
53+
<h1>{{@site.title}}</h1>
54+
{{#if @site.description}}
55+
<p class="gh-private-description">{{@site.description}}</p>
56+
{{/if}}
57+
{{#if @site.allow_self_signup}}
58+
<form class="gh-private-signup-form gh-signin" data-ghost-private-subscribe-form data-members-form="subscribe" data-site-url="{{@site.url}}" data-state="idle" novalidate="novalidate">
59+
<div class="gh-private-signup-fields">
60+
<input class="gh-input gh-private-signup-input" type="email" placeholder="{{t "Your email address"}}" aria-label="{{t "Your email address"}}" autocomplete="email" required data-members-email>
61+
<button class="gh-btn gh-private-signup-btn" type="submit">
62+
<span class="gh-private-signup-btn-content">
63+
<b class="gh-private-signup-btn-label">{{t "Subscribe"}}</b>
64+
<svg class="gh-private-signup-btn-loading" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
65+
<circle cx="4" cy="12" r="3"></circle>
66+
<circle cx="12" cy="12" r="3"></circle>
67+
<circle cx="20" cy="12" r="3"></circle>
68+
</svg>
69+
<svg class="gh-private-signup-btn-success" viewBox="0 0 15 14" aria-hidden="true" focusable="false">
70+
<path d="M1 6.89286L6.10714 12L13.9643 1" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
71+
</svg>
72+
</span>
73+
</button>
74+
</div>
75+
<p class="gh-private-signup-feedback" data-ghost-private-subscribe-feedback data-state="idle" aria-live="polite"></p>
76+
</form>
77+
{{/if}}
5078
</header>
51-
<form class="gh-signin" method="post" novalidate="novalidate">
52-
<div class="form-group{{#if error}} error{{/if}}">
53-
{{input_password class="gh-input" placeholder=(t "Password")}}
54-
{{#if error}}
55-
<p class="main-error">{{error.message}}</p>
56-
{{/if}}
57-
</div>
58-
<button class="gh-btn" type="submit"><span>{{t "Access site"}} &rarr;</span></button>
59-
</form>
6079
</section>
80+
<dialog class="gh-private-dialog" id="access" aria-labelledby="private-access-title" {{#if @site.accent_color}} style="--gh-private-accent-color: {{@site.accent_color}}; --gh-private-accent-color-rgba: {{color_to_rgba @site.accent_color 0.25}}; --gh-private-accent-contrast-color: {{contrast_text_color @site.accent_color}};"{{/if}} {{#if error}}data-auto-open="true"{{/if}}>
81+
<div class="gh-private-dialog-panel">
82+
<button class="gh-private-dialog-close" type="button" data-ghost-private-close aria-label="{{t "Close"}}">
83+
<svg class="gh-private-dialog-close-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
84+
<path d="M6 6L18 18" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2.5"></path>
85+
<path d="M18 6L6 18" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2.5"></path>
86+
</svg>
87+
</button>
88+
<header class="gh-private-dialog-header">
89+
<h2 id="private-access-title">{{t "Enter access code"}}</h2>
90+
</header>
91+
<form class="gh-signin gh-private-signin" method="post" novalidate="novalidate">
92+
<div class="form-group{{#if error}} error{{/if}}">
93+
{{input_password class="gh-input" placeholder=(t "Access code") data-1p-ignore="true"}}
94+
{{#if error}}
95+
<p class="main-error">{{error.message}}</p>
96+
{{/if}}
97+
</div>
98+
<button class="gh-btn" type="submit"><span>{{t "Enter"}} &rarr;</span></button>
99+
</form>
100+
</div>
101+
</dialog>
102+
<div class="gh-private-trigger-wrap">
103+
<div class="gh-private-footer-links">
104+
<a class="gh-private-powered" href="https://ghost.org" target="_blank" rel="noopener noreferrer">
105+
<svg class="gh-private-powered-logo" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
106+
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 12C24 18.6274 18.6274 24 12 24C5.37258 24 0 18.6274 0 12C0 5.37258 5.37258 0 12 0C18.6274 0 24 5.37258 24 12ZM11.8326 2.33879C6.37785 2.95189 3.95901 5.20797 3.41126 9.74699C3.34896 10.2632 3.22642 10.7805 3.10443 11.2954C2.93277 12.02 2.76221 12.74 2.76221 13.4458C2.76221 17.9885 6.5856 21.556 11.1283 21.556C12.8959 21.556 14.4433 20.8144 15.8756 20.048C19.0536 18.3478 22.0328 16.2597 22.0328 12.5411C22.0328 9.91512 20.1051 7.56932 18.466 5.5747C18.3834 5.47416 18.3015 5.37451 18.2206 5.27577C17.3866 4.25742 14.4333 2.04643 11.8326 2.33879Z" fill="currentColor"></path>
107+
</svg>
108+
<span>Powered by Ghost</span>
109+
</a>
110+
<span class="gh-private-footer-separator" aria-hidden="true">|</span>
111+
<a class="gh-private-trigger" href="#access" data-ghost-private-trigger>{{t "Enter access code"}}</a>
112+
<span class="gh-private-footer-separator" aria-hidden="true">|</span>
113+
<a href="{{@site.admin_url}}">{{t "Site owner login"}}</a>
114+
</div>
115+
</div>
61116
</div>
62117
</div>
63118
</main>
64119
</div>
65120
</div>
121+
<script id="gh-private-config" type="application/json">{{json
122+
accessDialogSelector="#access"
123+
footerLinksSelector=".gh-private-trigger-wrap"
124+
successResetDelay=1000
125+
invalidEmailMessage=(t "Please enter a valid email address")
126+
genericErrorMessage=(t "Something went wrong, please try again.")
127+
confirmEmailMessage=(t "Thanks! Now check your email to confirm.")
128+
subscriptionConfirmedMessage=(t "Subscription confirmed!")
129+
restrictedDomainMessage=(t "Signups from this email domain are currently restricted.")
130+
tooManyAttemptsMessage=(t "Too many sign-up attempts, try again later")
131+
}}</script>
132+
<script src="{{asset "public/private.js" hasMinFile="true"}}" defer></script>
66133
</body>
67134
</html>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const {Color} = require('@tryghost/color-utils');
2+
3+
module.exports = function color_to_rgba(color, alpha) { // eslint-disable-line camelcase
4+
const backgroundColor = (typeof color === 'string' && color.trim()) ? color.trim() : '#15171A';
5+
const opacity = Number.isFinite(alpha) ? alpha : Number.parseFloat(alpha);
6+
const normalizedOpacity = Number.isFinite(opacity) ? Math.max(0, Math.min(1, opacity)) : 0.25;
7+
8+
try {
9+
return new Color(backgroundColor).alpha(normalizedOpacity).rgb().string();
10+
} catch (err) {
11+
return 'rgba(21, 23, 26, 0.25)';
12+
}
13+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const {textColorForBackgroundColor} = require('@tryghost/color-utils');
2+
3+
module.exports = function contrast_text_color(color) { // eslint-disable-line camelcase
4+
const backgroundColor = (typeof color === 'string' && color.trim()) ? color.trim() : '#15171A';
5+
6+
try {
7+
return textColorForBackgroundColor(backgroundColor).hex();
8+
} catch (err) {
9+
return '#FFFFFF';
10+
}
11+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const {SafeString} = require('../services/handlebars');
2+
3+
module.exports = function json(value, options) {
4+
let payload = value;
5+
6+
if (arguments.length === 1 && value && value.hash) {
7+
payload = value.hash;
8+
} else if (options && options.hash && Object.keys(options.hash).length > 0) {
9+
payload = options.hash;
10+
}
11+
12+
const raw = JSON.stringify(payload);
13+
14+
if (raw === undefined) {
15+
return new SafeString('null');
16+
}
17+
18+
const serialized = raw
19+
.replace(/</g, '\\u003C')
20+
.replace(/>/g, '\\u003E')
21+
.replace(/&/g, '\\u0026')
22+
.replace(/\u2028/g, '\\u2028')
23+
.replace(/\u2029/g, '\\u2029');
24+
25+
return new SafeString(serialized);
26+
};

0 commit comments

Comments
 (0)