mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-29 15:12:58 +03:00
Wired UI for multiple products with site data
refs https://github.com/TryGhost/Team/issues/767 - wires real products data from site/fixtures to multiple products UI - adds new helpers to manage price display/UI - cleans up switch default params to clear type errors
This commit is contained in:
parent
7a65caa806
commit
8d7988a334
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React, {useContext, useState} from 'react';
|
||||||
import Switch from '../common/Switch';
|
import Switch from '../common/Switch';
|
||||||
import {isCookiesDisabled} from '../../utils/helpers';
|
import {getCurrencySymbol, getPriceString, getProducts, getStripeAmount, isCookiesDisabled} from '../../utils/helpers';
|
||||||
|
import AppContext from '../../AppContext';
|
||||||
|
|
||||||
export const ProductsSectionStyles = `
|
export const ProductsSectionStyles = `
|
||||||
.gh-portal-products {
|
.gh-portal-products {
|
||||||
@ -28,7 +29,7 @@ export const ProductsSectionStyles = `
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gh-portal-products-priceswitch .gh-portal-for-switch .input-toggle-component,
|
.gh-portal-products-priceswitch .gh-portal-for-switch .input-toggle-component,
|
||||||
.gh-portal-products-priceswitch .gh-portal-for-switch label:hover input:not(:checked) + .input-toggle-component,
|
.gh-portal-products-priceswitch .gh-portal-for-switch label:hover input:not(:checked) + .input-toggle-component,
|
||||||
.gh-portal-products-priceswitch .gh-portal-for-switch .container:hover input:not(:checked) + .input-toggle-component {
|
.gh-portal-products-priceswitch .gh-portal-for-switch .container:hover input:not(:checked) + .input-toggle-component {
|
||||||
background: var(--grey1);
|
background: var(--grey1);
|
||||||
border-color: var(--grey1);
|
border-color: var(--grey1);
|
||||||
@ -60,7 +61,7 @@ export const ProductsSectionStyles = `
|
|||||||
padding: 32px 5vw;
|
padding: 32px 5vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.gh-portal-product-card {
|
.gh-portal-product-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -222,7 +223,7 @@ export const ProductsSectionStyles = `
|
|||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-portal-popup-container.fullscreen footer.gh-portal-signup-footer,
|
.gh-portal-popup-container.fullscreen footer.gh-portal-signup-footer,
|
||||||
.gh-portal-popup-container.fullscreen footer.gh-portal-signin-footer {
|
.gh-portal-popup-container.fullscreen footer.gh-portal-signin-footer {
|
||||||
padding: 0 32px !important;
|
padding: 0 32px !important;
|
||||||
margin-top: 32px;
|
margin-top: 32px;
|
||||||
@ -234,12 +235,18 @@ export const ProductsSectionStyles = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const ProductsContext = React.createContext({
|
||||||
|
selectedInterval: 'month',
|
||||||
|
selectedProduct: 'free',
|
||||||
|
setSelectedProduct: null
|
||||||
|
});
|
||||||
|
|
||||||
function productColumns() {
|
function productColumns() {
|
||||||
const noOfProducts = 4;
|
const noOfProducts = 4;
|
||||||
return noOfProducts > 5 ? 5 : noOfProducts;
|
return noOfProducts > 5 ? 5 : noOfProducts;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Checkbox({name, id, onPlanSelect, isChecked, disabled = false}) {
|
function Checkbox({name, id, onProductSelect, isChecked, disabled = false}) {
|
||||||
if (isCookiesDisabled()) {
|
if (isCookiesDisabled()) {
|
||||||
disabled = true;
|
disabled = true;
|
||||||
}
|
}
|
||||||
@ -250,87 +257,169 @@ function Checkbox({name, id, onPlanSelect, isChecked, disabled = false}) {
|
|||||||
key={id}
|
key={id}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isChecked}
|
checked={isChecked}
|
||||||
|
onChange={(e) => {
|
||||||
|
onProductSelect(e, id);
|
||||||
|
}}
|
||||||
aria-label={name}
|
aria-label={name}
|
||||||
onChange={e => onPlanSelect(e, id)}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<span className='checkmark'></span>
|
<span className='checkmark' onClick={(e) => {
|
||||||
|
if (!disabled) {
|
||||||
|
onProductSelect(e, id);
|
||||||
|
}
|
||||||
|
}}></span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProductCardFooter({product}) {
|
||||||
|
const {selectedInterval} = useContext(ProductsContext);
|
||||||
|
const monthlyPrice = product.monthlyPrice;
|
||||||
|
const yearlyPrice = product.yearlyPrice;
|
||||||
|
const activePrice = selectedInterval === 'month' ? monthlyPrice : yearlyPrice;
|
||||||
|
const alternatePrice = selectedInterval === 'month' ? yearlyPrice : monthlyPrice;
|
||||||
|
if (!monthlyPrice || !yearlyPrice) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="gh-portal-product-card-footer">
|
||||||
|
<div className="gh-portal-product-price">
|
||||||
|
<span className="currency-sign">{getCurrencySymbol(activePrice.currency)}</span>
|
||||||
|
<span className="amount">{getStripeAmount(activePrice.amount)}</span>
|
||||||
|
<span className="billing-period">/{activePrice.interval}</span>
|
||||||
|
</div>
|
||||||
|
<div className="gh-portal-product-alternative-price">{getPriceString(alternatePrice)}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProductCard({product}) {
|
||||||
|
const {selectedProduct, setSelectedProduct} = useContext(ProductsContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="gh-portal-product-card" key={product.id}>
|
||||||
|
<div className="gh-portal-product-card-header">
|
||||||
|
<Checkbox name={product.id} id={`${product.id}-checkbox`} isChecked={selectedProduct === product.id} onProductSelect={() => {
|
||||||
|
setSelectedProduct(product.id);
|
||||||
|
}} />
|
||||||
|
<h4 className="gh-portal-product-name">{product.name}</h4>
|
||||||
|
<div className="gh-portal-product-description">{product.description}</div>
|
||||||
|
</div>
|
||||||
|
<ProductCardFooter product={product} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProductCards({products}) {
|
||||||
|
return products.map((product) => {
|
||||||
|
return (
|
||||||
|
<ProductCard product={product} key={product.id} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function DummyProductCards({products}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="gh-portal-product-card checked">
|
||||||
|
<div className="gh-portal-product-card-header">
|
||||||
|
<Checkbox name='x' id='x' isChecked={true} onPlanSelect='' />
|
||||||
|
<h4 className="gh-portal-product-name">Bronze</h4>
|
||||||
|
<div className="gh-portal-product-description">Access to all members articles</div>
|
||||||
|
</div>
|
||||||
|
<div className="gh-portal-product-card-footer">
|
||||||
|
<div className="gh-portal-product-price">
|
||||||
|
<span className="currency-sign">$</span>
|
||||||
|
<span className="amount">70</span>
|
||||||
|
<span className="billing-period">/year</span>
|
||||||
|
</div>
|
||||||
|
<div className="gh-portal-product-alternative-price">$7/month</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="gh-portal-product-card">
|
||||||
|
<div className="gh-portal-product-card-header">
|
||||||
|
<Checkbox name='x' id='x' isChecked={false} onPlanSelect='' />
|
||||||
|
<h4 className="gh-portal-product-name">Silver</h4>
|
||||||
|
<div className="gh-portal-product-description">Access to all members articles and weekly podcast</div>
|
||||||
|
</div>
|
||||||
|
<div className="gh-portal-product-card-footer">
|
||||||
|
<div className="gh-portal-product-price">
|
||||||
|
<span className="currency-sign">$</span>
|
||||||
|
<span className="amount">120</span>
|
||||||
|
<span className="billing-period">/year</span>
|
||||||
|
</div>
|
||||||
|
<div className="gh-portal-product-alternative-price">$12/month</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="gh-portal-product-card">
|
||||||
|
<div className="gh-portal-product-card-header">
|
||||||
|
<Checkbox name='x' id='x' isChecked={false} onPlanSelect='' />
|
||||||
|
<h4 className="gh-portal-product-name">Gold</h4>
|
||||||
|
<div className="gh-portal-product-description">Access to all members articles, weekly podcast and exclusive interviews</div>
|
||||||
|
</div>
|
||||||
|
<div className="gh-portal-product-card-footer">
|
||||||
|
<div className="gh-portal-product-price">
|
||||||
|
<span className="currency-sign">$</span>
|
||||||
|
<span className="amount">1000</span>
|
||||||
|
<span className="billing-period">/year</span>
|
||||||
|
</div>
|
||||||
|
<div className="gh-portal-product-alternative-price">$12/month</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FreeProductCard() {
|
||||||
|
const {selectedProduct, setSelectedProduct} = useContext(ProductsContext);
|
||||||
|
return (
|
||||||
|
<div className="gh-portal-product-card">
|
||||||
|
<div className="gh-portal-product-card-header">
|
||||||
|
<Checkbox name='x' id='x' isChecked={selectedProduct === 'free'} onProductSelect={() => {
|
||||||
|
setSelectedProduct('free');
|
||||||
|
}} />
|
||||||
|
<h4 className="gh-portal-product-name">Free</h4>
|
||||||
|
<div className="gh-portal-product-description">Free preview</div>
|
||||||
|
</div>
|
||||||
|
<div className="gh-portal-product-card-footer">
|
||||||
|
<div className="gh-portal-product-price">
|
||||||
|
<span className="currency-sign">$</span>
|
||||||
|
<span className="amount">0</span>
|
||||||
|
</div>
|
||||||
|
<div className="gh-portal-product-alternative-price"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProductsSection() {
|
function ProductsSection() {
|
||||||
|
const {site} = useContext(AppContext);
|
||||||
|
const products = getProducts({site});
|
||||||
|
const [selectedInterval, setSelectedInterval] = useState('month');
|
||||||
|
const [selectedProduct, setSelectedProduct] = useState('free');
|
||||||
|
const checked = selectedInterval === 'year';
|
||||||
return (
|
return (
|
||||||
<section className="gh-portal-products">
|
<ProductsContext.Provider value={{
|
||||||
|
selectedInterval,
|
||||||
|
selectedProduct,
|
||||||
|
setSelectedProduct
|
||||||
|
}}>
|
||||||
|
<section className="gh-portal-products">
|
||||||
|
<div className="gh-portal-products-priceswitch">
|
||||||
|
<span className="gh-portal-priceoption-label">Monthly</span>
|
||||||
|
<Switch id='product-interval' onToggle={(e) => {
|
||||||
|
const interval = selectedInterval === 'month' ? 'year' : 'month';
|
||||||
|
setSelectedInterval(interval);
|
||||||
|
}} checked={checked} />
|
||||||
|
<span className="gh-portal-priceoption-label">Yearly</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="gh-portal-products-priceswitch">
|
<div className="gh-portal-products-grid">
|
||||||
<span className="gh-portal-priceoption-label">Monthly</span>
|
<FreeProductCard />
|
||||||
<Switch onToggle='' checked={false} />
|
<ProductCards products={products} />
|
||||||
<span className="gh-portal-priceoption-label">Yearly</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="gh-portal-products-grid">
|
|
||||||
<div className="gh-portal-product-card">
|
|
||||||
<div className="gh-portal-product-card-header">
|
|
||||||
<Checkbox name='x' id='x' isChecked={false} onPlanSelect='' />
|
|
||||||
<h4 className="gh-portal-product-name">Free</h4>
|
|
||||||
<div className="gh-portal-product-description">Free preview</div>
|
|
||||||
</div>
|
|
||||||
<div className="gh-portal-product-card-footer">
|
|
||||||
<div className="gh-portal-product-price">
|
|
||||||
<span className="currency-sign">$</span>
|
|
||||||
<span className="amount">0</span>
|
|
||||||
</div>
|
|
||||||
<div className="gh-portal-product-alternative-price"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="gh-portal-product-card checked">
|
</section>
|
||||||
<div className="gh-portal-product-card-header">
|
</ProductsContext.Provider>
|
||||||
<Checkbox name='x' id='x' isChecked={true} onPlanSelect='' />
|
|
||||||
<h4 className="gh-portal-product-name">Bronze</h4>
|
|
||||||
<div className="gh-portal-product-description">Access to all members articles</div>
|
|
||||||
</div>
|
|
||||||
<div className="gh-portal-product-card-footer">
|
|
||||||
<div className="gh-portal-product-price">
|
|
||||||
<span className="currency-sign">$</span>
|
|
||||||
<span className="amount">70</span>
|
|
||||||
<span className="billing-period">/year</span>
|
|
||||||
</div>
|
|
||||||
<div className="gh-portal-product-alternative-price">$7/month</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="gh-portal-product-card">
|
|
||||||
<div className="gh-portal-product-card-header">
|
|
||||||
<Checkbox name='x' id='x' isChecked={false} onPlanSelect='' />
|
|
||||||
<h4 className="gh-portal-product-name">Silver</h4>
|
|
||||||
<div className="gh-portal-product-description">Access to all members articles and weekly podcast</div>
|
|
||||||
</div>
|
|
||||||
<div className="gh-portal-product-card-footer">
|
|
||||||
<div className="gh-portal-product-price">
|
|
||||||
<span className="currency-sign">$</span>
|
|
||||||
<span className="amount">120</span>
|
|
||||||
<span className="billing-period">/year</span>
|
|
||||||
</div>
|
|
||||||
<div className="gh-portal-product-alternative-price">$12/month</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="gh-portal-product-card">
|
|
||||||
<div className="gh-portal-product-card-header">
|
|
||||||
<Checkbox name='x' id='x' isChecked={false} onPlanSelect='' />
|
|
||||||
<h4 className="gh-portal-product-name">Gold</h4>
|
|
||||||
<div className="gh-portal-product-description">Access to all members articles, weekly podcast and exclusive interviews</div>
|
|
||||||
</div>
|
|
||||||
<div className="gh-portal-product-card-footer">
|
|
||||||
<div className="gh-portal-product-price">
|
|
||||||
<span className="currency-sign">$</span>
|
|
||||||
<span className="amount">1000</span>
|
|
||||||
<span className="billing-period">/year</span>
|
|
||||||
</div>
|
|
||||||
<div className="gh-portal-product-alternative-price">$12/month</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ export const SwitchStyles = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function Switch({id, label, onToggle, checked = false}) {
|
function Switch({id, label='', onToggle, checked = false}) {
|
||||||
const {action} = useContext(AppContext);
|
const {action} = useContext(AppContext);
|
||||||
const [isChecked, setIsChecked] = useState(checked);
|
const [isChecked, setIsChecked] = useState(checked);
|
||||||
const isActionChanged = ['updateNewsletter:failed', 'updateNewsletter:success'].includes(action);
|
const isActionChanged = ['updateNewsletter:failed', 'updateNewsletter:success'].includes(action);
|
||||||
|
@ -316,6 +316,19 @@ export const getCurrencySymbol = (currency) => {
|
|||||||
return Intl.NumberFormat('en', {currency, style: 'currency'}).format(0).replace(/[\d\s.]/g, '');
|
return Intl.NumberFormat('en', {currency, style: 'currency'}).format(0).replace(/[\d\s.]/g, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getStripeAmount = (amount) => {
|
||||||
|
if (isNaN(amount)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (amount / 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPriceString = (price = {}) => {
|
||||||
|
const symbol = getCurrencySymbol(price.currency);
|
||||||
|
const amount = getStripeAmount(price.amount);
|
||||||
|
return `${symbol}${amount}/${price.interval}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const formatNumber = (amount) => {
|
export const formatNumber = (amount) => {
|
||||||
if (amount === undefined || amount === null) {
|
if (amount === undefined || amount === null) {
|
||||||
return '';
|
return '';
|
||||||
|
Loading…
Reference in New Issue
Block a user