Browse Source

Updates account view content

main
Keith Daulton 1 year ago
parent
commit
4edfe8b46c
7 changed files with 299 additions and 551 deletions
  1. +2
    -4
      src/webviews/apps/plus/account/account.html
  2. +7
    -17
      src/webviews/apps/plus/account/account.scss
  3. +10
    -23
      src/webviews/apps/plus/account/account.ts
  4. +266
    -0
      src/webviews/apps/plus/account/components/account-content.ts
  5. +0
    -378
      src/webviews/apps/plus/account/components/header-card.ts
  6. +0
    -129
      src/webviews/apps/plus/account/components/plus-content.ts
  7. +14
    -0
      src/webviews/apps/shared/components/styles/lit/base.css.ts

+ 2
- 4
src/webviews/apps/plus/account/account.html View File

@ -4,10 +4,8 @@
<meta charset="utf-8" />
</head>
<body class="account preload" data-placement="#{placement}">
<header class="account__header">
<header-card id="header-card" image="#{webroot}/media/gitlens-logo.webp"></header-card>
</header>
<body class="account scrollable preload" data-placement="#{placement}">
<account-content id="account-content"></account-content>
#{endOfBody}
<style nonce="#{cspNonce}">

+ 7
- 17
src/webviews/apps/plus/account/account.scss View File

