feat: onboarding page (#5277)

This commit is contained in:
DarkSky
2023-12-19 13:54:41 +00:00
parent 31b1b2dade
commit 8ea910a2bb
12 changed files with 401 additions and 13 deletions

View File

@@ -8,6 +8,7 @@ export * from './confirm-change-email';
export * from './count-down-render';
export * from './modal';
export * from './modal-header';
export * from './onboarding-page';
export * from './password-input';
export * from './set-password-page';
export * from './sign-in-page-container';

View File

@@ -0,0 +1,141 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const scrollableContainer = style({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
padding: '0 200px',
'@media': {
'screen and (max-width: 1024px)': {
padding: '80px 36px',
alignItems: 'center',
},
},
});
export const onboardingContainer = style({
maxWidth: '600px',
padding: '160px 0',
'@media': {
'screen and (max-width: 1024px)': {
padding: '40px 0',
width: '100%',
},
},
});
export const content = style({
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
gap: '36px',
minHeight: '450px',
});
export const question = style({
color: 'var(--affine-text-color)',
fontFamily: 'Inter',
fontSize: 'var(--affine-font-h-1)',
fontStyle: 'normal',
fontWeight: 600,
lineHeight: '36px',
});
export const optionsWrapper = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: '16px',
// flexShrink: 0,
flexGrow: 1,
});
export const buttonWrapper = style({
display: 'flex',
flexDirection: 'row',
alignItems: 'flex-start',
gap: '24px',
flexShrink: 0,
});
export const checkBox = style({
alignItems: 'center',
fontSize: '24px',
});
globalStyle(`${checkBox} svg`, {
color: 'var(--affine-brand-color)',
flexShrink: 0,
marginRight: '8px',
});
export const label = style({
fontSize: 'var(--affine-font-base)',
fontWeight: 500,
});
export const input = style({
width: '520px',
'@media': {
'screen and (max-width: 768px)': {
width: '100%',
},
},
});
export const button = style({
fontWeight: 600,
fontSize: 'var(--affine-font-base)',
});
export const openAFFiNEButton = style({
alignSelf: 'flex-start',
});
export const rightCornerButton = style({
position: 'absolute',
top: '24px',
right: '24px',
});
export const thankContainer = style({
display: 'flex',
flexDirection: 'column',
gap: '24px',
});
export const thankTitle = style({
fontSize: 'var(--affine-font-title)',
fontWeight: '700',
lineHeight: '44px',
});
export const thankText = style({
fontSize: 'var(--affine-font-h-6)',
height: '300px',
fontWeight: '600',
lineHeight: '26px',
});
export const linkGroup = style({
display: 'flex',
position: 'absolute',
bottom: '24px',
right: '24px',
fontSize: 'var(--affine-font-xs)',
height: '16px',
gap: '6px',
width: '100%',
justifyContent: 'flex-end',
backgroundColor: 'var(--affine-background-color)',
});
export const link = style({
color: 'var(--affine-text-secondary-color)',
selectors: {
'&:visited': {
color: 'var(--affine-text-secondary-color)',
},
},
});

View File

