apps/portal/src/utils/helpers.js
import {getDateString} from './date-time';
export function removePortalLinkFromUrl() {
const [path] = window.location.hash.substr(1).split('?');
const linkRegex = /^\/portal\/?(?:\/(\w+(?:\/\w+)*))?\/?$/;
if (path && linkRegex.test(path)) {
window.history.pushState('', document.title, window.location.pathname + window.location.search);
}
}
export function getPortalLinkPath({page}) {
const Links = {
signin: '#/portal/signin',
signup: '#/portal/signup'
};
if (Object.keys(Links).includes(page)) {
return Links[page];
}
return Links.default;
}
export function getPortalLink({page, siteUrl}) {
const url = siteUrl || `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
const portalLinkPath = getPortalLinkPath({page});
return `${url}${portalLinkPath}`;
}
export function isCookiesDisabled() {
return !(navigator && navigator.cookieEnabled);
}
export function isSentryEventAllowed({event: sentryEvent}) {
const frames = sentryEvent?.exception?.values?.[0]?.stacktrace?.frames || [];
const fileNames = frames.map(frame => frame.filename).filter(filename => !!filename);
const lastFileName = fileNames[fileNames.length - 1] || '';
return lastFileName.includes('@tryghost/portal');
}
export function getMemberSubscription({member = {}}) {
if (isPaidMember({member})) {
const subscriptions = member.subscriptions || [];
const activeSubscription = subscriptions.find((sub) => {
return ['active', 'trialing', 'unpaid', 'past_due'].includes(sub.status);
});
if (!activeSubscription?.price && activeSubscription?.plan) {
activeSubscription.price = activeSubscription.plan;
}
return activeSubscription;
}
return null;
}
export function isComplimentaryMember({member = {}}) {
if (!member) {
return false;
}
const subscription = getMemberSubscription({member});
if (subscription) {
const {price} = subscription;
return (price && price.amount === 0);
} else if (!subscription && !!member.paid) {
return true;
}
return false;
}
export function isPaidMember({member = {}}) {
return (member && member.paid);
}
export function getProductCurrency({product}) {
if (!product?.monthlyPrice) {
return null;
}
return product.monthlyPrice.currency;
}
export function getNewsletterFromUuid({site, uuid}) {
if (!uuid) {
return null;
}
const newsletters = getSiteNewsletters({site});
return newsletters?.find((newsletter) => {
return newsletter.uuid = uuid;
});
}
export function allowCompMemberUpgrade({member}) {
return member?.subscriptions?.[0]?.tier?.expiry_at !== undefined;
}
export function getCompExpiry({member}) {
const subscription = getMemberSubscription({member});
if (subscription?.tier?.expiry_at) {
return getDateString(subscription.tier.expiry_at);
}
return '';
}
export function getUpgradeProducts({site, member}) {
const activePrice = getMemberActivePrice({member});
const activePriceCurrency = activePrice?.currency;
const availableProducts = getAvailableProducts({site});
if (!activePrice?.id) {
return availableProducts;
}
return availableProducts.filter((product) => {
return (isSameCurrency(getProductCurrency({product}), activePriceCurrency));
});
}
export function getFilteredPrices({prices, currency}) {
return prices.filter((d) => {
return isSameCurrency((d.currency || ''), (currency || ''));
});
}
export function getPriceFromSubscription({subscription}) {
if (subscription && subscription.price) {
return {
...subscription.price,
stripe_price_id: subscription.price.id,
id: subscription.price.price_id,
price: subscription.price.amount / 100,
name: subscription.price.nickname,
tierId: subscription.tier?.id,
cadence: subscription.price?.interval === 'month' ? 'month' : 'year',
currency: subscription.price.currency.toLowerCase(),
currency_symbol: getCurrencySymbol(subscription.price.currency)
};
}
return null;
}
export function getMemberActivePrice({member}) {
const subscription = getMemberSubscription({member});
return getPriceFromSubscription({subscription});
}
export function getMemberActiveProduct({member, site}) {
const subscription = getMemberSubscription({member});
const price = getPriceFromSubscription({subscription});
const allProducts = getAllProductsForSite({site});
return allProducts.find((product) => {
return product.id === price?.product.product_id;
});
}
export function isMemberActivePrice({priceId, site, member}) {
const activePrice = getMemberActivePrice({member});
const {tierId, cadence} = getProductCadenceFromPrice({site, priceId});
if (activePrice?.tierId === tierId && activePrice?.cadence === cadence) {
return true;
}
return false;
}
export function getSubscriptionFromId({member, subscriptionId}) {
if (isPaidMember({member})) {
const subscriptions = member.subscriptions || [];
return subscriptions.find(d => d.id === subscriptionId);
}
return null;
}
export function getMemberTierName({member}) {
const subscription = getMemberSubscription({member});
return subscription?.tier?.name || '';
}
export function hasOnlyFreePlan({plans, site = {}}) {
plans = plans || getSitePrices({site});
return !plans || plans.length === 0 || (plans.length === 1 && plans[0].type === 'free');
}
export function hasPrice({site = {}, plan}) {
const prices = getSitePrices({site});
if (plan === 'free') {
return !prices || prices.length === 0 || prices.find(p => p.type === 'free');
} else if (plan === 'monthly') {
return prices && prices.length > 0 && prices.find(p => p.name === 'Monthly');
} else if (plan === 'yearly') {
return prices && prices.length > 0 && prices.find(p => p.name === 'Yearly');
} else if (plan) {
return prices && prices.length > 0 && prices.find(p => p.id === plan);
}
return false;
}
export function getCheckoutSessionDataFromPlanAttribute(site, plan) {
const products = getAvailableProducts({site});
const defaultTier = products.find(p => p.type === 'paid');
if (plan === 'monthly') {
return {
cadence: 'month',
tierId: defaultTier.id
};
}
if (plan === 'yearly') {
return {
cadence: 'year',
tierId: defaultTier.id
};
}
return {
priceId: plan
};
}
export function getQueryPrice({site = {}, priceId}) {
const prices = getAvailablePrices({site});
if (priceId === 'free') {
return !prices || prices.length === 0 || prices.find(p => p.type === 'free');
} else if (prices && prices.length > 0 && priceId === 'monthly') {
const monthlyByName = prices.find(p => p.name === 'Monthly');
const monthlyByInterval = prices.find(p => p.interval === 'month');
return monthlyByName || monthlyByInterval;
} else if (prices && prices.length > 0 && priceId === 'yearly') {
const yearlyByName = prices.find(p => p.name === 'Yearly');
const yearlyByInterval = prices.find(p => p.interval === 'year');
return yearlyByName || yearlyByInterval;
} else if (prices && prices.length > 0 && priceId) {
return prices.find(p => p.id === priceId);
}
return null;
}
export function capitalize(str) {
if (typeof str !== 'string' || !str) {
return '';
}
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function isInviteOnlySite({site = {}, pageQuery = ''}) {
const prices = getSitePrices({site, pageQuery});
return prices.length === 0 || (site && site.members_signup_access === 'invite');
}
export function hasRecommendations({site}) {
return site?.recommendations_enabled === true;
}
export function isSigninAllowed({site}) {
return site?.members_signup_access === 'all' || site?.members_signup_access === 'invite';
}
export function isSignupAllowed({site}) {
return site?.members_signup_access === 'all' && (site?.is_stripe_configured || hasOnlyFreePlan({site}));
}
export function hasMultipleProducts({site}) {
const products = getAvailableProducts({site});
if (products?.length > 1) {
return true;
}
return false;
}
export function getRefDomain() {
const referrerSource = window.location.hostname.replace(/^www\./, '');
return referrerSource;
}
export function hasMultipleProductsFeature({site}) {
const {portal_products: portalProducts} = site || {};
return !!portalProducts;
}
export function hasCommentsEnabled({site}) {
return site?.comments_enabled && site?.comments_enabled !== 'off';
}
export function transformApiSiteData({site}) {
try {
if (!site) {
return null;
}
if (site.tiers) {
site.products = site.tiers;
}
site.products = site.products?.map((product) => {
return {
...product,
monthlyPrice: product.monthly_price,
yearlyPrice: product.yearly_price
};
});
site.is_stripe_configured = !!site.paid_members_enabled;
site.members_signup_access = 'all';
if (!site.members_enabled) {
site.members_signup_access = 'none';
}
if (site.members_invite_only) {
site.members_signup_access = 'invite';
}
site.allow_self_signup = false;
if (site.members_signup_access !== 'all') {
site.allow_self_signup = false;
}
// if stripe is not connected then selected plans mean nothing.
// disabling signup would be done by switching to "invite only" mode
if (site.paid_members_enabled) {
site.allow_self_signup = true;
}
// self signup must be available for free plan signup to work
if (site.portal_plans?.includes('free')) {
site.allow_self_signup = true;
}
// Map tier visibility to old settings
if (site.products?.[0]?.visibility) {
// Map paid tier visibility to portal products
site.portal_products = site.products.filter((p) => {
return p.visibility !== 'none' && p.type === 'paid';
}).map(p => p.id);
// Map free tier visibility to portal plans
const freeProduct = site.products.find(p => p.type === 'free');
if (freeProduct) {
site.portal_plans = site.portal_plans?.filter(d => d !== 'free');
if (freeProduct?.visibility === 'public') {
site.portal_plans?.push('free');
}
}
}
return site;
} catch (error) {
/* eslint-disable no-console */
console.warn(`[Portal] Failed to read site data:`, error);
}
}
export function getAvailableProducts({site}) {
const {portal_products: portalProducts, products = [], portal_plans: portalPlans = []} = site || {};
if (!portalPlans.includes('monthly') && !portalPlans.includes('yearly')) {
return [];
}
return products.filter(product => !!product).filter((product) => {
if (site.is_stripe_configured) {
return true;
}
return product.type !== 'paid';
}).filter((product) => {
return !!(product.monthlyPrice && product.yearlyPrice);
}).filter((product) => {
return !!(Object.keys(product.monthlyPrice).length > 0 && Object.keys(product.yearlyPrice).length > 0);
}).filter((product) => {
if (portalProducts && products.length > 1) {
return portalProducts.includes(product.id);
}
return true;
}).sort((productA, productB) => {
return productA?.monthlyPrice?.amount - productB?.monthlyPrice?.amount;
}).map((product) => {
product.monthlyPrice = {
...product.monthlyPrice,
currency_symbol: getCurrencySymbol(product.monthlyPrice.currency)
};
product.yearlyPrice = {
...product.yearlyPrice,
currency_symbol: getCurrencySymbol(product.yearlyPrice.currency)
};
return product;
});
}
export function getFreeProduct({site}) {
const {products = []} = site || {};
return products.find(product => product.type === 'free');
}
export function getAllProductsForSite({site}) {
const {products = [], portal_plans: portalPlans = []} = site || {};
if (!portalPlans.includes('monthly') && !portalPlans.includes('yearly')) {
return [];
}
return products.filter(product => !!product).filter((product) => {
return !!(product.monthlyPrice && product.yearlyPrice);
}).filter((product) => {
return !!(Object.keys(product.monthlyPrice).length > 0 && Object.keys(product.yearlyPrice).length > 0);
}).sort((productA, productB) => {
return productA?.monthlyPrice?.amount - productB?.monthlyPrice?.amount;
}).map((product) => {
product.monthlyPrice = {
...product.monthlyPrice,
currency_symbol: getCurrencySymbol(product.monthlyPrice.currency)
};
product.yearlyPrice = {
...product.yearlyPrice,
currency_symbol: getCurrencySymbol(product.yearlyPrice.currency)
};
return product;
});
}
export function hasBenefits({prices, site}) {
if (!hasMultipleProductsFeature({site})) {
return false;
}
if (!prices?.length) {
return false;
}
return prices.some((price) => {
return price?.benefits?.length;
});
}
export function getSiteProducts({site, pageQuery}) {
const products = getAvailableProducts({site});
const showOnlyFree = pageQuery === 'free';
if (showOnlyFree) {
return [];
}
if (hasFreeProductPrice({site})) {
products.unshift({
id: 'free',
type: 'free'
});
}
return products;
}
export function hasFreeTrialTier({site, pageQuery}) {
const tiers = getSiteProducts({site, pageQuery});
return tiers.some((tier) => {
return !!tier?.trial_days;
});
}
export function getFreeProductBenefits({site}) {
const freeProduct = getFreeProduct({site});
return freeProduct?.benefits || [];
}
export function getFreeTierTitle({site}) {
const freeProduct = getFreeProduct({site});
return freeProduct?.name || 'Free';
}
export function getFreeTierDescription({site}) {
const freeProduct = getFreeProduct({site});
return freeProduct?.description;
}
export function freeHasBenefitsOrDescription({site}) {
const freeProduct = getFreeProduct({site});
if (freeProduct?.description || freeProduct?.benefits?.length) {
return true;
}
return false;
}
export function getProductBenefits({product}) {
if (product?.monthlyPrice && product?.yearlyPrice) {
const productBenefits = product?.benefits || [];
const monthlyBenefits = productBenefits;
const yearlyBenefits = productBenefits;
return {
monthly: monthlyBenefits,
yearly: yearlyBenefits
};
}
}
export function getProductFromId({site, productId}) {
const availableProducts = getAllProductsForSite({site});
return availableProducts.find(product => product.id === productId);
}
export function getPricesFromProducts({site = null, products = null}) {
if (!site && !products) {
return [];
}
const availableProducts = products || getAvailableProducts({site});
const prices = availableProducts.reduce((accumPrices, product) => {
if (product.monthlyPrice && product.yearlyPrice) {
accumPrices.push(product.monthlyPrice);
accumPrices.push(product.yearlyPrice);
}
return accumPrices;
}, []);
return prices;
}
export function hasFreeProductPrice({site}) {
const {
allow_self_signup: allowSelfSignup,
portal_plans: portalPlans
} = site || {};
return allowSelfSignup && portalPlans.includes('free');
}
export function getSiteNewsletters({site}) {
const {
newsletters = []
} = site || {};
newsletters?.sort((a, b) => {
return a.sort_order - b.sort_order;
});
return newsletters;
}
export function hasMultipleNewsletters({site}) {
const {
newsletters
} = site || {};
return newsletters?.length > 1;
}
export function isEmailSuppressed({member}) {
return member?.email_suppression?.suppressed;
}
export function hasOnlyFreeProduct({site}) {
const products = getSiteProducts({site});
return (products.length === 1 && hasFreeProductPrice({site}));
}
export function getSubFreeTrialDaysLeft({sub} = {}) {
if (!subscriptionHasFreeTrial({sub})) {
return 0;
}
const today = (new Date()).setHours(0, 0, 0, 0);
const freeTrialEnd = (new Date(sub.trial_end_at)).setHours(0, 0, 0, 0);
const ONE_DAY = 1000 * 60 * 60 * 24;
return Math.ceil(((freeTrialEnd - today) / ONE_DAY));
}
export function subscriptionHasFreeTrial({sub} = {}) {
if (sub?.trial_end_at && !isInThePast(new Date(sub?.trial_end_at))) {
return true;
}
return false;
}
export function isInThePast(date) {
return date < new Date();
}
export function getProductFromPrice({site, priceId}) {
if (priceId === 'free') {
return getFreeProduct({site});
}
const products = getAllProductsForSite({site});
return products.find((product) => {
return (product?.monthlyPrice?.id === priceId) || (product?.yearlyPrice?.id === priceId);
});
}
export function getProductCadenceFromPrice({site, priceId}) {
if (priceId === 'free') {
return getFreeProduct({site});
}
const products = getAllProductsForSite({site});
const tier = products.find((product) => {
return (product?.monthlyPrice?.id === priceId) || (product?.yearlyPrice?.id === priceId);
});
let cadence = 'month';
if (tier?.yearlyPrice?.id === priceId) {
cadence = 'year';
}
return {
tierId: tier?.id,
cadence
};
}
export function getAvailablePrices({site, products = null}) {
const {
portal_plans: portalPlans = [],
is_stripe_configured: isStripeConfigured
} = site || {};
if (!isStripeConfigured) {
return [];
}
const productPrices = getPricesFromProducts({site, products});
return productPrices.filter((d) => {
return !!(d && d.id);
}).map((d) => {
return {
...d,
price_id: d.id,
price: d.amount / 100,
name: d.nickname,
currency_symbol: getCurrencySymbol(d.currency)
};
}).filter((price) => {
return price.amount !== 0 && price.type === 'recurring';
}).filter((price) => {
if (price.interval === 'month') {
return portalPlans.includes('monthly');
}
if (price.interval === 'year') {
return portalPlans.includes('yearly');
}
return false;
}).sort((a, b) => {
return a.amount - b.amount;
}).sort((a, b) => {
if (!a.currency || !b.currency) {
return 0;
}
return a.currency.localeCompare(b.currency, undefined, {ignorePunctuation: true});
});
}
export function getFreePriceCurrency({site}) {
const stripePrices = getAvailablePrices({site});
let freePriceCurrencyDetail = {
currency: 'usd',
currency_symbol: '$'
};
if (stripePrices?.length > 0) {
freePriceCurrencyDetail.currency = stripePrices[0].currency;
freePriceCurrencyDetail.currency_symbol = stripePrices[0].currency_symbol;
}
return freePriceCurrencyDetail;
}
export function getSitePrices({site = {}, pageQuery = ''} = {}) {
const {
allow_self_signup: allowSelfSignup,
portal_plans: portalPlans
} = site || {};
const plansData = [];
if (allowSelfSignup && portalPlans.includes('free')) {
const freePriceCurrencyDetail = getFreePriceCurrency({site});
plansData.push({
id: 'free',
type: 'free',
price: 0,
amount: 0,
name: getFreeTierTitle({site}),
...freePriceCurrencyDetail
});
}
const showOnlyFree = pageQuery === 'free' && hasFreeProductPrice({site});
if (!showOnlyFree) {
const stripePrices = getAvailablePrices({site});
stripePrices.forEach((price) => {
plansData.push(price);
});
}
return plansData;
}
export const getMemberEmail = ({member}) => {
if (!member) {
return '';
}
return member.email;
};
export const hasMemberGotEmailSuppression = ({member}) => {
if (!member) {
return '';
}
return member.email_suppression;
};
export const getFirstpromoterId = ({site}) => {
return (site && site.firstpromoter_account);
};
export const getMemberName = ({member}) => {
if (!member) {
return '';
}
return member.name;
};
export const getSupportAddress = ({site}) => {
const {members_support_address: oldSupportAddress, support_email_address: supportAddress} = site || {};
// If available, use the calculated setting support_email_address
if (supportAddress) {
return supportAddress;
}
// Deprecated: use the saved setting members_support_address
if (oldSupportAddress?.indexOf('@') < 0) {
const siteDomain = getSiteDomain({site});
const updatedDomain = siteDomain?.replace(/^(www)\.(?=[^/]*\..{2,5})/, '') || '';
return `${oldSupportAddress}@${updatedDomain}`;
}
if (oldSupportAddress?.split('@')?.length > 1) {
const [recipient, domain] = oldSupportAddress.split('@');
const updatedDomain = domain?.replace(/^(www)\.(?=[^/]*\..{2,5})/, '') || '';
return `${recipient}@${updatedDomain}`;
}
return oldSupportAddress || '';
};
export const getDefaultNewsletterSender = ({site}) => {
const {default_email_address: defaultEmailAddress} = site || {};
// If available, use the calculated setting default_email_address as default
const defaultAddress = defaultEmailAddress || `noreply@${getSiteDomain({site})}`;
const newsletters = getSiteNewsletters({site});
const defaultNewsletter = newsletters?.[0];
if (defaultNewsletter && defaultNewsletter.sender_email) {
return defaultNewsletter.sender_email;
} else {
return defaultAddress;
}
};
export const getSiteDomain = ({site}) => {
try {
return ((new URL(site.url)).origin).replace(/^http(s?):\/\//, '').replace(/\/$/, '');
} catch (e) {
return site.url.replace(/^http(s?):\/\//, '').replace(/\/$/, '');
}
};
export const getCurrencySymbol = (currency) => {
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) => {
if (amount === undefined || amount === null) {
return '';
}
return amount.toLocaleString();
};
export const createPopupNotification = ({type, status, autoHide, duration = 2600, closeable, state, message, meta = {}}) => {
let count = 0;
if (state && state.popupNotification) {
count = (state.popupNotification.count || 0) + 1;
}
return {
type,
status,
autoHide,
closeable,
duration,
meta,
message,
count
};
};
export function isSameCurrency(currency1, currency2) {
return currency1?.toLowerCase() === currency2?.toLowerCase();
}
export function getPriceIdFromPageQuery({site, pageQuery}) {
const productMonthlyPriceQueryRegex = /^(?:(\S+?))?\/monthly$/;
const productYearlyPriceQueryRegex = /^(?:(\S+?))?\/yearly$/;
if (productMonthlyPriceQueryRegex.test(pageQuery || '')) {
const [, productId] = pageQuery.match(productMonthlyPriceQueryRegex);
const product = getProductFromId({site, productId});
return product?.monthlyPrice?.id;
} else if (productYearlyPriceQueryRegex.test(pageQuery || '')) {
const [, productId] = pageQuery.match(productYearlyPriceQueryRegex);
const product = getProductFromId({site, productId});
return product?.yearlyPrice?.id;
}
return null;
}
export const getOfferOffAmount = ({offer}) => {
if (offer.type === 'fixed') {
return `${getCurrencySymbol(offer.currency)}${offer.amount / 100}`;
} else if (offer.type === 'percent') {
return `${offer.amount}%`;
}
return '';
};
export const getUpdatedOfferPrice = ({offer, price, useFormatted = false}) => {
const originalAmount = price.amount;
let updatedAmount;
if (offer.type === 'fixed' && isSameCurrency(offer.currency, price.currency)) {
updatedAmount = ((originalAmount - offer.amount)) / 100;
updatedAmount = updatedAmount > 0 ? updatedAmount : 0;
} else if (offer.type === 'percent') {
updatedAmount = (originalAmount - ((originalAmount * offer.amount) / 100)) / 100;
} else {
updatedAmount = originalAmount / 100;
}
if (useFormatted) {
return Intl.NumberFormat('en', {currency: price?.currency, style: 'currency'}).format(updatedAmount);
}
return updatedAmount;
};
export const isActiveOffer = ({site, offer}) => {
if (offer?.status !== 'active') {
return false;
}
// Check if the corresponding tier has been archived
const product = getProductFromId({site, productId: offer.tier.id});
return !!product;
};
function createMonthlyPrice({tier, priceId}) {
if (tier?.monthly_price) {
return {
id: `price-${priceId}`,
active: true,
type: 'recurring',
nickname: 'Monthly',
currency: tier.currency,
amount: tier.monthly_price,
interval: 'month'
};
}
return null;
}
function createYearlyPrice({tier, priceId}) {
if (tier?.yearly_price) {
return {
id: `price-${priceId}`,
active: true,
type: 'recurring',
nickname: 'Yearly',
currency: tier.currency,
amount: tier.yearly_price,
interval: 'year'
};
}
return null;
}
function createBenefits({tier}) {
return tier?.benefits?.map((benefit) => {
return {
name: benefit
};
});
}
export const transformApiTiersData = ({tiers}) => {
let priceId = 0;
return tiers.map((tier) => {
let monthlyPrice = createMonthlyPrice({tier, priceId});
priceId += 1;
let yearlyPrice = createYearlyPrice({tier, priceId});
priceId += 1;
let benefits = createBenefits({tier});
return {
...tier,
benefits: benefits,
monthly_price: monthlyPrice,
yearly_price: yearlyPrice
};
});
};
/**
* Returns the member attribution URL history, which is stored in localStorage, if there is any.
* @warning If you make changes here, please also update the one in signup-form!
* @returns {Object[]|undefined}
*/
export function getUrlHistory() {
const STORAGE_KEY = 'ghost-history';
try {
const historyString = localStorage.getItem(STORAGE_KEY);
if (historyString) {
const parsed = JSON.parse(historyString);
if (Array.isArray(parsed)) {
return parsed;
}
}
} catch (error) {
// Failed to access localStorage or something related to that.
// Log a warning, as this shouldn't happen on a modern browser.
/* eslint-disable no-console */
console.warn(`[Portal] Failed to load member URL history:`, error);
}
}
// Check if member is a recent member, i.e. created in last 24 hours
export function isRecentMember({member}) {
if (!member?.created_at) {
return false;
}
const now = new Date();
const created = new Date(member.created_at);
const diff = now.getTime() - created.getTime();
const diffHours = Math.round(diff / (1000 * 60 * 60));
return diffHours < 24;
}