feat(core): pricing plans actions (#4724)

This commit is contained in:
Cats Juice
2023-10-26 22:00:14 +08:00
committed by GitHub
parent 9334a363c7
commit edb6e0fd69
8 changed files with 607 additions and 178 deletions
@@ -1,10 +1,14 @@
import { RadioButton, RadioButtonGroup } from '@affine/component';
import { pushNotificationAtom } from '@affine/component/notification-center';
import {
pricesQuery,
SubscriptionPlan,
SubscriptionRecurring,
} from '@affine/graphql';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useQuery } from '@affine/workspace/affine/gql';
import { useSetAtom } from 'jotai';
import { Suspense, useEffect, useRef, useState } from 'react';
import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status';
@@ -14,10 +18,25 @@ import { type FixedPrice, getPlanDetail, PlanCard } from './plan-card';
import { PlansSkeleton } from './skeleton';
import * as styles from './style.css';
const getRecurringLabel = ({
recurring,
t,
}: {
recurring: SubscriptionRecurring;
t: ReturnType<typeof useAFFiNEI18N>;
}) => {
return recurring === SubscriptionRecurring.Monthly
? t['com.affine.payment.recurring-monthly']()
: t['com.affine.payment.recurring-yearly']();
};
const Settings = () => {
const t = useAFFiNEI18N();
const [subscription, mutateSubscription] = useUserSubscription();
const pushNotification = useSetAtom(pushNotificationAtom);
const loggedIn = useCurrentLoginStatus() === 'authenticated';
const planDetail = getPlanDetail();
const planDetail = getPlanDetail(t);
const scrollWrapper = useRef<HTMLDivElement>(null);
const {
@@ -44,6 +63,9 @@ const Settings = () => {
);
const currentPlan = subscription?.plan ?? SubscriptionPlan.Free;
const isCanceled = !!subscription?.canceledAt;
const currentRecurring =
subscription?.recurring ?? SubscriptionRecurring.Monthly;
const yearlyDiscount = (
planDetail.get(SubscriptionPlan.Pro) as FixedPrice | undefined
@@ -76,23 +98,39 @@ const Settings = () => {
}, [recurring]);
const subtitle = loggedIn ? (
<p>
You are current on the {currentPlan} plan. If you have any questions,
please contact our <span>{/*TODO: add action*/}customer support</span>.
</p>
isCanceled ? (
<p>
{t['com.affine.payment.subtitle-canceled']({
plan: `${getRecurringLabel({
recurring: currentRecurring,
t,
})} ${currentPlan}`,
})}
</p>
) : (
<p>
<Trans
plan={currentPlan}
i18nKey="com.affine.payment.subtitle-active"
values={{ currentPlan }}
>
You are current on the {{ currentPlan }} plan. If you have any
questions, please contact our&nbsp;
<a
href="#"
target="_blank"
style={{ color: 'var(--affine-link-color)' }}
>
customer support
</a>
.
</Trans>
</p>
)
) : (
<p>
This is the Pricing plans of AFFiNE Cloud. You can sign up or sign in to
your account first.
</p>
<p>{t['com.affine.payment.subtitle-not-signed-in']()}</p>
);
const getRecurringLabel = (recurring: SubscriptionRecurring) =>
({
[SubscriptionRecurring.Monthly]: 'Monthly',
[SubscriptionRecurring.Yearly]: 'Annually',
})[recurring];
const tabs = (
<RadioButtonGroup
className={styles.recurringRadioGroup}
@@ -101,10 +139,12 @@ const Settings = () => {
>
{Object.values(SubscriptionRecurring).map(recurring => (
<RadioButton key={recurring} value={recurring}>
{getRecurringLabel(recurring)}
{getRecurringLabel({ recurring, t })}
{recurring === SubscriptionRecurring.Yearly && yearlyDiscount && (
<span className={styles.radioButtonDiscount}>
{yearlyDiscount}% off
{t['com.affine.payment.discount-amount']({
amount: yearlyDiscount,
})}
</span>
)}
</RadioButton>
@@ -119,6 +159,21 @@ const Settings = () => {
<PlanCard
key={detail.plan}
onSubscriptionUpdate={mutateSubscription}
onNotify={({ detail, recurring }) => {
pushNotification({
type: 'success',
title: t['com.affine.payment.updated-notify-title'](),
message: t['com.affine.payment.updated-notify-msg']({
plan:
detail.plan === SubscriptionPlan.Free
? SubscriptionPlan.Free
: getRecurringLabel({
recurring: recurring as SubscriptionRecurring,
t,
}),
}),
});
}}
{...{ detail, subscription, recurring }}
/>
);
@@ -1,4 +1,5 @@
import { SettingHeader } from '@affine/component/setting-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightBigIcon } from '@blocksuite/icons';
import type { HtmlHTMLAttributes, ReactNode } from 'react';
@@ -14,32 +15,37 @@ export interface PlanLayoutProps
scrollRef?: React.RefObject<HTMLDivElement>;
}
const SeeAllLink = () => (
<a
className={styles.allPlansLink}
href="https://affine.pro/pricing"
target="_blank"
rel="noopener noreferrer"
>
See all plans
{<ArrowRightBigIcon width="16" height="16" />}
</a>
);
const SeeAllLink = () => {
const t = useAFFiNEI18N();
return (
<a
className={styles.allPlansLink}
href="https://affine.pro/pricing"
target="_blank"
rel="noopener noreferrer"
>
{t['com.affine.payment.see-all-plans']()}
{<ArrowRightBigIcon width="16" height="16" />}
</a>
);
};
export const PlanLayout = ({
subtitle,
tabs,
scroll,
title = 'Pricing Plans',
title,
footer = <SeeAllLink />,
scrollRef,
}: PlanLayoutProps) => {
const t = useAFFiNEI18N();
return (
<div className={styles.plansLayoutRoot}>
{/* TODO: SettingHeader component shouldn't have margin itself */}
<SettingHeader
style={{ marginBottom: '0px' }}
title={title}
title={title ?? t['com.affine.payment.title']()}
subtitle={subtitle}
/>
{tabs}
@@ -0,0 +1,119 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { DialogTrigger } from '@radix-ui/react-dialog';
import { Button } from '@toeverything/components/button';
import {
ConfirmModal,
type ConfirmModalProps,
Modal,
} from '@toeverything/components/modal';
import { type ReactNode, useEffect, useRef } from 'react';
import * as styles from './style.css';
/**
*
* @param param0
* @returns
*/
export const ConfirmLoadingModal = ({
type,
loading,
open,
content,
onOpenChange,
onConfirm,
...props
}: {
type: 'resume' | 'change';
loading?: boolean;
content?: ReactNode;
} & ConfirmModalProps) => {
const t = useAFFiNEI18N();
const confirmed = useRef(false);
const title = t[`com.affine.payment.modal.${type}.title`]();
const confirmText = t[`com.affine.payment.modal.${type}.confirm`]();
const cancelText = t[`com.affine.payment.modal.${type}.cancel`]();
const contentText =
type !== 'change' ? t[`com.affine.payment.modal.${type}.content`]() : '';
useEffect(() => {
if (!loading && open && confirmed.current) {
onOpenChange?.(false);
confirmed.current = false;
}
}, [loading, open, onOpenChange]);
return (
<ConfirmModal
title={title}
cancelText={cancelText}
confirmButtonOptions={{
type: 'primary',
children: confirmText,
loading,
}}
open={open}
onOpenChange={onOpenChange}
onConfirm={() => {
confirmed.current = true;
onConfirm?.();
}}
{...props}
>
{content ?? contentText}
</ConfirmModal>
);
};
/**
* Downgrade modal, confirm & cancel button are reversed
* @param param0
*/
export const DowngradeModal = ({
open,
onOpenChange,
onCancel,
}: {
loading?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
onCancel?: () => void;
}) => {
const t = useAFFiNEI18N();
return (
<Modal
title={t['com.affine.payment.modal.downgrade.title']()}
open={open}
contentOptions={{}}
width={480}
onOpenChange={onOpenChange}
>
<div className={styles.downgradeContentWrapper}>
<p className={styles.downgradeContent}>
{t['com.affine.payment.modal.downgrade.content']()}
</p>
<p className={styles.downgradeCaption}>
{t['com.affine.payment.modal.downgrade.caption']()}
</p>
</div>
<footer className={styles.downgradeFooter}>
<Button
onClick={() => {
onOpenChange?.(false);
onCancel?.();
}}
>
{t['com.affine.payment.modal.downgrade.cancel']()}
</Button>
<DialogTrigger asChild>
<Button onClick={() => onOpenChange?.(false)} type="primary">
{t['com.affine.payment.modal.downgrade.confirm']()}
</Button>
</DialogTrigger>
</footer>
</Modal>
);
};
@@ -5,19 +5,32 @@ import type {
import {
cancelSubscriptionMutation,
checkoutMutation,
resumeSubscriptionMutation,
SubscriptionPlan,
SubscriptionRecurring,
updateSubscriptionMutation,
} from '@affine/graphql';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMutation } from '@affine/workspace/affine/gql';
import { DoneIcon } from '@blocksuite/icons';
import { Button } from '@toeverything/components/button';
import { Tooltip } from '@toeverything/components/tooltip';
import { useSetAtom } from 'jotai';
import { useAtom } from 'jotai';
import { type PropsWithChildren, useCallback, useEffect, useRef } from 'react';
import {
type PropsWithChildren,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { openPaymentDisableAtom } from '../../../../../atoms';
import { authAtom } from '../../../../../atoms/index';
import { useCurrentLoginStatus } from '../../../../../hooks/affine/use-current-login-status';
import { BulledListIcon } from './icons/bulled-list';
import { ConfirmLoadingModal, DowngradeModal } from './modals';
import * as styles from './style.css';
export interface FixedPrice {
@@ -41,12 +54,13 @@ interface PlanCardProps {
subscription?: Subscription | null;
recurring: string;
onSubscriptionUpdate: SubscriptionMutator;
onNotify: (info: {
detail: FixedPrice | DynamicPrice;
recurring: string;
}) => void;
}
export function getPlanDetail() {
// const t = useAFFiNEI18N();
// TODO: i18n all things
export function getPlanDetail(t: ReturnType<typeof useAFFiNEI18N>) {
return new Map<SubscriptionPlan, FixedPrice | DynamicPrice>([
[
SubscriptionPlan.Free,
@@ -56,12 +70,12 @@ export function getPlanDetail() {
price: '0',
yearlyPrice: '0',
benefits: [
'Unlimited local workspace',
'Unlimited login devices',
'Unlimited blocks',
'AFFiNE Cloud Storage 10GB',
'The maximum file size is 10M',
'Number of members per Workspace ≤ 3',
t['com.affine.payment.benefit-1'](),
t['com.affine.payment.benefit-2'](),
t['com.affine.payment.benefit-3'](),
t['com.affine.payment.benefit-4']({ capacity: '10GB' }),
t['com.affine.payment.benefit-5']({ capacity: '10M' }),
t['com.affine.payment.benefit-6']({ capacity: '3' }),
],
},
],
@@ -73,12 +87,12 @@ export function getPlanDetail() {
price: '1',
yearlyPrice: '1',
benefits: [
'Unlimited local workspace',
'Unlimited login devices',
'Unlimited blocks',
'AFFiNE Cloud Storage 100GB',
'The maximum file size is 500M',
'Number of members per Workspace ≤ 10',
t['com.affine.payment.benefit-1'](),
t['com.affine.payment.benefit-2'](),
t['com.affine.payment.benefit-3'](),
t['com.affine.payment.benefit-4']({ capacity: '100GB' }),
t['com.affine.payment.benefit-5']({ capacity: '500M' }),
t['com.affine.payment.benefit-6']({ capacity: '10' }),
],
},
],
@@ -89,9 +103,9 @@ export function getPlanDetail() {
plan: SubscriptionPlan.Team,
contact: true,
benefits: [
'Best team workspace for collaboration and knowledge distilling.',
'Focusing on what really matters with team project management and automation.',
'Pay for seats, fits all team size.',
t['com.affine.payment.dynamic-benefit-1'](),
t['com.affine.payment.dynamic-benefit-2'](),
t['com.affine.payment.dynamic-benefit-3'](),
],
},
],
@@ -102,20 +116,16 @@ export function getPlanDetail() {
plan: SubscriptionPlan.Enterprise,
contact: true,
benefits: [
'Solutions & best practices for dedicated needs.',
'Embedable & interrogations with IT support.',
t['com.affine.payment.dynamic-benefit-4'](),
t['com.affine.payment.dynamic-benefit-5'](),
],
},
],
]);
}
export const PlanCard = ({
detail,
subscription,
recurring,
onSubscriptionUpdate,
}: PlanCardProps) => {
export const PlanCard = (props: PlanCardProps) => {
const { detail, subscription, recurring } = props;
const loggedIn = useCurrentLoginStatus() === 'authenticated';
const currentPlan = subscription?.plan ?? SubscriptionPlan.Free;
const currentRecurring = subscription?.recurring;
@@ -160,49 +170,7 @@ export const PlanCard = ({
)}
</p>
</div>
{
// branches:
// if contact => 'Contact Sales'
// if not signed in:
// if free => 'Sign up free'
// else => 'Buy Pro'
// else
// if isCurrent => 'Current Plan'
// else if free => 'Downgrade'
// else if currentRecurring !== recurring => 'Change to {recurring} Billing'
// else => 'Upgrade'
// TODO: should replace with components with proper actions
detail.type === 'dynamic' ? (
<ContactSales />
) : loggedIn ? (
detail.plan === currentPlan &&
(currentRecurring === recurring ||
(!currentRecurring && detail.plan === SubscriptionPlan.Free)) ? (
<CurrentPlan />
) : detail.plan === SubscriptionPlan.Free ? (
<Downgrade onSubscriptionUpdate={onSubscriptionUpdate} />
) : currentRecurring !== recurring &&
currentPlan === detail.plan ? (
<ChangeRecurring
// @ts-expect-error must exist
from={currentRecurring}
to={recurring as SubscriptionRecurring}
onSubscriptionUpdate={onSubscriptionUpdate}
/>
) : (
<Upgrade
recurring={recurring as SubscriptionRecurring}
onSubscriptionUpdate={onSubscriptionUpdate}
/>
)
) : (
<SignupAction>
{detail.plan === SubscriptionPlan.Free
? 'Sign up free'
: 'Buy Pro'}
</SignupAction>
)
}
<ActionButton {...props} />
</div>
<div className={styles.planBenefits}>
{detail.benefits.map((content, i) => (
@@ -226,15 +194,111 @@ export const PlanCard = ({
);
};
const ActionButton = ({
detail,
subscription,
recurring,
onSubscriptionUpdate,
onNotify,
}: PlanCardProps) => {
const t = useAFFiNEI18N();
const loggedIn = useCurrentLoginStatus() === 'authenticated';
const currentPlan = subscription?.plan ?? SubscriptionPlan.Free;
const currentRecurring = subscription?.recurring;
const mutateAndNotify = useCallback(
(sub: Parameters<SubscriptionMutator>[0]) => {
onSubscriptionUpdate?.(sub);
onNotify?.({ detail, recurring });
},
[onSubscriptionUpdate, onNotify, detail, recurring]
);
// branches:
// if contact => 'Contact Sales'
// if not signed in:
// if free => 'Sign up free'
// else => 'Buy Pro'
// else
// if isCurrent
// if canceled => 'Resume'
// else => 'Current Plan'
// if isCurrent => 'Current Plan'
// else if free => 'Downgrade'
// else if currentRecurring !== recurring => 'Change to {recurring} Billing'
// else => 'Upgrade'
// contact
if (detail.type === 'dynamic') {
return <ContactSales />;
}
// not signed in
if (!loggedIn) {
return (
<SignUpAction>
{detail.plan === SubscriptionPlan.Free
? t['com.affine.payment.sign-up-free']()
: t['com.affine.payment.buy-pro']()}
</SignUpAction>
);
}
const isCanceled = !!subscription?.canceledAt;
const isFree = detail.plan === SubscriptionPlan.Free;
const isCurrent =
detail.plan === currentPlan &&
(isFree ? true : currentRecurring === recurring);
// is current
if (isCurrent) {
return isCanceled ? (
<ResumeAction onSubscriptionUpdate={mutateAndNotify} />
) : (
<CurrentPlan />
);
}
if (isFree) {
return (
<Downgrade disabled={isCanceled} onSubscriptionUpdate={mutateAndNotify} />
);
}
return currentPlan === detail.plan ? (
<ChangeRecurring
from={currentRecurring as SubscriptionRecurring}
to={recurring as SubscriptionRecurring}
due={subscription?.nextBillAt || ''}
onSubscriptionUpdate={mutateAndNotify}
disabled={isCanceled}
/>
) : (
<Upgrade
recurring={recurring as SubscriptionRecurring}
onSubscriptionUpdate={mutateAndNotify}
/>
);
};
const CurrentPlan = () => {
return <Button className={styles.planAction}>Current Plan</Button>;
const t = useAFFiNEI18N();
return (
<Button className={styles.planAction}>
{t['com.affine.payment.current-plan']()}
</Button>
);
};
const Downgrade = ({
disabled,
onSubscriptionUpdate,
}: {
disabled?: boolean;
onSubscriptionUpdate: SubscriptionMutator;
}) => {
const t = useAFFiNEI18N();
const [open, setOpen] = useState(false);
const { isMutating, trigger } = useMutation({
mutation: cancelSubscriptionMutation,
});
@@ -247,25 +311,43 @@ const Downgrade = ({
});
}, [trigger, onSubscriptionUpdate]);
const tooltipContent = disabled
? t['com.affine.payment.downgraded-tooltip']()
: null;
return (
<Button
className={styles.planAction}
type="primary"
onClick={downgrade /* TODO: poppup confirmation modal instead */}
disabled={isMutating}
loading={isMutating}
>
Downgrade
</Button>
<>
<Tooltip content={tooltipContent} rootOptions={{ delayDuration: 0 }}>
<div className={styles.planAction}>
<Button
className={styles.planAction}
type="primary"
onClick={() => setOpen(true)}
disabled={disabled || isMutating}
loading={isMutating}
>
{t['com.affine.payment.downgrade']()}
</Button>
</div>
</Tooltip>
<DowngradeModal open={open} onCancel={downgrade} onOpenChange={setOpen} />
</>
);
};
const ContactSales = () => {
const t = useAFFiNEI18N();
return (
// TODO: add action
<Button className={styles.planAction} type="primary">
Contact Sales
</Button>
<a
className={styles.planAction}
href="https://6dxre9ihosp.typeform.com/to/uZeBtpPm"
target="_blank"
rel="noreferrer"
>
<Button className={styles.planAction} type="primary">
{t['com.affine.payment.contact-sales']()}
</Button>
</a>
);
};
@@ -276,6 +358,7 @@ const Upgrade = ({
recurring: SubscriptionRecurring;
onSubscriptionUpdate: SubscriptionMutator;
}) => {
const t = useAFFiNEI18N();
const { isMutating, trigger } = useMutation({
mutation: checkoutMutation,
});
@@ -330,27 +413,35 @@ const Upgrade = ({
}, [onClose]);
return (
<Button
className={styles.planAction}
type="primary"
onClick={upgrade}
disabled={isMutating}
loading={isMutating}
>
Upgrade
</Button>
<>
<Button
className={styles.planAction}
type="primary"
onClick={upgrade}
disabled={isMutating}
loading={isMutating}
>
{t['com.affine.payment.upgrade']()}
</Button>
</>
);
};
const ChangeRecurring = ({
from: _from /* TODO: from can be useful when showing confirmation modal */,
from,
to,
disabled,
due,
onSubscriptionUpdate,
}: {
from: SubscriptionRecurring;
to: SubscriptionRecurring;
disabled?: boolean;
due: string;
onSubscriptionUpdate: SubscriptionMutator;
}) => {
const t = useAFFiNEI18N();
const [open, setOpen] = useState(false);
const { isMutating, trigger } = useMutation({
mutation: updateSubscriptionMutation,
});
@@ -366,24 +457,102 @@ const ChangeRecurring = ({
);
}, [trigger, onSubscriptionUpdate, to]);
const changeCurringContent = (
<Trans values={{ from, to, due }} className={styles.downgradeContent}>
You are changing your <span className={styles.textEmphasis}>{from}</span>{' '}
subscription to <span className={styles.textEmphasis}>{to}</span>{' '}
subscription. This change will take effect in the next billing cycle, with
an effective date of <span className={styles.textEmphasis}>{due}</span>.
</Trans>
);
return (
<Button
className={styles.planAction}
type="primary"
onClick={change /* TODO: popup confirmation modal instead */}
disabled={isMutating}
loading={isMutating}
>
Change to {to} Billing
</Button>
<>
<Button
className={styles.planAction}
type="primary"
onClick={() => setOpen(true)}
disabled={disabled || isMutating}
loading={isMutating}
>
{t['com.affine.payment.change-to']({ to })}
</Button>
<ConfirmLoadingModal
type={'change'}
loading={isMutating}
open={open}
onConfirm={change}
onOpenChange={setOpen}
content={changeCurringContent}
/>
</>
);
};
const SignupAction = ({ children }: PropsWithChildren) => {
// TODO: add login action
const SignUpAction = ({ children }: PropsWithChildren) => {
const setOpen = useSetAtom(authAtom);
const onClickSignIn = useCallback(async () => {
setOpen(state => ({
...state,
openModal: true,
}));
}, [setOpen]);
return (
<Button className={styles.planAction} type="primary">
<Button
onClick={onClickSignIn}
className={styles.planAction}
type="primary"
>
{children}
</Button>
);
};
const ResumeAction = ({
onSubscriptionUpdate,
}: {
onSubscriptionUpdate: SubscriptionMutator;
}) => {
const t = useAFFiNEI18N();
const [open, setOpen] = useState(false);
const [hovered, setHovered] = useState(false);
const { isMutating, trigger } = useMutation({
mutation: resumeSubscriptionMutation,
});
const resume = useCallback(() => {
trigger(null, {
onSuccess: data => {
onSubscriptionUpdate(data.resumeSubscription);
},
});
}, [trigger, onSubscriptionUpdate]);
return (
<>
<Button
className={styles.planAction}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onClick={() => setOpen(true)}
loading={isMutating}
disabled={isMutating}
>
{hovered
? t['com.affine.payment.resume-renewal']()
: t['com.affine.payment.current-plan']()}
</Button>
<ConfirmLoadingModal
type={'resume'}
open={open}
onConfirm={resume}
onOpenChange={setOpen}
loading={isMutating}
/>
</>
);
};
@@ -14,7 +14,7 @@ export const radioButtonDiscount = style({
});
export const planCardsWrapper = style({
paddingRight: 'calc(var(--setting-modal-gap-x))',
paddingRight: 'calc(var(--setting-modal-gap-x) + 30px)',
display: 'flex',
gap: '16px',
width: 'fit-content',
@@ -116,3 +116,34 @@ export const planBenefitText = style({
flexDirection: 'column',
alignItems: 'center',
});
export const downgradeContentWrapper = style({
padding: '12px 0 20px 0px',
display: 'flex',
flexDirection: 'column',
gap: '12px',
});
export const downgradeContent = style({
fontSize: '15px',
lineHeight: '24px',
fontWeight: 400,
color: 'var(--affine-text-primary-color)',
});
export const downgradeCaption = style({
fontSize: '14px',
lineHeight: '22px',
color: 'var(--affine-text-secondary-color)',
});
export const downgradeFooter = style({
display: 'flex',
justifyContent: 'flex-end',
gap: '20px',
paddingTop: '20px',
});
export const textEmphasis = style({
color: 'var(--affine-text-emphasis-color)',
});
@@ -39,6 +39,7 @@ export const SettingModal = ({
const generalSettingList = useGeneralSettingList();
const modalContentRef = useRef<HTMLDivElement>(null);
const modalContentWrapperRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!modalProps.open) return;
@@ -46,19 +47,18 @@ export const SettingModal = ({
const onResize = debounce(() => {
cancelAnimationFrame(animationFrameId);
animationFrameId = requestAnimationFrame(() => {
if (!modalContentRef.current) return;
if (!modalContentRef.current || !modalContentWrapperRef.current) return;
const wrapperWidth = modalContentWrapperRef.current.offsetWidth;
const contentWidth = modalContentRef.current.offsetWidth;
const computedStyle = window.getComputedStyle(modalContentRef.current);
const marginX = parseInt(computedStyle.marginLeft, 10);
const paddingX = parseInt(computedStyle.paddingLeft, 10);
modalContentRef.current?.style.setProperty(
'--setting-modal-width',
`${contentWidth + marginX * 2}px`
`${wrapperWidth}px`
);
modalContentRef.current?.style.setProperty(
'--setting-modal-gap-x',
`${marginX + paddingX}px`
`${(wrapperWidth - contentWidth) / 2}px`
);
});
}, 200);
@@ -121,33 +121,35 @@ export const SettingModal = ({
<div
data-testid="setting-modal-content"
className={style.wrapper}
ref={modalContentRef}
ref={modalContentWrapperRef}
>
<div className={style.content}>
{activeTab === 'workspace' && workspaceId ? (
<Suspense fallback={<WorkspaceDetailSkeleton />}>
<WorkspaceSetting key={workspaceId} workspaceId={workspaceId} />
</Suspense>
) : null}
{generalSettingList.find(v => v.key === activeTab) ? (
<GeneralSetting generalKey={activeTab as GeneralSettingKeys} />
) : null}
{activeTab === 'account' && loginStatus === 'authenticated' ? (
<AccountSetting />
) : null}
</div>
<div className="footer">
<a
href="https://community.affine.pro/home"
target="_blank"
rel="noreferrer"
className={style.suggestionLink}
>
<span className={style.suggestionLinkIcon}>
<ContactWithUsIcon />
</span>
{t['com.affine.settings.suggestion']()}
</a>
<div ref={modalContentRef} className={style.centerContainer}>
<div className={style.content}>
{activeTab === 'workspace' && workspaceId ? (
<Suspense fallback={<WorkspaceDetailSkeleton />}>
<WorkspaceSetting key={workspaceId} workspaceId={workspaceId} />
</Suspense>
) : null}
{generalSettingList.find(v => v.key === activeTab) ? (
<GeneralSetting generalKey={activeTab as GeneralSettingKeys} />
) : null}
{activeTab === 'account' && loginStatus === 'authenticated' ? (
<AccountSetting />
) : null}
</div>
<div className="footer">
<a
href="https://community.affine.pro/home"
target="_blank"
rel="noreferrer"
className={style.suggestionLink}
>
<span className={style.suggestionLinkIcon}>
<ContactWithUsIcon />
</span>
{t['com.affine.settings.suggestion']()}
</a>
</div>
</div>
</div>
</Modal>
@@ -3,21 +3,25 @@ import { style } from '@vanilla-extract/css';
export const wrapper = style({
flexGrow: '1',
height: '100%',
maxWidth: '560px',
margin: '0 auto',
padding: '40px 15px 20px 15px',
// children
// margin: '0 auto',
padding: '40px 15px 20px 15px',
overflowX: 'hidden',
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'center',
justifyContent: 'center',
'::-webkit-scrollbar': {
display: 'none',
},
});
export const centerContainer = style({
width: '100%',
maxWidth: '560px',
});
export const content = style({
width: '100%',
marginBottom: '24px',