mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-05 03:19:52 +08:00
feat(core): auto scroll to current payment plan (#4714)
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
|||||||
|
export function BulledListIcon({ color = 'currentColor' }: { color: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<ellipse
|
||||||
|
cx="4.00008"
|
||||||
|
cy="7.99984"
|
||||||
|
rx="1.33333"
|
||||||
|
ry="1.33333"
|
||||||
|
fill={color}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
+63
-26
@@ -9,6 +9,7 @@ import {
|
|||||||
updateSubscriptionMutation,
|
updateSubscriptionMutation,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
||||||
|
import { DoneIcon } from '@blocksuite/icons';
|
||||||
import { Button } from '@toeverything/components/button';
|
import { Button } from '@toeverything/components/button';
|
||||||
import {
|
import {
|
||||||
type PropsWithChildren,
|
type PropsWithChildren,
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
type SubscriptionMutator,
|
type SubscriptionMutator,
|
||||||
useUserSubscription,
|
useUserSubscription,
|
||||||
} from '../../../../../hooks/use-subscription';
|
} from '../../../../../hooks/use-subscription';
|
||||||
|
import { BulledListIcon } from './icons/bulled-list';
|
||||||
import * as styles from './style.css';
|
import * as styles from './style.css';
|
||||||
|
|
||||||
interface FixedPrice {
|
interface FixedPrice {
|
||||||
@@ -108,6 +110,7 @@ const planDetail = new Map<SubscriptionPlan, FixedPrice | DynamicPrice>([
|
|||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
const [subscription, mutateSubscription] = useUserSubscription();
|
const [subscription, mutateSubscription] = useUserSubscription();
|
||||||
const loggedIn = useCurrentLoginStatus() === 'authenticated';
|
const loggedIn = useCurrentLoginStatus() === 'authenticated';
|
||||||
|
const scrollWrapper = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: { prices },
|
data: { prices },
|
||||||
@@ -139,6 +142,32 @@ const Settings = () => {
|
|||||||
planDetail.get(SubscriptionPlan.Pro) as FixedPrice | undefined
|
planDetail.get(SubscriptionPlan.Pro) as FixedPrice | undefined
|
||||||
)?.discount;
|
)?.discount;
|
||||||
|
|
||||||
|
// auto scroll to current plan card
|
||||||
|
useEffect(() => {
|
||||||
|
if (!scrollWrapper.current) return;
|
||||||
|
const currentPlanCard = scrollWrapper.current?.querySelector(
|
||||||
|
'[data-current="true"]'
|
||||||
|
);
|
||||||
|
const wrapperComputedStyle = getComputedStyle(scrollWrapper.current);
|
||||||
|
const left = currentPlanCard
|
||||||
|
? currentPlanCard.getBoundingClientRect().left -
|
||||||
|
scrollWrapper.current.getBoundingClientRect().left -
|
||||||
|
parseInt(wrapperComputedStyle.paddingLeft)
|
||||||
|
: 0;
|
||||||
|
const appeared =
|
||||||
|
scrollWrapper.current.getAttribute('data-appeared') === 'true';
|
||||||
|
const animationFrameId = requestAnimationFrame(() => {
|
||||||
|
scrollWrapper.current?.scrollTo({
|
||||||
|
behavior: appeared ? 'smooth' : 'instant',
|
||||||
|
left,
|
||||||
|
});
|
||||||
|
scrollWrapper.current?.setAttribute('data-appeared', 'true');
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
};
|
||||||
|
}, [recurring]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingHeader
|
<SettingHeader
|
||||||
@@ -175,18 +204,19 @@ const Settings = () => {
|
|||||||
</RadioButton>
|
</RadioButton>
|
||||||
))}
|
))}
|
||||||
</RadioButtonGroup>
|
</RadioButtonGroup>
|
||||||
{/* TODO: plan cards horizontal scroll behavior is not the same as design */}
|
<div className={styles.planCardsWrapper} ref={scrollWrapper}>
|
||||||
{/* TODO: may scroll current plan into view when first loading? */}
|
|
||||||
<div className={styles.planCardsWrapper}>
|
|
||||||
{Array.from(planDetail.values()).map(detail => {
|
{Array.from(planDetail.values()).map(detail => {
|
||||||
|
const isCurrent =
|
||||||
|
loggedIn &&
|
||||||
|
detail.plan === currentPlan &&
|
||||||
|
(currentPlan === SubscriptionPlan.Free
|
||||||
|
? true
|
||||||
|
: currentRecurring === recurring);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
data-current={isCurrent}
|
||||||
key={detail.plan}
|
key={detail.plan}
|
||||||
className={
|
className={isCurrent ? styles.currentPlanCard : styles.planCard}
|
||||||
loggedIn && currentPlan === detail.plan
|
|
||||||
? styles.currentPlanCard
|
|
||||||
: styles.planCard
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className={styles.planTitle}>
|
<div className={styles.planTitle}>
|
||||||
<p>
|
<p>
|
||||||
@@ -198,23 +228,27 @@ const Settings = () => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<div className={styles.planPriceWrapper}>
|
||||||
{detail.type === 'dynamic' ? (
|
<p>
|
||||||
<span className={styles.planPriceDesc}>
|
{detail.type === 'dynamic' ? (
|
||||||
Coming soon...
|
<span className={styles.planPriceDesc}>
|
||||||
</span>
|
Coming soon...
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className={styles.planPrice}>
|
|
||||||
$
|
|
||||||
{recurring === SubscriptionRecurring.Monthly
|
|
||||||
? detail.price
|
|
||||||
: detail.yearlyPrice}
|
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.planPriceDesc}>per month</span>
|
) : (
|
||||||
</>
|
<>
|
||||||
)}
|
<span className={styles.planPrice}>
|
||||||
</p>
|
$
|
||||||
|
{recurring === SubscriptionRecurring.Monthly
|
||||||
|
? detail.price
|
||||||
|
: detail.yearlyPrice}
|
||||||
|
</span>
|
||||||
|
<span className={styles.planPriceDesc}>
|
||||||
|
per month
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{
|
{
|
||||||
// branches:
|
// branches:
|
||||||
// if contact => 'Contact Sales'
|
// if contact => 'Contact Sales'
|
||||||
@@ -264,8 +298,11 @@ const Settings = () => {
|
|||||||
{detail.benefits.map((content, i) => (
|
{detail.benefits.map((content, i) => (
|
||||||
<div key={i} className={styles.planBenefit}>
|
<div key={i} className={styles.planBenefit}>
|
||||||
<div className={styles.planBenefitIcon}>
|
<div className={styles.planBenefitIcon}>
|
||||||
{/* TODO: icons */}
|
{detail.type == 'dynamic' ? (
|
||||||
{detail.type == 'dynamic' ? '·' : '✅'}
|
<BulledListIcon color="var(--affine-primary-color)" />
|
||||||
|
) : (
|
||||||
|
<DoneIcon color="var(--affine-primary-color)" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+9
@@ -22,6 +22,8 @@ export const planCardsWrapper = style({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
overflowX: 'auto',
|
overflowX: 'auto',
|
||||||
scrollSnapType: 'x mandatory',
|
scrollSnapType: 'x mandatory',
|
||||||
|
// TODO: should display the horizontal scrollbar, ensure the box-shadow is not clipped
|
||||||
|
paddingBottom: '21px',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const planCard = style({
|
export const planCard = style({
|
||||||
@@ -75,6 +77,13 @@ export const planTitle = style({
|
|||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const planPriceWrapper = style({
|
||||||
|
minHeight: '28px',
|
||||||
|
lineHeight: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
});
|
||||||
|
|
||||||
export const planPrice = style({
|
export const planPrice = style({
|
||||||
fontSize: 'var(--affine-font-h-5)',
|
fontSize: 'var(--affine-font-h-5)',
|
||||||
marginRight: '8px',
|
marginRight: '8px',
|
||||||
|
|||||||
Reference in New Issue
Block a user