@@ -0,0 +1,210 @@
import { fetchWithTraceReport } from '@affine/graphql';
import { ArrowRightSmallIcon } from '@blocksuite/icons';
import clsx from 'clsx';
import { useMemo, useState } from 'react';
import useSWR from 'swr';
import { Button } from '../../ui/button';
import { Checkbox } from '../../ui/checkbox';
import { Divider } from '../../ui/divider';
import Input from '../../ui/input';
import { ScrollableContainer } from '../../ui/scrollbar';
import * as styles from './onboarding-page.css';
import type { User } from './type';
type QuestionOption = {
type: 'checkbox' | 'input';
label: string;
value: string;
};
type Question = {
id?: string;
question: string;
options?: QuestionOption[];
};
type QuestionnaireAnswer = {
form: string;
ask: string;
answer: string[];
};
export const ScrollableLayout = ({
children,
}: {
children: React.ReactNode;
}) => {
return (
<ScrollableContainer className={styles.scrollableContainer}>
<div className={styles.onboardingContainer}>{children}</div>
<div className={styles.linkGroup}>
<a
className={styles.link}
href="https://affine.pro/terms"
target="_blank"
rel="noreferrer"
>
Terms of Conditions
</a>
<Divider orientation="vertical" />
<a
className={styles.link}
href="https://affine.pro/privacy"
target="_blank"
rel="noreferrer"
>
Privacy Policy
</a>
</div>
</ScrollableContainer>
);
};
export const OnboardingPage = ({
user,
onOpenAffine,
}: {
user: User;
onOpenAffine: () => void;
}) => {
const [questionIdx, setQuestionIdx] = useState(0);
const { data: questions } = useSWR<Question[]>(
'/api/worker/questionnaire',
url => fetchWithTraceReport(url).then(r => r.json()),
{ suspense: true, revalidateOnFocus: false }
);
const [options, setOptions] = useState(new Set<string>());
const [inputs, setInputs] = useState<Record<string, string>>({});
const question = useMemo(
() => questions?.[questionIdx],
[questionIdx, questions]
);
if (!questions) {
return null;
}
if (question) {
return (
<ScrollableLayout>
<div className={styles.content}>
<h1 className={styles.question}>{question.question}</h1>
<div className={styles.optionsWrapper}>
{question.options &&
question.options.length > 0 &&
question.options.map((option, optionIndex) => {
if (option.type === 'checkbox') {
return (
<Checkbox
key={optionIndex}
name={option.value}
className={styles.checkBox}
labelClassName={styles.label}
checked={options.has(option.value)}
onChange={e => {
setOptions(set => {
if (e.target.checked) {
set.add(option.value);
} else {
set.delete(option.value);
}
return new Set(set);
});
}}
label={option.label}
/>
);
} else if (option.type === 'input') {
return (
<Input
key={optionIndex}
className={styles.input}
type="text"
size="large"
placeholder={option.label}
value={inputs[option.value] || ''}
onChange={value =>
setInputs(prev => ({ ...prev, [option.value]: value }))
}
/>
);
}
return null;
})}
</div>
<div className={styles.buttonWrapper}>
<Button
className={clsx(styles.button, {
[styles.rightCornerButton]: questionIdx !== 0,
})}
size="extraLarge"
onClick={() => setQuestionIdx(questions.length)}
>
Skip
</Button>
<Button
className={styles.button}
type="primary"
size="extraLarge"
itemType="submit"
onClick={() => {
if (question.id && user?.id) {
const answer: QuestionnaireAnswer = {
form: user.id,
ask: question.id,
answer: [
...Array.from(options),
...Object.entries(inputs).map(
([key, value]) => `${key}:${value}`
),
],
};
// eslint-disable-next-line @typescript-eslint/no-floating-promises
fetchWithTraceReport('/api/worker/questionnaire', {
method: 'POST',
body: JSON.stringify(answer),
}).finally(() => {
setOptions(new Set());
setInputs({});
setQuestionIdx(questionIdx + 1);
});
} else {
setQuestionIdx(questionIdx + 1);
}
}}
iconPosition="end"
icon={<ArrowRightSmallIcon />}
>
{questionIdx === 0 ? 'start' : 'Next'}
</Button>
</div>
</div>
</ScrollableLayout>
);
}
return (
<ScrollableLayout>
<div className={styles.thankContainer}>
<h1 className={styles.thankTitle}>Thank you!</h1>
<p className={styles.thankText}>
We will continue to enhance our products based on your feedback. Thank
you once again for your supports.
</p>
<Button
className={clsx(styles.button, styles.openAFFiNEButton)}
type="primary"
size="extraLarge"
onClick={onOpenAffine}
iconPosition="end"
icon={<ArrowRightSmallIcon />}
>
Get Started
</Button>
</div>
</ScrollableLayout>
);
};