Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MU WPCOM: dashboard: add launchpad #41138

Merged
merged 10 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
743 changes: 738 additions & 5 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Dashboard: add launchpad
5 changes: 5 additions & 0 deletions projects/packages/jetpack-mu-wpcom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@babel/core": "7.26.0",
"@babel/plugin-transform-react-jsx": "7.25.9",
"@babel/preset-react": "7.26.3",
"@babel/runtime": "7.24.7",
"@playwright/test": "1.48.2",
"@types/node": "^20.4.2",
"@types/react": "^18.2.28",
Expand All @@ -48,9 +49,11 @@
"dependencies": {
"@automattic/calypso-color-schemes": "3.1.3",
"@automattic/color-studio": "4.0.0",
"@automattic/components": "2.2.0",
"@automattic/i18n-utils": "1.2.3",
"@automattic/jetpack-base-styles": "workspace:*",
"@automattic/jetpack-shared-extension-utils": "workspace:*",
"@automattic/launchpad": "1.1.0",
"@automattic/page-pattern-modal": "1.1.5",
"@automattic/typography": "1.0.0",
"@popperjs/core": "^2.11.8",
Expand All @@ -73,6 +76,8 @@
"@wordpress/url": "4.16.0",
"clsx": "2.1.1",
"debug": "4.4.0",
"events": "^3.3.0",
"i18n-calypso": "7.0.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is interesting, we have some components that use this, and others that use @wordpress/i18n. What's the story there? Maybe @jsnajdr knows. How to decide what to use...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I think ideally we should switch to @wordpress/i18n. But I don't want to trigger string changes for this PR. Also, there's some use cases that can't be easily done with @wordpress/i18n such as strings with HTML elements inside of them (I think).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, there's some use cases that can't be easily done with @wordpress/i18n such as strings with HTML elements inside of them (I think).

I think we have createInterpolateElement for that use-case. So I think we're covered there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, looking a that, it does work a bit differently, and the strings would still have to change. I guess that could be fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be wrong, but I don't think that translation strings using i18n-calypso's translate() function would even be picked up, only the @wordpress/i18n-style __(), _x(), and so on.

We've generally avoided @automattic/components for similar reasons. It has (or has had in the past, anyway) a lot of stuff that's specific to Calypso that doesn't work so well in Jetpack.

"preact": "^10.13.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { Gridicon, ConfettiAnimation } from '@automattic/components';
import { Button, Modal, Tooltip } from '@wordpress/components';
import { useCopyToClipboard } from '@wordpress/compose';
import { useState, useEffect } from '@wordpress/element';
import { Icon, copy } from '@wordpress/icons';
import { useTranslate } from 'i18n-calypso';
import { wpcomTrackEvent } from '../../../common/tracks';

import './celebrate-launch-modal.scss';

/**
* CelebrateLaunchModal component
*
* @param {object} props - Props.
* @param {Function} props.onRequestClose - Callback on modal close.
* @param {object} props.sitePlan - The site plan.
* @param {string} props.siteDomain - The site domain.
* @param {string} props.siteUrl - The site URL.
* @param {boolean} props.hasCustomDomain - Whether the site has a custom domain.
*
* @return {JSX.Element} The CelebrateLaunchModal component.
*/
export default function CelebrateLaunchModal( {
onRequestClose,
sitePlan = {},
siteDomain: siteSlug,
siteUrl,
hasCustomDomain,
} ) {
const translate = useTranslate();
const isPaidPlan = ! sitePlan.is_free;
const isBilledMonthly = sitePlan.product_slug?.includes( 'monthly' );
const [ clipboardCopied, setClipboardCopied ] = useState( false );

useEffect( () => {
wpcomTrackEvent( `calypso_launchpad_celebration_modal_view`, {
product_slug: sitePlan.product_slug,
} );
}, [ sitePlan.product_slug ] );

/**
* Render the upsell content.
*
* @return {JSX.Element} The upsell content.
*/
function renderUpsellContent() {
let contentElement;
let buttonText;
let buttonHref;

if ( ! isPaidPlan && ! hasCustomDomain ) {
contentElement = (
<p>
{ translate(
'Supercharge your website with a {{strong}}custom address{{/strong}} that matches your blog, brand, or business.',
{ components: { strong: <strong /> } }
) }
</p>
);
buttonText = translate( 'Claim your domain' );
buttonHref = `https://wordpress.com/domains/add/${ siteSlug }`;
} else if ( isPaidPlan && isBilledMonthly && ! hasCustomDomain ) {
contentElement = (
<p>
{ translate(
'Interested in a custom domain? It’s free for the first year when you switch to annual billing.'
) }
</p>
);
buttonText = translate( 'Claim your domain' );
buttonHref = `https://wordpress.com/domains/add/${ siteSlug }`;
} else if ( isPaidPlan && ! hasCustomDomain ) {
contentElement = (
<p>
{ translate(
'Your paid plan includes a domain name {{strong}}free for one year{{/strong}}. Choose one that’s easy to remember and even easier to share.',
{ components: { strong: <strong /> } }
) }
</p>
);
buttonText = translate( 'Claim your free domain' );
buttonHref = `https://wordpress.com/domains/add/${ siteSlug }`;
} else if ( hasCustomDomain ) {
return null;
}

return (
<div className="launched__modal-upsell">
<div className="launched__modal-upsell-content">{ contentElement }</div>
<Button
variant="primary"
href={ buttonHref }
onClick={ () =>
wpcomTrackEvent( `calypso_launchpad_celebration_modal_upsell_clicked`, {
product_slug: sitePlan.product_slug,
} )
}
>
<span>{ buttonText }</span>
</Button>
</div>
);
}

const ref = useCopyToClipboard( siteSlug, () => setClipboardCopied( true ) );

return (
<Modal onRequestClose={ onRequestClose } className="launched__modal">
<ConfettiAnimation />
<div className="launched__modal-content">
<div className="launched__modal-text">
<h1 className="launched__modal-heading">
{ translate( 'Congrats, your site is live!' ) }
</h1>
<p className="launched__modal-body">
{ translate( 'Now you can head over to your site and share it with the world.' ) }
</p>
</div>
<div className="launched__modal-actions">
<div className="launched__modal-site">
<div className="launched__modal-domain">
<p className="launched__modal-domain-text">{ siteSlug }</p>
<Tooltip
text={ clipboardCopied ? translate( 'Copied to clipboard!' ) : '' }
delay={ 0 }
hideOnClick={ false }
>
<Button
label={ translate( 'Copy URL' ) }
className="launchpad__clipboard-button"
borderless
size="compact"
ref={ ref }
onMouseLeave={ () => setClipboardCopied( false ) }
>
<Icon icon={ copy } size={ 18 } />
</Button>
</Tooltip>
</div>

<Button href={ siteUrl } target="_blank" className="launched__modal-view-site">
<Gridicon icon="domains" size={ 18 } />
<span className="launched__modal-view-site-text">{ translate( 'View site' ) }</span>
</Button>
</div>
</div>
</div>
{ renderUpsellContent() }
</Modal>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
$breakpoint-mobile: 782px; //Mobile size.

.launched__modal {
p {
font-size: 16px;
margin: 0;
}

&.components-modal__frame {
max-width: 640px;
}

.components-modal__header {
padding: 48px;
}

.components-modal__content {
margin: 0;
padding: 0;
}

&-content {
padding: 48px;
padding-bottom: 40px;
display: flex;
flex-direction: column;
gap: 32px;

@media (min-width: $breakpoint-mobile) {
min-width: 640px;
}
}

&-heading {
color: var(--studio-gray-100);
font-family: Recoleta, "Noto Serif", Georgia, "Times New Roman", Times, serif;
font-size: 2rem;
line-height: 40px;
font-weight: 400;
letter-spacing: 0.2px;
margin: 0;
padding-bottom: 8px;
}

&-domain {
display: flex;
justify-content: center;
align-items: center;
max-width: 100%;
@media (min-width: $breakpoint-mobile) {
max-width: 75%;
}
}

&-body,
&-domain-text {
margin: 0;
color: var(--studio-gray-80);
font-size: 1rem;
line-height: 24px;
}

&-domain-text {
padding: 0 8px;

// prevent text overflow
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}

&-buttons {
display: flex;
flex-direction: row;
justify-content: end;
gap: 16px;
}

&-site {
padding: 8px;
background: var(--studio-gray-0);
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
@media (min-width: $breakpoint-mobile) {
flex-direction: row;
}
}

.launchpad__clipboard-button {
min-width: 18px;
opacity: 0;
}

.launchpad__clipboard-button:focus {
opacity: 1;
}

&-site:hover {
.launchpad__clipboard-button {
opacity: 1;
}
}

&-customize {
color: var(--studio-blue-50);
font-size: 0.875rem;
display: inline-flex;
flex-direction: row;
justify-content: start;
gap: 6px;
padding: 0;
margin: 0;
margin-top: 16px;
}

&-upsell {
border-top: 1px solid var(--studio-gray-5);
background: var(--studio-gray-0);
display: flex;
justify-content: center;
align-items: center;
padding: 32px 48px;
gap: 32px;
flex-direction: column;
@media (min-width: $breakpoint-mobile) {
flex-direction: row;
}
.components-button {
height: 42px;
}
}

&-upsell-content p {
margin-bottom: 0;
}

&-upsell-content-highlight {
font-weight: bold;
}

&-view-site {
display: flex;
gap: 4px;

font-weight: 500;
/* stylelint-disable-next-line */
font-size: 14px;
line-height: 20px;
letter-spacing: -0.154px;
color: #101517;
}

&-view-site:visited {
color: #101517;
}

&-view-site:hover {
text-decoration: underline;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import '../../common/public-path';
import React from 'react';
import ReactDOM from 'react-dom/client';
import CelebrateLaunchModal from './celebrate-launch/celebrate-launch-modal';
import WpcomLaunchpadWidget from './wpcom-launchpad-widget';
import WpcomSiteManagementWidget from './wpcom-site-management-widget';

const data = typeof window === 'object' ? window.JETPACK_MU_WPCOM_DASHBOARD_WIDGETS : {};

const widgets = [
{
id: 'wpcom_launchpad_widget_main',
Widget: WpcomLaunchpadWidget,
},
{
id: 'wpcom_site_preview_widget_main',
Widget: WpcomSiteManagementWidget,
Expand All @@ -19,3 +24,21 @@ widgets.forEach( ( { id, Widget } ) => {
root.render( <Widget { ...data } /> );
}
} );

const url = new URL( window.location.href );
if ( url.searchParams.has( 'celebrate-launch' ) ) {
url.searchParams.delete( 'celebrate-launch' );
window.history.replaceState( null, '', url.toString() );
const rootElement = document.createElement( 'div' );
document.body.appendChild( rootElement );
const root = ReactDOM.createRoot( rootElement );
root.render(
<CelebrateLaunchModal
{ ...data }
onRequestClose={ () => {
root.unmount();
rootElement.remove();
} }
/>
);
}
Loading
Loading