@ -1,3 +1,5 @@
@use '../../shared/styles/properties';
:root {
--gitlens-z-inline: 1000;
--gitlens-z-sticky: 1100;
@ -42,8 +44,6 @@ html {
height: 100%;
font-size: 62.5%;
text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
@ -101,21 +101,11 @@ body {
outline-color: var(--vscode-focusBorder);
}
account-content {
margin-top: 1.3rem;
margin-bottom: 1.3rem;
}
.account {
padding: 0;
height: 100%;
display: flex;
flex-direction: column;
gap: 0.4rem;
overflow: hidden;
&__header {
flex: none;
padding: 0 2rem;
position: relative;
[aria-hidden='false'] ~ header-card {
display: none;
}
}
}

+ 10
- 23
src/webviews/apps/plus/account/account.ts View File

@ -8,9 +8,8 @@ import type { IpcMessage } from '../../../protocol';
import { ExecuteCommandType, onIpc } from '../../../protocol';
import { App } from '../../shared/appBase';
import { DOM } from '../../shared/dom';
import type { HeaderCard } from './components/header-card';
import '../../shared/components/code-icon';
import './components/header-card';
import type { AccountContent } from './components/account-content';
import './components/account-content';
export class AccountApp extends App<State> {
constructor() {
@ -76,29 +75,17 @@ export class AccountApp extends App {
return getSubscriptionTimeRemaining(this.state.subscription, 'days') ?? 0;
}
private updateHeader(days = this.getDaysRemaining()) {
private updateState() {
const days = this.getDaysRemaining();
const { subscription, avatar } = this.state;
const $headerContent = document.getElementById('header-card')! as HeaderCard;
if (avatar) {
$headerContent.setAttribute('image', avatar);
}
$headerContent.setAttribute('name', subscription.account?.name ?? '');
// TODO: remove
const steps = 0;
const completed = 0;
const $content = document.getElementById('account-content')! as AccountContent;
$headerContent.setAttribute('steps', steps.toString());
$headerContent.setAttribute('completed', completed.toString());
$headerContent.setAttribute('state', subscription.state.toString());
$headerContent.setAttribute('plan', subscription.plan.effective.name);
$headerContent.setAttribute('days', days.toString());
}
private updateState() {
const days = this.getDaysRemaining();
this.updateHeader(days);
$content.image = avatar ?? '';
$content.name = subscription.account?.name ?? '';
$content.state = subscription.state;
$content.plan = subscription.plan.effective.name;
$content.days = days;
}
}

+ 266
- 0
src/webviews/apps/plus/account/components/account-content.ts View File

@ -0,0 +1,266 @@
import { css, html, LitElement, nothing } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { SubscriptionState } from '../../../../../subscription';
import { pluralize } from '../../../../../system/string';
import { elementBase, linkBase } from '../../../shared/components/styles/lit/base.css';
import '../../../shared/components/button';
import '../../../shared/components/button-container';
import '../../../shared/components/code-icon';
@customElement('account-content')
export class AccountContent extends LitElement {
static override styles = [
elementBase,
linkBase,
css`
:host {
display: block;
margin-bottom: 1.3rem;
}
button-container {
margin-bottom: 1.3rem;
}
.account {
position: relative;
display: grid;
gap: 0 0.8rem;
grid-template-columns: 3.4rem auto;
grid-auto-flow: column;
margin-bottom: 1.3rem;
}
.account__media {
grid-column: 1;
grid-row: 1 / span 2;
display: flex;
align-items: center;
}
.account__image {
width: 100%;
aspect-ratio: 1 / 1;
border-radius: 50%;
}
.account__title {
font-size: var(--vscode-font-size);
font-weight: 600;
margin: 0;
}
.account__access {
position: relative;
margin: 0;
color: var(--color-foreground--65);
}
.repo-access {
font-size: 1.1em;
margin-right: 0.2rem;
}
.repo-access:not(.is-pro) {
filter: grayscale(1) brightness(0.7);
}
`,
];
@property()
image = '';
@property()
name = '';
@property({ type: Number })
days = 0;
@property({ type: Number })
state: SubscriptionState = SubscriptionState.Free;
@property()
plan = '';
get daysRemaining() {
if (this.days < 1) {
return '<1 day';
}
return pluralize('day', this.days);
}
get planName() {
switch (this.state) {
case SubscriptionState.Free:
case SubscriptionState.FreePreviewTrialExpired:
case SubscriptionState.FreePlusTrialExpired:
return 'GitLens Free';
case SubscriptionState.FreeInPreviewTrial:
case SubscriptionState.FreePlusInTrial:
return 'GitLens Pro (Trial)';
case SubscriptionState.VerificationRequired:
return `${this.plan} (Unverified)`;
default:
return this.plan;
}
}
get daysLeft() {
switch (this.state) {
case SubscriptionState.FreeInPreviewTrial:
case SubscriptionState.FreePlusInTrial:
return `, ${this.daysRemaining} left`;
default:
return '';
}
}
get hasAccount() {
switch (this.state) {
case SubscriptionState.Free:
case SubscriptionState.FreePreviewTrialExpired:
case SubscriptionState.FreeInPreviewTrial:
return false;
}
return true;
}
get isPro() {
switch (this.state) {
case SubscriptionState.Free:
case SubscriptionState.FreePreviewTrialExpired:
case SubscriptionState.FreePlusTrialExpired:
case SubscriptionState.VerificationRequired:
return false;
}
return true;
}
private renderAccountInfo() {
if (this.state === SubscriptionState.FreeInPreviewTrial) {
return html`
<div class="account">
<div class="account__media">
<code-icon icon="account" size="34"></code-icon>
</div>
<p class="account__title"></p>
<p class="account__access"><span class="repo-access"></span>${this.planName}${this.daysLeft}</p>
</div>
`;
}
if (!this.hasAccount) {
return nothing;
}
return html`
<div class="account">
<div class="account__media">
${this.image ? html`<img src=${this.image} class="account__image" />` : nothing}
</div>
<p class="account__title">${this.name}</p>
<p class="account__access">
<span class="repo-access${this.isPro ? ' is-pro' : ''}"></span>${this.planName}${this.daysLeft}
</p>
</div>
`;
}
private renderAccountNavigation() {
if (!this.hasAccount) {
return nothing;
}
return html`
<button-container>
<gk-button full href="command:gitlens.plus.manage">Manage Account</gk-button>
<gk-button href="command:gitlens.plus.logout"
><code-icon icon="sign-out" title="Sign Out" aria-label="Sign Out"></code-icon
></gk-button>
</button-container>
`;
}
private renderAccountState() {
switch (this.state) {
case SubscriptionState.VerificationRequired:
return html`
<p>You must verify your email before you can continue.</p>
<button-container>
<gk-button full href="command:gitlens.plus.resendVerification"
>Resend verification email</gk-button
>
</button-container>
<button-container>
<gk-button full href="command:gitlens.plus.validate">Refresh verification status</gk-button>
</button-container>
`;
case SubscriptionState.Free:
return html`
<p>
A GitLens Pro subscription enables services that increase productivity, focus and collaboration.
</p>
<p>
Start a trial to access these services or
<a href="command:gitlens.plus.loginOrSignUp">sign in</a>.
</p>
<button-container>
<gk-button full href="command:gitlens.plus.loginOrSignUp">Start Free Pro Trial</gk-button>
</button-container>
<p>
A trial or subscription is required for use.<br />
A trial or subscription is required for use on privately hosted repos.
</p>
`;
case SubscriptionState.FreePreviewTrialExpired:
return html`
<p>
Your free 3-day Pro trial has ended, extend your free trial to get an additional 7-days, or
<a href="command:gitlens.plus.loginOrSignUp">sign in</a>.
</p>
<button-container>
<gk-button full href="command:gitlens.plus.loginOrSignUp">Extend Free Pro Trial</gk-button>
</button-container>
<p>
A trial or subscription is required for use.<br />
A trial or subscription is required for use on privately hosted repos.
</p>
`;
case SubscriptionState.FreePlusTrialExpired:
return html`
<p>Your Pro trial has ended, please upgrade to continue to use this on privately hosted repos.</p>
<button-container>
<gk-button full href="command:gitlens.plus.purchase">Upgrade to Pro</gk-button>
</button-container>
<p>
A trial or subscription is required for use.<br />
A trial or subscription is required for use on privately hosted repos.
</p>
`;
case SubscriptionState.FreeInPreviewTrial:
case SubscriptionState.FreePlusInTrial:
return html`
<p>
Your have ${this.daysRemaining} remaining in your Pro trial. Once your trial ends, you'll need a
paid plan to continue using features.
</p>
<button-container>
<gk-button full href="command:gitlens.plus.purchase">Upgrade to Pro</gk-button>
</button-container>
<p>
A trial or subscription is required for use.<br />
A trial or subscription is required for use on privately hosted repos.
</p>
`;
}
return nothing;
}
override render() {
return html`${this.renderAccountInfo()}${this.renderAccountState()}${this.renderAccountNavigation()}`;
}
}

+ 0
- 378
src/webviews/apps/plus/account/components/header-card.ts View File

@ -1,378 +0,0 @@
import { attr, css, customElement, FASTElement, html, ref, volatile, when } from '@microsoft/fast-element';
import { SubscriptionState } from '../../../../../subscription';
import { pluralize } from '../../../../../system/string';
import { numberConverter } from '../../../shared/components/converters/number-converter';
import '../../../shared/components/code-icon';
const template = html<HeaderCard>`
<div class="header-card__media"><img class="header-card__image" src="${x => x.image}" alt="GitLens Logo" /></div>
<h1 class="header-card__title${x => (x.name === '' ? ' logo' : '')}">
${when(x => x.name === '', html<HeaderCard>`Git<span class="brand">Lens</span> 13`)}
${when(x => x.name !== '', html<HeaderCard>`${x => x.name}`)}
</h1>
<p class="header-card__account">
<span class="status">
<span ${ref('statusNode')} tabindex="-1" class="status-label"
><span class="repo-access${x => (x.isPro ? ' is-pro' : '')}"></span>${x =>
`${x.planName}${x.daysLeft}`}</span
>
</span>
<span class="account-actions">
${when(
x => !x.hasAccount,
html<HeaderCard>`<a class="action" href="command:gitlens.plus.loginOrSignUp">Sign In</a>`,
)}
${when(
x => x.hasAccount,
html<HeaderCard>`
<a
class="action is-icon"
href="command:gitlens.plus.manage"
aria-label="Manage Account"
title="Manage Account"
><code-icon icon="account"></code-icon></a
>&nbsp;<a
class="action is-icon"
href="command:gitlens.plus.logout"
aria-label="Sign Out"
title="Sign Out"
><code-icon icon="sign-out"></code-icon
></a>
`,
)}
</span>
</p>
<p class="features">
${x =>
x.isPro
? 'You have access to all GitLens features on any repo.'
: 'You have access to ✨ features on local & public repos, and all other GitLens features on any repo.'}
<br /><br />
indicates a subscription is required to use this feature on privately hosted repos.
<a class="link-inline" href="command:gitlens.plus.learn">learn more</a>
</p>
<div
class="progress header-card__progress"
role="progressbar"
aria-valuemax="${x => x.progressMax}"
aria-valuenow="${x => x.progressNow}"
aria-label="${x => x.progressNow} of ${x => x.progressMax} steps completed"
hidden
>
<div ${ref('progressNode')} class="progress__indicator"></div>
</div>
<span class="actions">
${when(
x => x.state === SubscriptionState.FreePreviewTrialExpired,
html<HeaderCard>`<a class="action is-primary" href="command:gitlens.plus.loginOrSignUp"
>Extend Pro Trial</a
>`,
)}
${when(
x =>
x.state === SubscriptionState.FreeInPreviewTrial ||
x.state === SubscriptionState.FreePlusInTrial ||
x.state === SubscriptionState.FreePlusTrialExpired,
html<HeaderCard>`<a class="action is-primary" href="command:gitlens.plus.purchase">Upgrade to Pro</a>`,
)}
${when(
x => x.state === SubscriptionState.VerificationRequired,
html<HeaderCard>`
<a
class="action is-primary"
href="command:gitlens.plus.resendVerification"
title="Resend Verification Email"
aria-label="Resend Verification Email"
>Verify</a
>&nbsp;<a
class="action"
href="command:gitlens.plus.validate"
title="Refresh Verification Status"
aria-label="Refresh Verification Status"
>Refresh</a
>
`,
)}
</span>
`;
const styles = css`
* {
box-sizing: border-box;
}
:host {
position: relative;
display: grid;
padding: 1rem 0 1.2rem;
gap: 0 0.8rem;
grid-template-columns: 3.4rem auto;
grid-auto-flow: column;
}
a {
color: var(--vscode-textLink-foreground);
text-decoration: none;
}
a:focus {
outline-color: var(--focus-border);
}
a:hover {
text-decoration: underline;
}
.header-card__media {
grid-column: 1;
grid-row: 1 / span 2;
display: flex;
align-items: center;
}
.header-card__image {
width: 100%;
aspect-ratio: 1 / 1;
border-radius: 50%;
}
.header-card__title {
font-size: var(--vscode-font-size);
font-weight: 600;
margin: 0;
}
.header-card__title.logo {
font-family: 'Segoe UI Semibold', var(--font-family);
font-size: 1.5rem;
}
.header-card__account {
position: relative;
margin: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0 0.4rem;
}
.features {
grid-column: 1 / 3;
grid-row: 3;
}
.progress {
width: 100%;
overflow: hidden;
}
:host-context(.vscode-high-contrast) .progress,
:host-context(.vscode-dark) .progress {
background-color: var(--color-background--lighten-15);
}
:host-context(.vscode-high-contrast-light) .progress,
:host-context(.vscode-light) .progress {
background-color: var(--color-background--darken-15);
}
.progress__indicator {
height: 4px;
background-color: var(--vscode-progressBar-background);
}
.header-card__progress {
position: absolute;
bottom: 0;
left: 0;
}
.brand {
color: var(--gitlens-brand-color-2);
}
.status {
color: var(--color-foreground--65);
}
.repo-access {
font-size: 1.1em;
margin-right: 0.2rem;
}
.repo-access:not(.is-pro) {
filter: grayscale(1) brightness(0.7);
}
.actions {
position: absolute;
right: 0.1rem;
top: 0.1rem;
}
.action {
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 0.3rem;
color: var(--color-foreground--75);
}
:host-context(.vscode-high-contrast) .action.is-primary,
:host-context(.vscode-dark) .action.is-primary {
border: 1px solid var(--color-background--lighten-15);
}
:host-context(.vscode-high-contrast-light) .action.is-primary,
:host-context(.vscode-light) .action.is-primary {
border: 1px solid var(--color-background--darken-15);
}
.action.is-icon {
display: inline-flex;
justify-content: center;
align-items: center;
width: 2.2rem;
height: 2.2rem;
padding: 0;
}
.action:hover {
text-decoration: none;
color: var(--color-foreground);
}
:host-context(.vscode-high-contrast) .action:hover,
:host-context(.vscode-dark) .action:hover {
background-color: var(--color-background--lighten-10);
}
:host-context(.vscode-high-contrast-light) .action:hover,
:host-context(.vscode-light) .action:hover {
background-color: var(--color-background--darken-10);
}
.link-inline {
color: inherit;
text-decoration: underline;
}
.link-inline:hover {
color: var(--vscode-textLink-foreground);
}
`;
@customElement({ name: 'header-card', template: template, styles: styles })
export class HeaderCard extends FASTElement {
@attr
image = '';
@attr
name = '';
@attr({ converter: numberConverter })
days = 0;
@attr({ converter: numberConverter })
steps = 4;
@attr({ converter: numberConverter })
completed = 0;
@attr({ converter: numberConverter })
state: SubscriptionState = SubscriptionState.Free;
@attr
plan = '';
@attr({ attribute: 'pin-status', mode: 'boolean' })
pinStatus = true;
progressNode!: HTMLElement;
statusNode!: HTMLElement;
override attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
super.attributeChangedCallback(name, oldValue, newValue);
if (oldValue === newValue || this.progressNode == null) {
return;
}
this.updateProgressWidth();
}
get daysRemaining() {
if (this.days < 1) {
return '<1 day';
}
return pluralize('day', this.days);
}
get progressNow() {
return this.completed + 1;
}
get progressMax() {
return this.steps + 1;
}
@volatile
get progress() {
return `${(this.progressNow / this.progressMax) * 100}%`;
}
@volatile
get planName() {
switch (this.state) {
case SubscriptionState.Free:
case SubscriptionState.FreePreviewTrialExpired:
case SubscriptionState.FreePlusTrialExpired:
return 'GitLens Free';
case SubscriptionState.FreeInPreviewTrial:
case SubscriptionState.FreePlusInTrial:
return 'GitLens Pro (Trial)';
case SubscriptionState.VerificationRequired:
return `${this.plan} (Unverified)`;
default:
return this.plan;
}
}
@volatile
get daysLeft() {
switch (this.state) {
case SubscriptionState.FreeInPreviewTrial:
case SubscriptionState.FreePlusInTrial:
return `, ${this.daysRemaining} left`;
default:
return '';
}
}
get hasAccount() {
switch (this.state) {
case SubscriptionState.Free:
case SubscriptionState.FreePreviewTrialExpired:
case SubscriptionState.FreeInPreviewTrial:
return false;
}
return true;
}
get isPro() {
switch (this.state) {
case SubscriptionState.Free:
case SubscriptionState.FreePreviewTrialExpired:
case SubscriptionState.FreePlusTrialExpired:
case SubscriptionState.VerificationRequired:
return false;
}
return true;
}
updateProgressWidth() {
this.progressNode.style.width = this.progress;
}
dismissStatus(_e: MouseEvent) {
this.pinStatus = false;
this.$emit('dismiss-status');
window.requestAnimationFrame(() => {
this.statusNode?.focus();
});
}
}

+ 0
- 129
src/webviews/apps/plus/account/components/plus-content.ts View File

@ -1,129 +0,0 @@
import { attr, css, customElement, FASTElement, html, volatile, when } from '@microsoft/fast-element';
import { SubscriptionState } from '../../../../../subscription';
import { pluralize } from '../../../../../system/string';
import { numberConverter } from '../../../shared/components/converters/number-converter';
import '../../../shared/components/code-icon';
const template = html<PlusContent>`
<div class="icon"><code-icon icon="info"></code-icon></div>
<div class="content">
${when(
x => x.state === SubscriptionState.Free,
html<PlusContent>`
<p class="mb-1">
<a title="Learn more about GitLens+ features" href="command:gitlens.plus.learn"
>GitLens+ features</a
>
are free for local and public repos, no account required, while upgrading to GitLens Pro gives you
access on private repos.
</p>
<p class="mb-0">All other GitLens features can always be used on any repo.</p>
`,
)}
${when(
x => x.state !== SubscriptionState.Free,
html<PlusContent>` <p class="mb-0">All other GitLens features can always be used on any repo</p> `,
)}
</div>
`;
const styles = css`
* {
box-sizing: border-box;
}
:host {
display: flex;
flex-direction: row;
padding: 0.8rem 1.2rem;
background-color: var(--color-alert-neutralBackground);
border-left: 0.3rem solid var(--color-foreground--50);
color: var(--color-alert-foreground);
}
a {
color: var(--vscode-textLink-foreground);
text-decoration: none;
}
a:focus {
outline-color: var(--focus-border);
}
a:hover {
text-decoration: underline;
}
p {
margin-top: 0;
}
.icon {
display: none;
flex: none;
margin-right: 0.4rem;
}
.icon code-icon {
font-size: 2.4rem;
margin-top: 0.2rem;
}
.content {
font-size: 1.2rem;
line-height: 1.2;
text-align: left;
}
.mb-1 {
margin-bottom: 0.8rem;
}
.mb-0 {
margin-bottom: 0;
}
`;
@customElement({ name: 'plus-content', template: template, styles: styles })
export class PlusContent extends FASTElement {
@attr({ converter: numberConverter })
days = 0;
@attr({ converter: numberConverter })
state: SubscriptionState = SubscriptionState.Free;
@attr
plan = '';
@attr
visibility: 'local' | 'public' | 'mixed' | 'private' = 'public';
get daysRemaining() {
if (this.days < 1) {
return 'less than one day';
}
return pluralize('day', this.days);
}
get isFree() {
return ['local', 'public'].includes(this.visibility);
}
@volatile
get planName() {
switch (this.state) {
case SubscriptionState.Free:
case SubscriptionState.FreePreviewTrialExpired:
case SubscriptionState.FreePlusTrialExpired:
return 'GitLens Free';
case SubscriptionState.FreeInPreviewTrial:
case SubscriptionState.FreePlusInTrial:
return 'GitLens Pro (Trial)';
case SubscriptionState.VerificationRequired:
return `${this.plan} (Unverified)`;
default:
return this.plan;
}
}
fireAction(command: string) {
this.$emit('action', command);
}
}

+ 14
- 0
src/webviews/apps/shared/components/styles/lit/base.css.ts View File

@ -1,4 +1,5 @@
import { css } from 'lit';
import { focusOutline } from './a11y.css';
export const elementBase = css`
:host {
@ -13,3 +14,16 @@ export const elementBase = css`
display: none !important;
}
`;
export const linkBase = css`
a {
color: var(--vscode-textLink-foreground);
text-decoration: none;
}
a:focus {
${focusOutline}
}
a:hover {
text-decoration: underline;
}
`;

Loading…
Cancel
Save