feat(core): add ai pricing tip for plans page (#6704)

This commit is contained in:
CatsJuice
2024-04-26 03:28:27 +00:00
parent 8bdd940ac8
commit 5b5c27b6ce
5 changed files with 186 additions and 9 deletions

View File

@@ -0,0 +1,3 @@
import { atom } from 'jotai';
export const settingModalScrollContainerAtom = atom<HTMLElement | null>(null);

View File

@@ -190,6 +190,7 @@ const Settings = () => {
return (
<PlanLayout
aiTip
cloud={
<CloudPlanLayout
caption={cloudCaption}

View File

@@ -1,5 +1,5 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
import { keyframes, style } from '@vanilla-extract/css';
export const plansLayoutRoot = style({
display: 'flex',
flexDirection: 'column',
@@ -70,3 +70,80 @@ export const affineCloudHeader = style({
alignItems: 'center',
marginBottom: 24,
});
export const aiDivider = style({
opacity: 0,
selectors: {
'[data-ai-visible] &': {
opacity: 1,
},
},
});
const slideInBottom = keyframes({
from: {
marginBottom: -100,
},
to: {
marginBottom: 0,
},
});
export const aiScrollTip = style({
position: 'absolute',
zIndex: 1,
bottom: 12,
width: 'var(--setting-modal-content-width)',
background: cssVar('white'),
borderRadius: 8,
border: `1px solid ${cssVar('borderColor')}`,
transition: 'transform 0.36s ease 0.4s, opacity 0.3s ease 0.46s',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 20px 12px 16px',
boxShadow: cssVar('shadow1'),
display: 'flex',
marginBottom: -100,
animation: `${slideInBottom} 0.3s ease 0.5s forwards`,
selectors: {
'[data-ai-visible] &': {
transform: 'translateY(100px)',
opacity: 0,
},
},
});
export const aiScrollTipLabel = style({
display: 'flex',
alignItems: 'center',
});
export const aiScrollTipText = style({
padding: '0px 10px 0px 8px',
fontSize: cssVar('fontSm'),
fontWeight: 600,
lineHeight: '22px',
color: cssVar('textPrimaryColor'),
});
export const aiScrollTipTag = style({
background: 'linear-gradient(180deg, #41B0FF 0%, #0873BE 100%)',
borderRadius: 3,
fontWeight: 600,
fontSize: 10,
lineHeight: '12px',
letterSpacing: '-1%',
color: cssVar('pureWhite'),
boxShadow:
'0px 0px 1px 0px #45474926, 1px 2px 2px 0px #45474921, 2px 4px 3px 0px #45474914, 4px 6px 3px 0px #45474905, 6px 10px 3px 0px #45474900',
padding: 1,
});
export const aiScrollTipTagInner = style({
borderRadius: 2,
padding: '2px 3px',
fontWeight: 'inherit',
fontSize: 'inherit',
lineHeight: 'inherit',
content: 'var(--content, "")',
letterSpacing: 'inherit',
background:
'linear-gradient(180deg, #56B9FF 0%, #23A4FF 37.88%, #1E96EB 75%)',
});

View File

@@ -1,21 +1,29 @@
import { Divider, IconButton } from '@affine/component';
import { Button, Divider, IconButton } from '@affine/component';
import { SettingHeader } from '@affine/component/setting-components';
import { openSettingModalAtom } from '@affine/core/atoms';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightBigIcon, ArrowUpSmallIcon } from '@blocksuite/icons';
import {
ArrowDownBigIcon,
ArrowRightBigIcon,
ArrowUpSmallIcon,
} from '@blocksuite/icons';
import * as Collapsible from '@radix-ui/react-collapsible';
import * as ScrollArea from '@radix-ui/react-scroll-area';
import { useAtom } from 'jotai';
import { cssVar } from '@toeverything/theme';
import { useAtom, useAtomValue } from 'jotai';
import {
type HtmlHTMLAttributes,
type PropsWithChildren,
type ReactNode,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import { createPortal } from 'react-dom';
import { settingModalScrollContainerAtom } from '../../atoms';
import * as styles from './layout.css';
export const SeeAllLink = () => {
@@ -70,12 +78,32 @@ const PricingCollapsible = ({
export interface PlanLayoutProps {
cloud?: ReactNode;
ai?: ReactNode;
aiTip?: boolean;
}
export const PlanLayout = ({ cloud, ai }: PlanLayoutProps) => {
export const PlanLayout = ({ cloud, ai, aiTip }: PlanLayoutProps) => {
const t = useAFFiNEI18N();
const [{ scrollAnchor }, setOpenSettingModal] = useAtom(openSettingModalAtom);
const aiPricingPlanRef = useRef<HTMLDivElement>(null);
const aiScrollTipRef = useRef<HTMLDivElement>(null);
const settingModalScrollContainer = useAtomValue(
settingModalScrollContainerAtom
);
const updateAiTipState = useCallback(() => {
if (!aiTip) return;
const aiContainer = aiPricingPlanRef.current;
if (!settingModalScrollContainer || !aiContainer) return;
const minVisibleHeight = 30;
const containerRect = settingModalScrollContainer.getBoundingClientRect();
const aiTop = aiContainer.getBoundingClientRect().top - containerRect.top;
const aiIntoView = aiTop < containerRect.height - minVisibleHeight;
if (aiIntoView) {
settingModalScrollContainer.dataset.aiVisible = '';
}
}, [aiTip, settingModalScrollContainer]);
// TODO: Need a better solution to handle this situation
useLayoutEffect(() => {
@@ -88,6 +116,23 @@ export const PlanLayout = ({ cloud, ai }: PlanLayoutProps) => {
});
}, [scrollAnchor, setOpenSettingModal]);
useEffect(() => {
if (!settingModalScrollContainer || !aiScrollTipRef.current) return;
settingModalScrollContainer.addEventListener('scroll', updateAiTipState);
updateAiTipState();
return () => {
settingModalScrollContainer.removeEventListener(
'scroll',
updateAiTipState
);
};
}, [settingModalScrollContainer, updateAiTipState]);
const scrollAiIntoView = useCallback(() => {
aiPricingPlanRef.current?.scrollIntoView({ behavior: 'smooth' });
}, []);
return (
<div className={styles.plansLayoutRoot}>
{/* TODO: SettingHeader component shouldn't have margin itself */}
@@ -98,12 +143,35 @@ export const PlanLayout = ({ cloud, ai }: PlanLayoutProps) => {
{cloud}
{ai ? (
<>
<Divider />
<Divider className={styles.aiDivider} />
<div ref={aiPricingPlanRef} id="aiPricingPlan">
{ai}
</div>
</>
) : null}
{aiTip && settingModalScrollContainer
? createPortal(
<div className={styles.aiScrollTip} ref={aiScrollTipRef}>
<div className={styles.aiScrollTipLabel}>
<ArrowDownBigIcon
width={24}
height={24}
color={cssVar('iconColor')}
/>
<div className={styles.aiScrollTipText}>Meet AFFiNE AI</div>
<div className={styles.aiScrollTipTag}>
<div className={styles.aiScrollTipTagInner}>NEW</div>
</div>
</div>
<Button onClick={scrollAiIntoView} type="primary">
View
</Button>
</div>,
settingModalScrollContainer,
'aiScrollTip'
)
: null}
</div>
);
};

View File

@@ -16,9 +16,16 @@ import {
} from '@toeverything/infra';
import { useSetAtom } from 'jotai';
import { debounce } from 'lodash-es';
import { Suspense, useCallback, useLayoutEffect, useRef } from 'react';
import {
Suspense,
useCallback,
useEffect,
useLayoutEffect,
useRef,
} from 'react';
import { AccountSetting } from './account-setting';
import { settingModalScrollContainerAtom } from './atoms';
import { GeneralSetting } from './general-setting';
import { SettingSidebar } from './setting-sidebar';
import * as style from './style.css';
@@ -56,6 +63,9 @@ const SettingModalInner = ({
const modalContentRef = useRef<HTMLDivElement>(null);
const modalContentWrapperRef = useRef<HTMLDivElement>(null);
const setSettingModalScrollContainer = useSetAtom(
settingModalScrollContainerAtom
);
useLayoutEffect(() => {
if (!modalProps.open) return;
@@ -66,13 +76,24 @@ const SettingModalInner = ({
if (!modalContentRef.current || !modalContentWrapperRef.current) return;
const wrapperWidth = modalContentWrapperRef.current.offsetWidth;
const wrapperHeight = modalContentWrapperRef.current.offsetHeight;
const contentWidth = modalContentRef.current.offsetWidth;
modalContentRef.current?.style.setProperty(
const wrapper = modalContentWrapperRef.current;
wrapper?.style.setProperty(
'--setting-modal-width',
`${wrapperWidth}px`
);
modalContentRef.current?.style.setProperty(
wrapper?.style.setProperty(
'--setting-modal-height',
`${wrapperHeight}px`
);
wrapper?.style.setProperty(
'--setting-modal-content-width',
`${contentWidth}px`
);
wrapper?.style.setProperty(
'--setting-modal-gap-x',
`${(wrapperWidth - contentWidth) / 2}px`
);
@@ -87,6 +108,13 @@ const SettingModalInner = ({
};
}, [modalProps.open]);
useEffect(() => {
setSettingModalScrollContainer(modalContentWrapperRef.current);
return () => {
setSettingModalScrollContainer(null);
};
}, [setSettingModalScrollContainer]);
const onTabChange = useCallback(
(key: ActiveTab, meta: WorkspaceMetadata | null) => {
onSettingClick({ activeTab: key, workspaceMetadata: meta });